@c4a/context-cli 0.5.29-alpha.2 → 0.5.29-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -34
- package/cli.js +43511 -20153
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json.template +1 -1
- package/plugin/.codex-plugin/plugin.json.template +8 -5
- package/plugin/.cursor-plugin/plugin.json.template +29 -0
- package/plugin/README.md +127 -0
- package/plugin/README_CN.md +125 -0
- package/plugin/assets/icon.svg +3 -0
- package/plugin/assets/logo.svg +3 -0
- package/plugin/assets/workflow-en.png +0 -0
- package/plugin/assets/workflow.png +0 -0
- package/plugin/commands/align.md +50 -42
- package/plugin/commands/capture.md +27 -11
- package/plugin/commands/compile.md +91 -36
- package/plugin/commands/context.md +7 -4
- package/plugin/commands/drop.md +14 -6
- package/plugin/commands/extract.md +3 -3
- package/plugin/commands/init.md +48 -9
- package/plugin/commands/purge.md +6 -6
- package/plugin/commands/query.md +11 -3
- package/plugin/commands/status.md +4 -2
- package/plugin/skills/skill-align-workflow/SKILL.md +61 -0
- package/plugin/skills/skill-align-workflow/references/candidate-resolution.md +60 -0
- package/plugin/skills/skill-align-workflow/references/density-profile.md +23 -0
- package/plugin/skills/skill-align-workflow/references/gates.md +95 -0
- package/plugin/skills/skill-compile-close/SKILL.md +127 -0
- package/plugin/skills/skill-compile-draft/SKILL.md +409 -0
- package/plugin/skills/skill-context-query/SKILL.md +184 -0
- package/plugin/skills/skill-drop/SKILL.md +190 -0
- package/plugin/skills/skill-semantic-reconcile/SKILL.md +251 -0
- package/scripts/build-plugin.ts +674 -45
- package/templates/aspects/code/aspect.yaml +21 -0
- package/templates/aspects/code/prompt.md +33 -18
- package/templates/aspects/design-system/prompt.md +2 -3
- package/templates/aspects/graphql/prompt.md +2 -2
- package/templates/aspects/openapi/prompt.md +2 -2
- package/plugin/.claude-plugin/plugin.json +0 -16
- package/plugin/.codex-plugin/plugin.json +0 -35
- package/plugin/commands/capture-aspect.md +0 -17
- package/plugin/commands/capture-code.md +0 -25
- package/plugin/skills/align-finalize/SKILL.md +0 -137
- package/plugin/skills/align-propose/SKILL.md +0 -163
- package/plugin/skills/align-scan/SKILL.md +0 -161
- package/plugin/skills/align-scan/references/data-model.md +0 -343
- package/plugin/skills/align-scan/references/user-question-contract.md +0 -159
- package/plugin/skills/compile-close/SKILL.md +0 -122
- package/plugin/skills/compile-draft/SKILL.md +0 -252
- package/plugin/skills/context-query/SKILL.md +0 -166
- package/plugin/skills/drop/SKILL.md +0 -170
- package/plugin/skills/semantic-reconcile/SKILL.md +0 -129
package/scripts/build-plugin.ts
CHANGED
|
@@ -1,69 +1,698 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* build-plugin.ts — materialize plugin
|
|
4
|
-
*
|
|
3
|
+
* build-plugin.ts — materialize installable plugin trees from the human-edited
|
|
4
|
+
* plugin/ source directory directly into the nested c4a-plugins/ repo
|
|
5
|
+
* (remote: github.com/context4ai/context). c4a-plugins/ is .gitignore'd by
|
|
6
|
+
* c4a; it is its own git repo, the single source of marketplace distribution.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Source of truth:
|
|
9
|
+
* - plugin/commands/
|
|
10
|
+
* - plugin/skills/
|
|
7
11
|
* - plugin/.claude-plugin/plugin.json.template
|
|
8
12
|
* - plugin/.codex-plugin/plugin.json.template
|
|
13
|
+
* - plugin/.cursor-plugin/plugin.json.template
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
15
|
+
* Generated outputs under c4a-plugins/:
|
|
16
|
+
* - claude/ — Claude plugin root
|
|
17
|
+
* - codex/ — Codex plugin root
|
|
18
|
+
* - cursor/ — Cursor plugin root
|
|
19
|
+
* - skills/ — Vercel-style standalone skills (no plugin manifest)
|
|
13
20
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
21
|
+
* Plus three marketplace.json files at c4a-plugins/ root pointing at the
|
|
22
|
+
* respective subdirs, so a single repo serves four install paths.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
19
|
-
import { dirname, resolve } from "node:path";
|
|
25
|
+
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
26
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
20
27
|
import { fileURLToPath } from "node:url";
|
|
21
28
|
|
|
22
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
30
|
const pkgRoot = resolve(__dirname, "..");
|
|
31
|
+
const repoRoot = resolve(pkgRoot, "../..");
|
|
32
|
+
// Integration tests stage build into a temp dir; allow override so the staged
|
|
33
|
+
// run does not require a real c4a-plugins/ clone next to the staged scripts.
|
|
34
|
+
const PLUGINS_ROOT = process.env.C4A_PLUGINS_ROOT
|
|
35
|
+
? resolve(process.env.C4A_PLUGINS_ROOT)
|
|
36
|
+
: resolve(repoRoot, "c4a-plugins");
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
interface CommandSource {
|
|
39
|
+
slug: string;
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
body: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const INTERNAL_PROCEDURES_DIR = "references/internal-procedures";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rewrite `packaged context:skill-*` references in a command body so agents
|
|
49
|
+
* without a packaged-skill registry (Codex / Cursor / Vercel-style) get
|
|
50
|
+
* direct file references they can read instead of an unactionable verb.
|
|
51
|
+
*
|
|
52
|
+
* Claude has a packaged-skill registry and consumes the original phrasing
|
|
53
|
+
* verbatim, so this transformer is only applied to non-Claude builds.
|
|
54
|
+
*/
|
|
55
|
+
function rewritePackagedSkillReferences(body: string, pathFor: (skill: string) => string): string {
|
|
56
|
+
let out = body;
|
|
57
|
+
|
|
58
|
+
// Imperative: "Use/Invoke packaged `context:skill-X` [and follow its procedure]" → read the file
|
|
59
|
+
// The optional trailer is consumed because "Read ... and follow it" already covers it.
|
|
60
|
+
out = out.replace(
|
|
61
|
+
/\b(?:Invoke|Use|invoke|use) packaged `context:(skill-[a-z-]+)`(?:\s+and follow its procedure)?/g,
|
|
62
|
+
(_match, skill: string) => `Read \`${pathFor(skill)}\` in full and follow it`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Imperative: "Feed/Pass <stuff> to packaged `context:skill-X`" → pass to procedure file
|
|
66
|
+
// Do not inject "do not reconstruct from memory" here; the surrounding sentence
|
|
67
|
+
// already carries that warning in its own tail and double-stating it reads redundant.
|
|
68
|
+
out = out.replace(
|
|
69
|
+
/\b(Feed|Pass|feed|pass) ([^.`]*?) to packaged `context:(skill-[a-z-]+)`/g,
|
|
70
|
+
(_match, verb: string, mid: string, skill: string) =>
|
|
71
|
+
`${verb} ${mid} to the procedure in \`${pathFor(skill)}\` (read it in full first if you have not already)`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// "run `context:skill-X`" (no "packaged") → consult the procedure file
|
|
75
|
+
out = out.replace(
|
|
76
|
+
/\brun `context:(skill-[a-z-]+)`/g,
|
|
77
|
+
(_match, skill: string) => `consult the procedure in \`${pathFor(skill)}\``,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Anything left is a descriptive bare reference (e.g. "stop before invoking
|
|
81
|
+
// `context:skill-X`"); substitute the file path so the agent can resolve it.
|
|
82
|
+
// The wildcard form `context:skill-*` (with literal asterisk) stays untouched
|
|
83
|
+
// because the [a-z-]+ class does not match `*`.
|
|
84
|
+
out = out.replace(
|
|
85
|
+
/`context:(skill-[a-z-]+)`/g,
|
|
86
|
+
(_match, skill: string) => `\`${pathFor(skill)}\``,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function publicEntrySkillPath(skill: string): string {
|
|
93
|
+
return `${INTERNAL_PROCEDURES_DIR}/${skill}.md`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cursorCommandSkillPath(skill: string): string {
|
|
97
|
+
// Cursor command files live under `commands/`; bundled skills live under
|
|
98
|
+
// sibling `skills/`. Use a plain relative path that any markdown reader can
|
|
99
|
+
// resolve from the command file's location.
|
|
100
|
+
return `../skills/${skill}/SKILL.md`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cursorCommandFileName(slug: string): string {
|
|
104
|
+
return slug === "context" ? "context.md" : `context-${slug}.md`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rewriteClaudeSlashCommandsForCursor(body: string): string {
|
|
108
|
+
return body
|
|
109
|
+
.replace(/\/context:\*/g, "/context-*")
|
|
110
|
+
.replace(/\/context:([a-z-]+)/g, (_match, slug: string) =>
|
|
111
|
+
slug === "context" ? "/context" : `/context-${slug}`
|
|
112
|
+
)
|
|
113
|
+
.replace(/^## Your task$/gm, "## Workflow")
|
|
114
|
+
.replace(/\bPackaged skill invoked by\b/g, "Internal procedure invoked by")
|
|
115
|
+
.replace(/\bnot a user slash command\b/g, "not a user command");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scan a generated build root for residual packaged-skill references that
|
|
120
|
+
* should have been rewritten in agent-facing entry files (Codex / Vercel
|
|
121
|
+
* public skills, Cursor commands).
|
|
122
|
+
*
|
|
123
|
+
* Skips internal skill bodies — directories named `internal-procedures` (the
|
|
124
|
+
* codex/vercel embed point) and any directory whose name starts with `skill-`
|
|
125
|
+
* (the cursor-build internal skills tree, mirrored from `plugin/skills/`).
|
|
126
|
+
* Cross-skill mentions inside those bodies are descriptive ("the next stage
|
|
127
|
+
* handles X") and stay untouched.
|
|
128
|
+
*
|
|
129
|
+
* Throws on any leak so future commands or rewrite-pattern gaps fail the
|
|
130
|
+
* build instead of silently shipping unactionable verbs.
|
|
131
|
+
*/
|
|
132
|
+
async function verifyNoPackagedSkillLeak(root: string, label: string): Promise<void> {
|
|
133
|
+
const offenders: string[] = [];
|
|
134
|
+
const stack: string[] = [root];
|
|
135
|
+
while (stack.length > 0) {
|
|
136
|
+
const current = stack.pop()!;
|
|
137
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const path = join(current, entry.name);
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
if (entry.name === "internal-procedures" || entry.name.startsWith("skill-")) continue;
|
|
142
|
+
stack.push(path);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
146
|
+
const content = await readFile(path, "utf8");
|
|
147
|
+
const matches = content.match(/`context:skill-[a-z-]+`/g);
|
|
148
|
+
if (matches && matches.length > 0) {
|
|
149
|
+
offenders.push(`${path}: ${[...new Set(matches)].join(", ")}`);
|
|
150
|
+
}
|
|
55
151
|
}
|
|
56
|
-
|
|
57
|
-
|
|
152
|
+
}
|
|
153
|
+
if (offenders.length > 0) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`${label} build still contains unrewritten packaged skill references in agent-facing entry files — extend rewritePackagedSkillReferences() to cover the new phrasing:\n${offenders.join("\n")}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rewrite `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` cross-skill references
|
|
162
|
+
* inside bundled internal procedure copies to plain relative paths that any
|
|
163
|
+
* agent can resolve. The number of `..` segments depends on how deep the
|
|
164
|
+
* file is below its build's "skills siblings root" (the directory under
|
|
165
|
+
* which all `skill-*` siblings live).
|
|
166
|
+
*/
|
|
167
|
+
function rewriteClaudePluginRoot(content: string, levelsAboveSiblingsRoot: number): string {
|
|
168
|
+
const upPrefix = "../".repeat(levelsAboveSiblingsRoot);
|
|
169
|
+
return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/skills\//g, upPrefix);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function depthOfFileBelowSiblingsRoot(filePath: string, siblingsRoot: string): number {
|
|
173
|
+
const rel = relative(siblingsRoot, dirname(filePath));
|
|
174
|
+
if (rel === "" || rel === ".") return 0;
|
|
175
|
+
return rel.split(sep).filter((seg: string) => seg.length > 0).length;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Recursively copy a markdown tree, applying `rewriteClaudePluginRoot` to
|
|
180
|
+
* every `.md` file based on its location below `siblingsRoot`. Non-markdown
|
|
181
|
+
* files are copied verbatim. Empty subtrees (no files, no recursive content)
|
|
182
|
+
* are skipped — the destination directory is created only when at least one
|
|
183
|
+
* descendant is actually written, so accidental empty source dirs don't
|
|
184
|
+
* propagate into build outputs.
|
|
185
|
+
*/
|
|
186
|
+
async function copyMarkdownTreeRewrittenForPublicEntry(
|
|
187
|
+
src: string,
|
|
188
|
+
dest: string,
|
|
189
|
+
siblingsRoot: string,
|
|
190
|
+
): Promise<boolean> {
|
|
191
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
192
|
+
let wroteSomething = false;
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const srcPath = join(src, entry.name);
|
|
195
|
+
const destPath = join(dest, entry.name);
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
const subtreeWrote = await copyMarkdownTreeRewrittenForPublicEntry(srcPath, destPath, siblingsRoot);
|
|
198
|
+
if (subtreeWrote) wroteSomething = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!entry.isFile()) continue;
|
|
202
|
+
if (!wroteSomething) {
|
|
203
|
+
await mkdir(dest, { recursive: true });
|
|
58
204
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
205
|
+
if (entry.name.endsWith(".md")) {
|
|
206
|
+
const content = await readFile(srcPath, "utf8");
|
|
207
|
+
const depth = depthOfFileBelowSiblingsRoot(destPath, siblingsRoot);
|
|
208
|
+
await writeFile(destPath, rewriteClaudePluginRoot(content, depth), "utf8");
|
|
209
|
+
} else {
|
|
210
|
+
await cp(srcPath, destPath);
|
|
61
211
|
}
|
|
212
|
+
wroteSomething = true;
|
|
213
|
+
}
|
|
214
|
+
return wroteSomething;
|
|
215
|
+
}
|
|
62
216
|
|
|
63
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Scan a generated build root for residual `${CLAUDE_PLUGIN_ROOT}` tokens.
|
|
219
|
+
* That token is Claude-specific and must not survive into Codex / Cursor /
|
|
220
|
+
* Vercel-style builds — agents on those platforms cannot resolve it.
|
|
221
|
+
*/
|
|
222
|
+
async function verifyNoClaudePluginRootLeak(root: string, label: string): Promise<void> {
|
|
223
|
+
const offenders: string[] = [];
|
|
224
|
+
const stack: string[] = [root];
|
|
225
|
+
while (stack.length > 0) {
|
|
226
|
+
const current = stack.pop()!;
|
|
227
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const path = join(current, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
stack.push(path);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
235
|
+
const content = await readFile(path, "utf8");
|
|
236
|
+
if (content.includes("${CLAUDE_PLUGIN_ROOT}")) {
|
|
237
|
+
offenders.push(path);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (offenders.length > 0) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`${label} build contains \${CLAUDE_PLUGIN_ROOT} tokens — extend rewriteClaudePluginRoot() so non-Claude agents can resolve cross-skill paths:\n${offenders.join("\n")}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function rewriteMarkdownFiles(root: string, rewrite: (content: string) => string): Promise<void> {
|
|
249
|
+
const stack = [root];
|
|
250
|
+
while (stack.length > 0) {
|
|
251
|
+
const current = stack.pop()!;
|
|
252
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const path = join(current, entry.name);
|
|
255
|
+
if (entry.isDirectory()) {
|
|
256
|
+
stack.push(path);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
260
|
+
const original = await readFile(path, "utf8");
|
|
261
|
+
const next = rewrite(original);
|
|
262
|
+
if (next !== original) await writeFile(path, next, "utf8");
|
|
263
|
+
}
|
|
64
264
|
}
|
|
65
265
|
}
|
|
66
266
|
|
|
267
|
+
|
|
268
|
+
function generatedNotice(kind: string): string {
|
|
269
|
+
return [
|
|
270
|
+
`# Generated ${kind}`,
|
|
271
|
+
"",
|
|
272
|
+
"This directory is generated by `bun run build:plugin`.",
|
|
273
|
+
"Do not edit files here directly.",
|
|
274
|
+
"Edit `packages/context-cli/plugin/` and rerun the build instead.",
|
|
275
|
+
"",
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseFrontmatter(markdown: string, fileName: string): { frontmatter: string; body: string } {
|
|
280
|
+
const match = markdown.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
281
|
+
if (!match) throw new Error(`${fileName} must start with YAML frontmatter`);
|
|
282
|
+
return {
|
|
283
|
+
frontmatter: match[1] ?? "",
|
|
284
|
+
body: markdown.slice(match[0].length).trimStart(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function quotedFrontmatterValue(frontmatter: string, key: string, fileName: string): string {
|
|
289
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*["']([\\s\\S]*?)["']\\s*$`, "m"));
|
|
290
|
+
if (!match?.[1]) throw new Error(`${fileName} must declare quoted ${key}`);
|
|
291
|
+
return match[1];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function titleFromSlug(slug: string): string {
|
|
295
|
+
if (slug === "context") return "Router";
|
|
296
|
+
return slug
|
|
297
|
+
.split("-")
|
|
298
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
299
|
+
.join(" ");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function skillNameForCommand(slug: string): string {
|
|
303
|
+
return slug === "context" ? "context-router" : `context-${slug}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function codexSkillNameForCommand(slug: string): string {
|
|
307
|
+
return slug === "context" ? "router" : slug;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function claudeCommandForSlug(slug: string): string {
|
|
311
|
+
return slug === "context" ? "/context:context" : `/context:${slug}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function cursorCommandForSlug(slug: string): string {
|
|
315
|
+
return slug === "context" ? "/context" : `/context-${slug}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const CURSOR_COMMAND_SUMMARIES: Record<string, string> = {
|
|
319
|
+
align: "Review raw material and confirm the Node tree before compiling knowledge.",
|
|
320
|
+
capture: "Capture documents, source code, notes, inbox files, or refreshed sources into the workspace.",
|
|
321
|
+
compile: "Turn confirmed aligned sources into source-linked knowledge through draft and semantic review.",
|
|
322
|
+
context: "Route a natural-language C4A request to the right Context command.",
|
|
323
|
+
drop: "Plan and apply source retraction with semantic decisions and archive safety.",
|
|
324
|
+
extract: "Preview extraction output for a file without writing workspace state.",
|
|
325
|
+
init: "Choose workspace layout, language, and focus, then create the C4A workspace.",
|
|
326
|
+
purge: "Permanently delete archived dropped-source artifacts after explicit confirmation.",
|
|
327
|
+
query: "Answer from the local knowledge workspace with citations and gap handling.",
|
|
328
|
+
status: "Show workspace state, pending work, cache health, and the next useful command.",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function cursorCommandSummary(command: CommandSource): string {
|
|
332
|
+
return CURSOR_COMMAND_SUMMARIES[command.slug] ?? command.description;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function stripHtmlComments(markdown: string): string {
|
|
336
|
+
return markdown.replace(/<!--[\s\S]*?-->\n*/g, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function publicSkillBody(command: CommandSource, publicName: string): string {
|
|
340
|
+
const claudeCommand = claudeCommandForSlug(command.slug);
|
|
341
|
+
const rewrittenBody = rewritePackagedSkillReferences(command.body, publicEntrySkillPath)
|
|
342
|
+
.replace(/^## Your task$/gm, "## Workflow");
|
|
343
|
+
return `---\nname: ${publicName}\ndescription: >\n ${command.description} Equivalent to Claude ${claudeCommand}; use the local \`context\` CLI for workspace writes.\ntools:\n - Bash\n---\n\n# ${command.title}\n\nPublic C4A Context entry for agents that expose skills instead of Claude slash commands.\n\n- Public entry: \`${publicName}\`\n- Claude equivalent: \`${claudeCommand}\`\n- CLI primitive prefix: \`context ...\`\n- Internal procedures live under \`${INTERNAL_PROCEDURES_DIR}/\`; read each referenced file in full before following that step.\n\n---\n\n${rewrittenBody.trimEnd()}\n`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function readCommands(): Promise<CommandSource[]> {
|
|
347
|
+
const commandsRoot = join(pkgRoot, "plugin/commands");
|
|
348
|
+
const files = (await readdir(commandsRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
349
|
+
const commands: CommandSource[] = [];
|
|
350
|
+
for (const file of files) {
|
|
351
|
+
const raw = await readFile(join(commandsRoot, file), "utf8");
|
|
352
|
+
const { frontmatter, body } = parseFrontmatter(raw, file);
|
|
353
|
+
const slug = basename(file, ".md");
|
|
354
|
+
commands.push({
|
|
355
|
+
slug,
|
|
356
|
+
title: titleFromSlug(slug),
|
|
357
|
+
description: quotedFrontmatterValue(frontmatter, "description", file),
|
|
358
|
+
body,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return commands;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function renderManifest(input: {
|
|
365
|
+
templatePath: string;
|
|
366
|
+
outputPath: string;
|
|
367
|
+
version: string;
|
|
368
|
+
label: string;
|
|
369
|
+
}): Promise<Record<string, unknown>> {
|
|
370
|
+
const template = await readFile(input.templatePath, "utf8");
|
|
371
|
+
const rendered = template.replace(/__VERSION__/g, input.version);
|
|
372
|
+
await mkdir(dirname(input.outputPath), { recursive: true });
|
|
373
|
+
await writeFile(input.outputPath, rendered, "utf8");
|
|
374
|
+
|
|
375
|
+
const parsed = JSON.parse(rendered) as Record<string, unknown>;
|
|
376
|
+
if (parsed.name !== "context") {
|
|
377
|
+
throw new Error(`${input.label} plugin.json name must be "context", got "${String(parsed.name)}"`);
|
|
378
|
+
}
|
|
379
|
+
if (parsed.version !== input.version) {
|
|
380
|
+
throw new Error(`${input.label} plugin.json version mismatch: ${String(parsed.version)} vs ${input.version}`);
|
|
381
|
+
}
|
|
382
|
+
return parsed;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function resetDir(dir: string): Promise<void> {
|
|
386
|
+
await rm(dir, { recursive: true, force: true });
|
|
387
|
+
await mkdir(dir, { recursive: true });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function copyDir(from: string, to: string): Promise<void> {
|
|
391
|
+
await cp(from, to, { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function copyAssetsIfPresent(outputRoot: string): Promise<void> {
|
|
395
|
+
const assetsRoot = join(pkgRoot, "plugin/assets");
|
|
396
|
+
if (await pathExists(assetsRoot)) {
|
|
397
|
+
await copyDir(assetsRoot, join(outputRoot, "assets"));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
402
|
+
try {
|
|
403
|
+
await stat(path);
|
|
404
|
+
return true;
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function writeGeneratedGuards(root: string, agentFile: "CLAUDE.md" | "AGENTS.md", label: string): Promise<void> {
|
|
411
|
+
await writeFile(join(root, agentFile), generatedNotice(label), "utf8");
|
|
412
|
+
await writeFile(join(root, "README.md"), generatedNotice(label), "utf8");
|
|
413
|
+
await writeFile(join(root, ".generated"), "generated by packages/context-cli/scripts/build-plugin.ts\n", "utf8");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function buildClaude(version: string): Promise<void> {
|
|
417
|
+
const out = join(PLUGINS_ROOT, "claude");
|
|
418
|
+
await resetDir(out);
|
|
419
|
+
await renderManifest({
|
|
420
|
+
label: "claude",
|
|
421
|
+
version,
|
|
422
|
+
templatePath: join(pkgRoot, "plugin/.claude-plugin/plugin.json.template"),
|
|
423
|
+
outputPath: join(out, ".claude-plugin/plugin.json"),
|
|
424
|
+
});
|
|
425
|
+
await copyDir(join(pkgRoot, "plugin/commands"), join(out, "commands"));
|
|
426
|
+
await copyDir(join(pkgRoot, "plugin/skills"), join(out, "skills"));
|
|
427
|
+
await writeGeneratedGuards(out, "CLAUDE.md", "Claude plugin build");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function writePublicSkills(
|
|
431
|
+
outputRoot: string,
|
|
432
|
+
commands: readonly CommandSource[],
|
|
433
|
+
skillName: (slug: string) => string,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const skillsRoot = join(outputRoot, "skills");
|
|
436
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
437
|
+
const internalSkillNames = (await readdir(join(pkgRoot, "plugin/skills")))
|
|
438
|
+
.filter((name) => name.startsWith("skill-"))
|
|
439
|
+
.sort();
|
|
440
|
+
for (const command of commands) {
|
|
441
|
+
const publicName = skillName(command.slug);
|
|
442
|
+
const skillRoot = join(skillsRoot, publicName);
|
|
443
|
+
await mkdir(skillRoot, { recursive: true });
|
|
444
|
+
await writeFile(join(skillRoot, "SKILL.md"), publicSkillBody(command, publicName), "utf8");
|
|
445
|
+
if (command.body.includes("context:skill-")) {
|
|
446
|
+
const referencesRoot = join(skillRoot, INTERNAL_PROCEDURES_DIR);
|
|
447
|
+
await mkdir(referencesRoot, { recursive: true });
|
|
448
|
+
for (const skill of internalSkillNames) {
|
|
449
|
+
const sourceRoot = join(pkgRoot, "plugin/skills", skill);
|
|
450
|
+
// skill-X.md sits directly in the siblings root; depth = 0.
|
|
451
|
+
const skillBody = await readFile(join(sourceRoot, "SKILL.md"), "utf8");
|
|
452
|
+
await writeFile(
|
|
453
|
+
join(referencesRoot, `${skill}.md`),
|
|
454
|
+
rewriteClaudePluginRoot(skillBody, 0),
|
|
455
|
+
"utf8",
|
|
456
|
+
);
|
|
457
|
+
const refs = join(sourceRoot, "references");
|
|
458
|
+
if (await pathExists(refs)) {
|
|
459
|
+
await copyMarkdownTreeRewrittenForPublicEntry(
|
|
460
|
+
refs,
|
|
461
|
+
join(referencesRoot, skill, "references"),
|
|
462
|
+
referencesRoot,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function buildCodex(version: string, commands: readonly CommandSource[]): Promise<void> {
|
|
471
|
+
const out = join(PLUGINS_ROOT, "codex");
|
|
472
|
+
await resetDir(out);
|
|
473
|
+
const manifest = await renderManifest({
|
|
474
|
+
label: "codex",
|
|
475
|
+
version,
|
|
476
|
+
templatePath: join(pkgRoot, "plugin/.codex-plugin/plugin.json.template"),
|
|
477
|
+
outputPath: join(out, ".codex-plugin/plugin.json"),
|
|
478
|
+
});
|
|
479
|
+
if (manifest.skills !== "./skills/") {
|
|
480
|
+
throw new Error(`codex plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
|
|
481
|
+
}
|
|
482
|
+
await copyAssetsIfPresent(out);
|
|
483
|
+
await writePublicSkills(out, commands, codexSkillNameForCommand);
|
|
484
|
+
await writeGeneratedGuards(out, "AGENTS.md", "Codex plugin build");
|
|
485
|
+
await verifyNoPackagedSkillLeak(out, "codex");
|
|
486
|
+
await verifyNoClaudePluginRootLeak(out, "codex");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function buildVercel(commands: readonly CommandSource[]): Promise<void> {
|
|
490
|
+
// Vercel-style standalone skills live directly under c4a-plugins/skills/.
|
|
491
|
+
// No plugin manifest, no per-build README; the top-level c4a-plugins/README.md
|
|
492
|
+
// owns onboarding for this install path.
|
|
493
|
+
const skillsRoot = join(PLUGINS_ROOT, "skills");
|
|
494
|
+
await resetDir(skillsRoot);
|
|
495
|
+
await writePublicSkills(PLUGINS_ROOT, commands, skillNameForCommand);
|
|
496
|
+
await verifyNoPackagedSkillLeak(skillsRoot, "vercel");
|
|
497
|
+
await verifyNoClaudePluginRootLeak(skillsRoot, "vercel");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function writeCursorCommands(outRoot: string, commands: readonly CommandSource[]): Promise<void> {
|
|
501
|
+
const dest = join(outRoot, "commands");
|
|
502
|
+
await mkdir(dest, { recursive: true });
|
|
503
|
+
const sourceRoot = join(pkgRoot, "plugin/commands");
|
|
504
|
+
for (const command of commands) {
|
|
505
|
+
const sourcePath = join(sourceRoot, `${command.slug}.md`);
|
|
506
|
+
const raw = await readFile(sourcePath, "utf8");
|
|
507
|
+
const { frontmatter } = parseFrontmatter(raw, `${command.slug}.md`);
|
|
508
|
+
const rewrittenBody = rewriteClaudeSlashCommandsForCursor(
|
|
509
|
+
rewritePackagedSkillReferences(command.body, cursorCommandSkillPath),
|
|
510
|
+
);
|
|
511
|
+
const body = stripHtmlComments(rewrittenBody).trimStart();
|
|
512
|
+
const file = `---\n${frontmatter}\n---\n\n${cursorCommandSummary(command)}\n\n---\n\n${body.trimEnd()}\n`;
|
|
513
|
+
await writeFile(join(dest, cursorCommandFileName(command.slug)), file, "utf8");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function buildCursor(version: string, commands: readonly CommandSource[]): Promise<void> {
|
|
518
|
+
const out = join(PLUGINS_ROOT, "cursor");
|
|
519
|
+
await resetDir(out);
|
|
520
|
+
const manifest = await renderManifest({
|
|
521
|
+
label: "cursor",
|
|
522
|
+
version,
|
|
523
|
+
templatePath: join(pkgRoot, "plugin/.cursor-plugin/plugin.json.template"),
|
|
524
|
+
outputPath: join(out, ".cursor-plugin/plugin.json"),
|
|
525
|
+
});
|
|
526
|
+
if (manifest.skills !== "./skills/") {
|
|
527
|
+
throw new Error(`cursor plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
|
|
528
|
+
}
|
|
529
|
+
if (manifest.commands !== "./commands/") {
|
|
530
|
+
throw new Error(`cursor plugin.json must point commands to "./commands/", got "${String(manifest.commands)}"`);
|
|
531
|
+
}
|
|
532
|
+
await copyAssetsIfPresent(out);
|
|
533
|
+
await writeCursorCommands(out, commands);
|
|
534
|
+
// Cursor's internal skills tree is the siblings root; depth-aware rewrite
|
|
535
|
+
// turns `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` into the right number of
|
|
536
|
+
// `..` segments per file location.
|
|
537
|
+
await copyMarkdownTreeRewrittenForPublicEntry(
|
|
538
|
+
join(pkgRoot, "plugin/skills"),
|
|
539
|
+
join(out, "skills"),
|
|
540
|
+
join(out, "skills"),
|
|
541
|
+
);
|
|
542
|
+
await writeGeneratedGuards(out, "AGENTS.md", "Cursor plugin build");
|
|
543
|
+
await writeFile(
|
|
544
|
+
join(out, "README.md"),
|
|
545
|
+
[
|
|
546
|
+
generatedNotice("Cursor plugin build").trimEnd(),
|
|
547
|
+
"",
|
|
548
|
+
"Install shape:",
|
|
549
|
+
"",
|
|
550
|
+
"- Marketplace/GitHub plugin shape: this directory is a plugin root with `.cursor-plugin/plugin.json`, `commands/`, and `skills/`.",
|
|
551
|
+
"- User entries are under `commands/`; Cursor command files are prefixed as `context-*` to avoid global slash-command collisions.",
|
|
552
|
+
"- Local plugin fallback: symlink or copy this directory to `~/.cursor/plugins/local/context/` only when Marketplace import is unavailable.",
|
|
553
|
+
"",
|
|
554
|
+
"Do not copy these files into `.cursor/skills/` as the main install path. Cursor Marketplace installs this plugin root directly.",
|
|
555
|
+
"",
|
|
556
|
+
].join("\n"),
|
|
557
|
+
"utf8",
|
|
558
|
+
);
|
|
559
|
+
await rewriteMarkdownFiles(out, rewriteClaudeSlashCommandsForCursor);
|
|
560
|
+
await verifyNoPackagedSkillLeak(out, "cursor");
|
|
561
|
+
await verifyNoClaudePluginRootLeak(out, "cursor");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function ensurePluginsRoot(): Promise<void> {
|
|
565
|
+
try {
|
|
566
|
+
const info = await stat(PLUGINS_ROOT);
|
|
567
|
+
if (!info.isDirectory()) {
|
|
568
|
+
throw new Error(`c4a-plugins exists but is not a directory: ${PLUGINS_ROOT}`);
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
572
|
+
if (code !== "ENOENT") throw err;
|
|
573
|
+
throw new Error(
|
|
574
|
+
`c4a-plugins/ not found at ${PLUGINS_ROOT}. ` +
|
|
575
|
+
`Run: git clone https://github.com/context4ai/context.git c4a-plugins ` +
|
|
576
|
+
`(from the c4a repo root) before building plugins.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
await stat(join(PLUGINS_ROOT, ".git"));
|
|
581
|
+
} catch {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`c4a-plugins/ exists but is not a git repo. Expected remote: ` +
|
|
584
|
+
`https://github.com/context4ai/context.git`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function writeMarketplaceManifests(version: string): Promise<void> {
|
|
590
|
+
const claudeMarketplace = {
|
|
591
|
+
name: "context",
|
|
592
|
+
owner: { name: "c4a" },
|
|
593
|
+
plugins: [{
|
|
594
|
+
name: "context",
|
|
595
|
+
source: "./claude",
|
|
596
|
+
description: "Context For AI — local knowledge workspace. Capture docs, extract code structure, compile into an interlinked wiki with source-traced facts.",
|
|
597
|
+
}],
|
|
598
|
+
};
|
|
599
|
+
const cursorMarketplace = {
|
|
600
|
+
name: "context",
|
|
601
|
+
owner: { name: "Context4AI", email: "support@context4ai.dev" },
|
|
602
|
+
metadata: { description: "C4A Context plugin marketplace" },
|
|
603
|
+
plugins: [{
|
|
604
|
+
name: "context",
|
|
605
|
+
source: "./cursor",
|
|
606
|
+
description: "Turn project sources into a local, source-linked knowledge workspace your agent can maintain and query.",
|
|
607
|
+
}],
|
|
608
|
+
};
|
|
609
|
+
const codexMarketplace = {
|
|
610
|
+
name: "context",
|
|
611
|
+
interface: { displayName: "C4A Marketplace" },
|
|
612
|
+
plugins: [{
|
|
613
|
+
name: "context",
|
|
614
|
+
source: { source: "local", path: "./codex" },
|
|
615
|
+
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
|
|
616
|
+
category: "Productivity",
|
|
617
|
+
}],
|
|
618
|
+
};
|
|
619
|
+
await mkdir(join(PLUGINS_ROOT, ".claude-plugin"), { recursive: true });
|
|
620
|
+
await mkdir(join(PLUGINS_ROOT, ".cursor-plugin"), { recursive: true });
|
|
621
|
+
await mkdir(join(PLUGINS_ROOT, ".agents", "plugins"), { recursive: true });
|
|
622
|
+
await writeFile(
|
|
623
|
+
join(PLUGINS_ROOT, ".claude-plugin", "marketplace.json"),
|
|
624
|
+
`${JSON.stringify(claudeMarketplace, null, 2)}\n`,
|
|
625
|
+
"utf8",
|
|
626
|
+
);
|
|
627
|
+
await writeFile(
|
|
628
|
+
join(PLUGINS_ROOT, ".cursor-plugin", "marketplace.json"),
|
|
629
|
+
`${JSON.stringify(cursorMarketplace, null, 2)}\n`,
|
|
630
|
+
"utf8",
|
|
631
|
+
);
|
|
632
|
+
await writeFile(
|
|
633
|
+
join(PLUGINS_ROOT, ".agents", "plugins", "marketplace.json"),
|
|
634
|
+
`${JSON.stringify(codexMarketplace, null, 2)}\n`,
|
|
635
|
+
"utf8",
|
|
636
|
+
);
|
|
637
|
+
await writeFile(
|
|
638
|
+
join(PLUGINS_ROOT, "VERSION"),
|
|
639
|
+
`${version}\n`,
|
|
640
|
+
"utf8",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function writePluginsTopLevelDocs(): Promise<void> {
|
|
645
|
+
for (const name of ["README.md", "README_CN.md"]) {
|
|
646
|
+
const body = await readFile(join(pkgRoot, "plugin", name), "utf8");
|
|
647
|
+
await writeFile(join(PLUGINS_ROOT, name), body, "utf8");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const assetsSrc = join(pkgRoot, "plugin/assets");
|
|
651
|
+
if (await pathExists(assetsSrc)) {
|
|
652
|
+
const assetsDst = join(PLUGINS_ROOT, "assets");
|
|
653
|
+
await rm(assetsDst, { recursive: true, force: true });
|
|
654
|
+
await copyDir(assetsSrc, assetsDst);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const gitignorePath = join(PLUGINS_ROOT, ".gitignore");
|
|
658
|
+
if (!(await pathExists(gitignorePath))) {
|
|
659
|
+
await writeFile(
|
|
660
|
+
gitignorePath,
|
|
661
|
+
[
|
|
662
|
+
".DS_Store",
|
|
663
|
+
"node_modules/",
|
|
664
|
+
".idea/",
|
|
665
|
+
".vscode/",
|
|
666
|
+
"",
|
|
667
|
+
].join("\n"),
|
|
668
|
+
"utf8",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function main(): Promise<void> {
|
|
674
|
+
const pkgRaw = await readFile(resolve(pkgRoot, "package.json"), "utf-8");
|
|
675
|
+
const pkg = JSON.parse(pkgRaw) as { version?: string };
|
|
676
|
+
const version = pkg.version;
|
|
677
|
+
if (!version) throw new Error("package.json is missing `version`");
|
|
678
|
+
|
|
679
|
+
await ensurePluginsRoot();
|
|
680
|
+
|
|
681
|
+
const commands = await readCommands();
|
|
682
|
+
await buildClaude(version);
|
|
683
|
+
await buildCodex(version, commands);
|
|
684
|
+
await buildCursor(version, commands);
|
|
685
|
+
await buildVercel(commands);
|
|
686
|
+
await writeMarketplaceManifests(version);
|
|
687
|
+
await writePluginsTopLevelDocs();
|
|
688
|
+
|
|
689
|
+
process.stdout.write(`c4a-plugins/claude generated (version=${version})\n`);
|
|
690
|
+
process.stdout.write(`c4a-plugins/codex generated (version=${version}, publicSkills=${commands.length})\n`);
|
|
691
|
+
process.stdout.write(`c4a-plugins/cursor generated (version=${version}, publicSkills=${commands.length})\n`);
|
|
692
|
+
process.stdout.write(`c4a-plugins/skills generated (publicSkills=${commands.length})\n`);
|
|
693
|
+
process.stdout.write(`c4a-plugins/{marketplace.json,README.md,README_CN.md,VERSION} written\n`);
|
|
694
|
+
}
|
|
695
|
+
|
|
67
696
|
main().catch((err) => {
|
|
68
697
|
process.stderr.write(`build-plugin failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
69
698
|
process.exit(1);
|