🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

opencode-copilot-plugin

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-copilot-plugin - npm Package Compare versions

Comparing version
0.4.2
to
0.5.0
+291
-5
dist/index.js
// index.ts
import * as path8 from "node:path";
import * as path11 from "node:path";

@@ -758,3 +758,255 @@ // src/file-tracker.ts

}
// src/prompts/discovery.ts
import * as path8 from "node:path";
import * as os7 from "node:os";
import * as fs5 from "node:fs/promises";
import matter4 from "gray-matter";
var LOCAL_PROMPTS_SUBDIR = path8.join(".github", "prompts");
var GLOBAL_PROMPTS_DIR = path8.join(os7.homedir(), ".copilot", "prompts");
async function discoverLocalPrompts(rootDir) {
return collectPromptFiles(path8.join(rootDir, LOCAL_PROMPTS_SUBDIR), "local");
}
async function discoverGlobalPrompts(globalDir = GLOBAL_PROMPTS_DIR) {
return collectPromptFiles(globalDir, "global");
}
function mergePrompts(local, global) {
const localNames = new Set(local.map((p) => p.name));
const filteredGlobal = global.filter((p) => {
if (localNames.has(p.name)) {
process.stderr.write(`[opencode-copilot-plugin] Skipping global prompt "${p.name}": overridden by a local prompt with the same name
`);
return false;
}
return true;
});
return [...local, ...filteredGlobal];
}
async function collectPromptFiles(dir, scope) {
const results = [];
let entries;
try {
entries = await fs5.readdir(dir, { withFileTypes: true, encoding: "utf8" });
} catch {
return results;
}
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".prompt.md"))
continue;
const filePath = path8.join(dir, entry.name);
const prompt = await parsePromptFile(filePath, dir, scope);
if (prompt)
results.push(prompt);
}
return results;
}
async function parsePromptFile(filePath, dirPath, scope) {
let raw;
try {
raw = await fs5.readFile(filePath, "utf8");
} catch {
return null;
}
const { data: frontmatter, content } = matter4(raw);
const fm = frontmatter;
const name = derivePromptName(path8.basename(filePath));
const fmName = fm.name;
if (typeof fmName === "string" && fmName !== name) {
process.stderr.write(`[opencode-copilot-plugin] Warning: "name" in ${filePath} is "${fmName}" but filename-derived name is "${name}". Using filename-derived name as canonical name.
`);
}
const description = typeof fm.description === "string" && fm.description.trim() ? fm.description.trim() : name;
const trimmedContent = content.trim();
const args = extractArguments(trimmedContent);
return {
name,
description,
scope,
dirPath,
filePath,
content: trimmedContent,
frontmatter: fm,
arguments: args
};
}
function derivePromptName(filename) {
if (filename.endsWith(".prompt.md"))
return filename.slice(0, -".prompt.md".length);
return filename;
}
function extractArguments(content) {
const seen = new Set;
const args = [];
const vscodePattern = /\$\{input:([^}:]+)(?::([^}]*))?\}/g;
let match;
while ((match = vscodePattern.exec(content)) !== null) {
const id = match[1].trim();
const placeholder = match[2]?.trim();
if (!seen.has(id)) {
seen.add(id);
args.push({ id, placeholder: placeholder || undefined });
}
}
const crushPattern = /(?<!\{)\$([A-Z][A-Z0-9_]*)(?!\{)/g;
while ((match = crushPattern.exec(content)) !== null) {
const id = match[1];
if (!seen.has(id)) {
seen.add(id);
args.push({ id });
}
}
return args;
}
function substituteArguments(content, args, argValues) {
let result = content;
for (const arg of args) {
const value = argValues[arg.id];
if (value === undefined)
continue;
result = result.replace(new RegExp(`\\$\\{input:${escapeRegex(arg.id)}(?::[^}]*)?\\}`, "g"), value);
result = result.replace(new RegExp(`(?<!\\{)\\$${escapeRegex(arg.id)}(?!\\{)`, "g"), value);
}
return result;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// src/prompts/resolve-references.ts
import * as path9 from "node:path";
import * as fs6 from "node:fs/promises";
var MAX_TOTAL_RESOLVED_BYTES = 1e5;
var MAX_REFERENCE_DEPTH = 5;
async function resolveFileReferences(content, promptDir) {
const state = { totalBytes: 0, visited: new Set };
return resolveContent(content, promptDir, state, 0);
}
async function resolveContent(content, baseDir, state, depth) {
if (depth >= MAX_REFERENCE_DEPTH)
return content;
const linkPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
const segments = [];
let lastIndex = 0;
let match;
while ((match = linkPattern.exec(content)) !== null) {
const fullMatch = match[0];
const label = match[1] ?? "";
const href = match[2] ?? "";
if (isUrl(href) || href.startsWith("#")) {
segments.push(content.slice(lastIndex, match.index + fullMatch.length));
lastIndex = match.index + fullMatch.length;
continue;
}
segments.push(content.slice(lastIndex, match.index));
lastIndex = match.index + fullMatch.length;
const absolutePath = path9.resolve(baseDir, href);
if (state.visited.has(absolutePath)) {
process.stderr.write(`[opencode-copilot-plugin] Skipping circular reference to ${absolutePath} in prompt
`);
segments.push(fullMatch);
continue;
}
if (state.totalBytes >= MAX_TOTAL_RESOLVED_BYTES) {
segments.push(fullMatch);
continue;
}
let fileContent;
try {
fileContent = await fs6.readFile(absolutePath, "utf8");
} catch {
process.stderr.write(`[opencode-copilot-plugin] Warning: could not read referenced file "${absolutePath}" — leaving link as-is
`);
segments.push(fullMatch);
continue;
}
state.visited.add(absolutePath);
state.totalBytes += Buffer.byteLength(fileContent, "utf8");
const resolvedFileContent = await resolveContent(fileContent, path9.dirname(absolutePath), state, depth + 1);
state.visited.delete(absolutePath);
const relPath = href;
const block = buildReferencedFileBlock(label, relPath, resolvedFileContent);
segments.push(block);
}
segments.push(content.slice(lastIndex));
return segments.join("");
}
function buildReferencedFileBlock(label, relPath, content) {
const lines = [
`<referenced_file path="${relPath}">`,
content,
`</referenced_file>`
];
if (label.trim()) {
return `${label}
${lines.join(`
`)}`;
}
return lines.join(`
`);
}
function isUrl(href) {
return /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//.test(href);
}
// src/prompts/format.ts
import * as os8 from "node:os";
import * as path10 from "node:path";
var home3 = os8.homedir();
function toDisplayPath3(filePath) {
return filePath.startsWith(home3) ? filePath.replace(home3, "~") : filePath;
}
function formatPromptHeader(prompt) {
const displayPath = toDisplayPath3(prompt.filePath);
const scopeLabel = prompt.scope === "local" ? ".github/prompts" : "~/.copilot/prompts";
const fileName = path10.basename(prompt.filePath);
const lines = [
`## Copilot Prompt: ${prompt.name} (from \`${scopeLabel}/${fileName}\`)`
];
if (prompt.description && prompt.description !== prompt.name) {
lines.push(`> ${prompt.description}`);
}
const notes = buildInformationalNotes(prompt);
if (notes.length > 0) {
lines.push("", ...notes);
}
lines.push("");
return lines.join(`
`);
}
function buildInformationalNotes(prompt) {
const notes = [];
const fm = prompt.frontmatter;
if (fm.agent) {
notes.push(`> **Note:** This prompt specifies agent \`${fm.agent}\` — informational only in OpenCode.`);
}
if (fm.model) {
notes.push(`> **Note:** This prompt prefers model \`${fm.model}\` — informational only in OpenCode.`);
}
if (fm.tools && fm.tools.length > 0) {
const toolList = fm.tools.map((t) => `\`${t}\``).join(", ");
notes.push(`> **Note:** This prompt specifies tools [${toolList}] — informational only in OpenCode.`);
}
return notes;
}
function parseCommandArguments(rawArguments, promptArgs) {
const result = {};
if (!rawArguments.trim())
return result;
const hasKeyValue = rawArguments.includes("=");
if (hasKeyValue) {
const kvPattern = /([A-Za-z_][A-Za-z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]+)/g;
let match;
while ((match = kvPattern.exec(rawArguments)) !== null) {
const key = match[1];
let value = match[2];
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1).replace(/\\(.)/g, "$1");
}
result[key] = value;
}
} else if (promptArgs.length > 0) {
const firstArg = promptArgs[0];
if (firstArg) {
result[firstArg.id] = rawArguments.trim();
}
}
return result;
}
// index.ts

@@ -776,5 +1028,9 @@ var FILE_TOOLS = new Set(["read", "edit", "write", "patch"]);

let allAgents = mergeAgents(localAgents, globalAgents);
const localAgentsDir = path8.join(rootDir, LOCAL_AGENTS_SUBDIR);
const localSkillsDir = path8.join(rootDir, LOCAL_SKILLS_SUBDIR);
await log(client, "info", `Loaded ${projectInstructions.length} project instruction(s), ` + `${globalInstructions.length} global instruction(s), ` + `${allSkills.length} skill(s) (${localSkills.length} local, ${globalSkills.length} global), ` + `${Object.keys(hookRegistry).length} hook type(s), ` + `${allAgents.length} agent(s) (${localAgents.length} local, ${globalAgents.length} global)`);
let localPrompts = await discoverLocalPrompts(rootDir);
let globalPrompts = await discoverGlobalPrompts();
let allPrompts = mergePrompts(localPrompts, globalPrompts);
const localAgentsDir = path11.join(rootDir, LOCAL_AGENTS_SUBDIR);
const localSkillsDir = path11.join(rootDir, LOCAL_SKILLS_SUBDIR);
const localPromptsDir = path11.join(rootDir, LOCAL_PROMPTS_SUBDIR);
await log(client, "info", `Loaded ${projectInstructions.length} project instruction(s), ` + `${globalInstructions.length} global instruction(s), ` + `${allSkills.length} skill(s) (${localSkills.length} local, ${globalSkills.length} global), ` + `${Object.keys(hookRegistry).length} hook type(s), ` + `${allAgents.length} agent(s) (${localAgents.length} local, ${globalAgents.length} global), ` + `${allPrompts.length} prompt(s) (${localPrompts.length} local, ${globalPrompts.length} global)`);
const hooks = {

@@ -912,2 +1168,13 @@ "experimental.chat.system.transform": async (input, output) => {

},
"command.execute.before": async (input, output) => {
const commandBaseName = extractCommandBaseName(input.command);
const prompt = allPrompts.find((p) => p.name === commandBaseName);
if (!prompt)
return;
const resolvedContent = await resolveFileReferences(prompt.content, prompt.dirPath);
const argValues = parseCommandArguments(input.arguments, prompt.arguments);
const finalContent = substituteArguments(resolvedContent, prompt.arguments, argValues);
const header = formatPromptHeader(prompt);
output.parts = [{ type: "text", text: header + finalContent }];
},
event: async ({ event }) => {

@@ -922,2 +1189,4 @@ if (event.type === "file.watcher.updated") {

const isGlobalAgent = changedPath.startsWith(GLOBAL_AGENTS_DIR);
const isLocalPrompt = changedPath.startsWith(localPromptsDir);
const isGlobalPrompt = changedPath.startsWith(GLOBAL_PROMPTS_DIR);
if (isProjectInstruction) {

@@ -951,2 +1220,12 @@ projectInstructions = await discoverInstructions(rootDir);

}
if (isLocalPrompt) {
localPrompts = await discoverLocalPrompts(rootDir);
allPrompts = mergePrompts(localPrompts, globalPrompts);
await log(client, "info", `Hot-reloaded ${allPrompts.length} prompt(s) after change to ${changedPath}`);
}
if (isGlobalPrompt) {
globalPrompts = await discoverGlobalPrompts();
allPrompts = mergePrompts(localPrompts, globalPrompts);
await log(client, "info", `Hot-reloaded ${allPrompts.length} prompt(s) after change to ${changedPath}`);
}
}

@@ -1029,2 +1308,9 @@ if (event.type === "session.created") {

};
function extractCommandBaseName(command) {
const colonIndex = command.indexOf(":");
if (colonIndex === -1)
return command;
const afterPrefix = command.slice(colonIndex + 1);
return afterPrefix.replace(/:/g, "/");
}
async function log(client, level, message) {

@@ -1031,0 +1317,0 @@ try {

+1
-1
{
"name": "opencode-copilot-plugin",
"version": "0.4.2",
"version": "0.5.0",
"description": "OpenCode plugin that brings GitHub Copilot's custom instructions and skills system to OpenCode",

@@ -5,0 +5,0 @@ "type": "module",