@c4a/context-cli 0.6.0-beta.8 → 0.6.1
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/LICENSE +21 -0
- package/README.md +129 -137
- package/cli.js +1573 -3320
- package/package.json +18 -3
- package/plugins/VERSION +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/skill-review-gate/SKILL.md +0 -3
- package/plugins/claude/skills/skill-source-scope/SKILL.md +0 -6
- package/plugins/codex/.codex-plugin/plugin.json +2 -2
- package/plugins/codex/skills/continue/references/internal-procedures/skill-review-gate.md +0 -3
- package/plugins/codex/skills/continue/references/internal-procedures/skill-source-scope.md +0 -6
- package/plugins/codex/skills/init/references/internal-procedures/skill-review-gate.md +0 -3
- package/plugins/codex/skills/init/references/internal-procedures/skill-source-scope.md +0 -6
- package/plugins/cursor/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor/skills/skill-review-gate/SKILL.md +0 -3
- package/plugins/cursor/skills/skill-source-scope/SKILL.md +0 -6
- package/plugins/skills/context-continue/references/internal-procedures/skill-review-gate.md +0 -3
- package/plugins/skills/context-continue/references/internal-procedures/skill-source-scope.md +0 -6
- package/plugins/skills/context-init/references/internal-procedures/skill-review-gate.md +0 -3
- package/plugins/skills/context-init/references/internal-procedures/skill-source-scope.md +0 -6
- package/plugin/.claude-plugin/plugin.json.template +0 -16
- package/plugin/.codex-plugin/plugin.json.template +0 -38
- package/plugin/.cursor-plugin/plugin.json.template +0 -29
- package/plugin/README.md +0 -158
- package/plugin/README_CN.md +0 -142
- package/plugin/assets/icon.svg +0 -3
- package/plugin/assets/logo.svg +0 -3
- package/plugin/assets/workflow-en.png +0 -0
- package/plugin/assets/workflow.png +0 -0
- package/plugin/commands/continue.md +0 -34
- package/plugin/commands/init.md +0 -21
- package/plugin/skills/skill-continue-workflow/SKILL.md +0 -235
- package/plugin/skills/skill-init-workspace/SKILL.md +0 -74
- package/plugin/skills/skill-package-output/SKILL.md +0 -180
- package/plugin/skills/skill-prose-align/SKILL.md +0 -322
- package/plugin/skills/skill-prose-align/references/internal-procedures/structure-planning/references/candidate-resolution.md +0 -91
- package/plugin/skills/skill-prose-align/references/internal-procedures/structure-planning/references/density-profile.md +0 -30
- package/plugin/skills/skill-prose-align/references/internal-procedures/structure-planning/references/gates.md +0 -263
- package/plugin/skills/skill-prose-align/references/internal-procedures/structure-planning.md +0 -282
- package/plugin/skills/skill-prose-compile/SKILL.md +0 -287
- package/plugin/skills/skill-prose-compile/references/internal-procedures/close-gate.md +0 -132
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-actions/references/action-domain-gates.md +0 -52
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-actions/references/notes.md +0 -48
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-actions/references/refresh-and-update.md +0 -85
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-actions/references/structural-challenges.md +0 -73
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-actions.md +0 -413
- package/plugin/skills/skill-prose-compile/references/internal-procedures/compile-judgment.md +0 -123
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment/references/disposition-semantics.md +0 -81
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment/references/leakage-and-ownership.md +0 -55
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment/references/scope-review-and-omit.md +0 -93
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment/references/temporal-and-evidence.md +0 -79
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment/references/user-confirmation.md +0 -111
- package/plugin/skills/skill-prose-compile/references/internal-procedures/semantic-judgment.md +0 -194
- package/plugin/skills/skill-review-gate/SKILL.md +0 -141
- package/plugin/skills/skill-source-scope/SKILL.md +0 -531
- package/plugin/skills/skill-source-scope/references/capture-source.md +0 -201
- package/plugins/assets/icon.svg +0 -3
- package/plugins/assets/workflow-en.png +0 -0
- package/plugins/assets/workflow.png +0 -0
- package/plugins/codex/assets/workflow-en.png +0 -0
- package/plugins/codex/assets/workflow.png +0 -0
- package/plugins/cursor/assets/icon.svg +0 -3
- package/plugins/cursor/assets/workflow-en.png +0 -0
- package/plugins/cursor/assets/workflow.png +0 -0
- package/scripts/build-plugin.ts +0 -658
package/scripts/build-plugin.ts
DELETED
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* build-plugin.ts — materialize installable plugin trees from the human-edited
|
|
4
|
-
* plugin/ source directory.
|
|
5
|
-
*
|
|
6
|
-
* The normal 0.6.x target is `dist/plugins` inside the @c4a/context-cli npm
|
|
7
|
-
* package. `context plugin install` resolves that bundled directory at runtime
|
|
8
|
-
* and installs it globally for Claude / Codex.
|
|
9
|
-
* Source of truth:
|
|
10
|
-
* - plugin/commands/
|
|
11
|
-
* - plugin/skills/
|
|
12
|
-
* - plugin/.claude-plugin/plugin.json.template
|
|
13
|
-
* - plugin/.codex-plugin/plugin.json.template
|
|
14
|
-
* - plugin/.cursor-plugin/plugin.json.template
|
|
15
|
-
*
|
|
16
|
-
* Generated outputs under the target root:
|
|
17
|
-
* - claude/ — Claude plugin root
|
|
18
|
-
* - codex/ — Codex plugin root
|
|
19
|
-
* - cursor/ — Cursor plugin root
|
|
20
|
-
* - skills/ — Vercel-style standalone skills (no plugin manifest)
|
|
21
|
-
*
|
|
22
|
-
* Plus three marketplace.json files at the target root pointing at the
|
|
23
|
-
* respective subdirs, so a single repo serves four install paths.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
27
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
28
|
-
import { fileURLToPath } from "node:url";
|
|
29
|
-
|
|
30
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
const pkgRoot = resolve(__dirname, "..");
|
|
32
|
-
const PLUGINS_ROOT = resolve(pkgRoot, "dist", "plugins");
|
|
33
|
-
const MARKETPLACE_NAME = "c4a";
|
|
34
|
-
const PLUGIN_NAME = "context";
|
|
35
|
-
|
|
36
|
-
interface CommandSource {
|
|
37
|
-
slug: string;
|
|
38
|
-
title: string;
|
|
39
|
-
description: string;
|
|
40
|
-
body: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const INTERNAL_PROCEDURES_DIR = "references/internal-procedures";
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Rewrite internal `skill-*` procedure references in a command body so
|
|
47
|
-
* non-Claude agent surfaces get direct file references they can read.
|
|
48
|
-
*/
|
|
49
|
-
function rewriteInternalProcedureReferences(body: string, pathFor: (skill: string) => string): string {
|
|
50
|
-
let out = body;
|
|
51
|
-
|
|
52
|
-
// Imperative: "Apply/Use/Invoke the internal `context:skill-X` procedure" -> read the file.
|
|
53
|
-
out = out.replace(
|
|
54
|
-
/\b(Apply|Use|Invoke|apply|use|invoke) the internal `(?:context:)?(skill-[a-z-]+)` procedure/g,
|
|
55
|
-
(_match, verb: string, skill: string) => `${verb} the procedure in \`${pathFor(skill)}\``,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Imperative: "invoke the internal `skill-X` procedure" in sentence middles.
|
|
59
|
-
out = out.replace(
|
|
60
|
-
/\binvoke the internal `(?:context:)?(skill-[a-z-]+)` procedure/g,
|
|
61
|
-
(_match, skill: string) => `invoke the procedure in \`${pathFor(skill)}\``,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Descriptive bare internal procedure references also become file paths for non-Claude builds.
|
|
65
|
-
out = out.replace(
|
|
66
|
-
/`(?:context:)?(skill-[a-z-]+)`/g,
|
|
67
|
-
(_match, skill: string) => `\`${pathFor(skill)}\``,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function publicEntrySkillPath(skill: string): string {
|
|
74
|
-
return `${INTERNAL_PROCEDURES_DIR}/${skill}.md`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function cursorCommandSkillPath(skill: string): string {
|
|
78
|
-
// Cursor command files live under `commands/`; bundled skills live under
|
|
79
|
-
// sibling `skills/`. Use a plain relative path that any markdown reader can
|
|
80
|
-
// resolve from the command file's location.
|
|
81
|
-
return `../skills/${skill}/SKILL.md`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function cursorCommandFileName(slug: string): string {
|
|
85
|
-
return `context-${slug}.md`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function rewriteClaudeSlashCommandsForCursor(body: string): string {
|
|
89
|
-
return body
|
|
90
|
-
.replace(/\/context:\*/g, "/context-*")
|
|
91
|
-
.replace(/\/context:([a-z-]+)/g, (_match, slug: string) =>
|
|
92
|
-
slug === "context" ? "/context" : `/context-${slug}`
|
|
93
|
-
)
|
|
94
|
-
.replace(/^## Your [Tt]ask$/gm, "## Workflow")
|
|
95
|
-
.replace(/\bnot a user slash command\b/g, "not a user command");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Scan a generated build root for residual unresolved internal procedure
|
|
100
|
-
* references in agent-facing entry files (Codex / Vercel public skills,
|
|
101
|
-
* Cursor commands).
|
|
102
|
-
*
|
|
103
|
-
* Skips internal procedure bodies — directories named `internal-procedures`
|
|
104
|
-
* (the Codex/Vercel embed point) and directories whose name starts with
|
|
105
|
-
* `skill-` (the cursor-build internal skills tree, mirrored from
|
|
106
|
-
* `plugin/skills/`). Cross-skill mentions inside those bodies are descriptive
|
|
107
|
-
* ("the next stage handles X") and stay in the source skill style.
|
|
108
|
-
*
|
|
109
|
-
* Throws on any leak so future commands or rewrite-pattern gaps fail the
|
|
110
|
-
* build instead of silently shipping unactionable verbs.
|
|
111
|
-
*/
|
|
112
|
-
async function verifyNoInternalProcedureReferenceLeak(root: string, label: string): Promise<void> {
|
|
113
|
-
const offenders: string[] = [];
|
|
114
|
-
const legacyProcedureTokenPattern = new RegExp("`context:" + "skill-[a-z-*]+`", "g");
|
|
115
|
-
const stack: string[] = [root];
|
|
116
|
-
while (stack.length > 0) {
|
|
117
|
-
const current = stack.pop()!;
|
|
118
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
const path = join(current, entry.name);
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
if (entry.name === INTERNAL_PROCEDURES_DIR.split("/").at(-1) || entry.name.startsWith("skill-")) continue;
|
|
123
|
-
stack.push(path);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
127
|
-
const content = await readFile(path, "utf8");
|
|
128
|
-
const matches = [
|
|
129
|
-
...(content.match(/`skill-[a-z-*]+`/g) ?? []),
|
|
130
|
-
...(content.match(legacyProcedureTokenPattern) ?? []),
|
|
131
|
-
];
|
|
132
|
-
if (matches && matches.length > 0) {
|
|
133
|
-
offenders.push(`${path}: ${[...new Set(matches)].join(", ")}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (offenders.length > 0) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`${label} build still contains unresolved internal procedure references in agent-facing entry files — extend rewriteInternalProcedureReferences() to cover the new phrasing:\n${offenders.join("\n")}`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Rewrite `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` cross-skill references
|
|
146
|
-
* inside bundled internal procedure copies to plain relative paths that any
|
|
147
|
-
* agent can resolve. The number of `..` segments depends on how deep the
|
|
148
|
-
* file is below its build's "skills siblings root" (the directory under
|
|
149
|
-
* which all `skill-*` siblings live).
|
|
150
|
-
*/
|
|
151
|
-
function rewriteClaudePluginRoot(content: string, levelsAboveSiblingsRoot: number): string {
|
|
152
|
-
const upPrefix = "../".repeat(levelsAboveSiblingsRoot);
|
|
153
|
-
return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/skills\//g, upPrefix);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function rewriteFlattenedProcedureReferences(content: string, skill: string): string {
|
|
157
|
-
const localReferences = content.replace(
|
|
158
|
-
/(?<![\w./-])references\/([A-Za-z0-9._/-]+\.md)/g,
|
|
159
|
-
`${skill}/references/$1`,
|
|
160
|
-
);
|
|
161
|
-
return localReferences.replace(
|
|
162
|
-
/(?<![\w./-])((?:\.\.\/)*)(skill-[a-z-]+)\/SKILL\.md/g,
|
|
163
|
-
(_match, prefix: string, procedure: string) => `${prefix}${procedure}.md`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function depthOfFileBelowSiblingsRoot(filePath: string, siblingsRoot: string): number {
|
|
168
|
-
const rel = relative(siblingsRoot, dirname(filePath));
|
|
169
|
-
if (rel === "" || rel === ".") return 0;
|
|
170
|
-
return rel.split(sep).filter((seg: string) => seg.length > 0).length;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Recursively copy a markdown tree, applying `rewriteClaudePluginRoot` to
|
|
175
|
-
* every `.md` file based on its location below `siblingsRoot`. Non-markdown
|
|
176
|
-
* files are copied verbatim. Empty subtrees (no files, no recursive content)
|
|
177
|
-
* are skipped — the destination directory is created only when at least one
|
|
178
|
-
* descendant is actually written, so accidental empty source dirs don't
|
|
179
|
-
* propagate into build outputs.
|
|
180
|
-
*/
|
|
181
|
-
async function copyMarkdownTreeRewrittenForPublicEntry(
|
|
182
|
-
src: string,
|
|
183
|
-
dest: string,
|
|
184
|
-
siblingsRoot: string,
|
|
185
|
-
): Promise<boolean> {
|
|
186
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
187
|
-
let wroteSomething = false;
|
|
188
|
-
for (const entry of entries) {
|
|
189
|
-
const srcPath = join(src, entry.name);
|
|
190
|
-
const destPath = join(dest, entry.name);
|
|
191
|
-
if (entry.isDirectory()) {
|
|
192
|
-
const subtreeWrote = await copyMarkdownTreeRewrittenForPublicEntry(srcPath, destPath, siblingsRoot);
|
|
193
|
-
if (subtreeWrote) wroteSomething = true;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (!entry.isFile()) continue;
|
|
197
|
-
if (!wroteSomething) {
|
|
198
|
-
await mkdir(dest, { recursive: true });
|
|
199
|
-
}
|
|
200
|
-
if (entry.name.endsWith(".md")) {
|
|
201
|
-
const content = await readFile(srcPath, "utf8");
|
|
202
|
-
const depth = depthOfFileBelowSiblingsRoot(destPath, siblingsRoot);
|
|
203
|
-
await writeFile(destPath, rewriteClaudePluginRoot(content, depth), "utf8");
|
|
204
|
-
} else {
|
|
205
|
-
await cp(srcPath, destPath);
|
|
206
|
-
}
|
|
207
|
-
wroteSomething = true;
|
|
208
|
-
}
|
|
209
|
-
return wroteSomething;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Scan a generated build root for residual `${CLAUDE_PLUGIN_ROOT}` tokens.
|
|
214
|
-
* That token is Claude-specific and must not survive into Codex / Cursor /
|
|
215
|
-
* Vercel-style builds — agents on those platforms cannot resolve it.
|
|
216
|
-
*/
|
|
217
|
-
async function verifyNoClaudePluginRootLeak(root: string, label: string): Promise<void> {
|
|
218
|
-
const offenders: string[] = [];
|
|
219
|
-
const stack: string[] = [root];
|
|
220
|
-
while (stack.length > 0) {
|
|
221
|
-
const current = stack.pop()!;
|
|
222
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
223
|
-
for (const entry of entries) {
|
|
224
|
-
const path = join(current, entry.name);
|
|
225
|
-
if (entry.isDirectory()) {
|
|
226
|
-
stack.push(path);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
230
|
-
const content = await readFile(path, "utf8");
|
|
231
|
-
if (content.includes("${CLAUDE_PLUGIN_ROOT}")) {
|
|
232
|
-
offenders.push(path);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (offenders.length > 0) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`${label} build contains \${CLAUDE_PLUGIN_ROOT} tokens — extend rewriteClaudePluginRoot() so non-Claude agents can resolve cross-skill paths:\n${offenders.join("\n")}`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function rewriteMarkdownFiles(root: string, rewrite: (content: string) => string): Promise<void> {
|
|
244
|
-
const stack = [root];
|
|
245
|
-
while (stack.length > 0) {
|
|
246
|
-
const current = stack.pop()!;
|
|
247
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
const path = join(current, entry.name);
|
|
250
|
-
if (entry.isDirectory()) {
|
|
251
|
-
stack.push(path);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
255
|
-
const original = await readFile(path, "utf8");
|
|
256
|
-
const next = rewrite(original);
|
|
257
|
-
if (next !== original) await writeFile(path, next, "utf8");
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
function generatedNotice(kind: string): string {
|
|
264
|
-
return [
|
|
265
|
-
`# Generated ${kind}`,
|
|
266
|
-
"",
|
|
267
|
-
"This directory is generated by `bun run build:plugin`.",
|
|
268
|
-
"Do not edit files here directly.",
|
|
269
|
-
"Edit `packages/context-cli/plugin/` and rerun the build instead.",
|
|
270
|
-
"",
|
|
271
|
-
].join("\n");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function parseFrontmatter(markdown: string, fileName: string): { frontmatter: string; body: string } {
|
|
275
|
-
const match = markdown.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
276
|
-
if (!match) throw new Error(`${fileName} must start with YAML frontmatter`);
|
|
277
|
-
return {
|
|
278
|
-
frontmatter: match[1] ?? "",
|
|
279
|
-
body: markdown.slice(match[0].length).trimStart(),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function quotedFrontmatterValue(frontmatter: string, key: string, fileName: string): string {
|
|
284
|
-
const match = frontmatter.match(new RegExp(`^${key}:\\s*["']([\\s\\S]*?)["']\\s*$`, "m"));
|
|
285
|
-
if (!match?.[1]) throw new Error(`${fileName} must declare quoted ${key}`);
|
|
286
|
-
return match[1];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function titleFromSlug(slug: string): string {
|
|
290
|
-
return slug
|
|
291
|
-
.split("-")
|
|
292
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
293
|
-
.join(" ");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function skillNameForCommand(slug: string): string {
|
|
297
|
-
return `context-${slug}`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function codexSkillNameForCommand(slug: string): string {
|
|
301
|
-
return slug;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const CURSOR_COMMAND_SUMMARIES: Record<string, string> = {
|
|
305
|
-
init: "Initialize a Context knowledge workspace.",
|
|
306
|
-
continue: "Continue the Context workflow from the current workspace state.",
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
function cursorCommandSummary(command: CommandSource): string {
|
|
310
|
-
return CURSOR_COMMAND_SUMMARIES[command.slug] ?? command.description;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function stripHtmlComments(markdown: string): string {
|
|
314
|
-
return markdown.replace(/<!--[\s\S]*?-->\n*/g, "");
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function publicSkillBody(command: CommandSource, publicName: string): string {
|
|
318
|
-
const rewrittenBody = rewriteInternalProcedureReferences(command.body, publicEntrySkillPath)
|
|
319
|
-
.replace(/^## Your [Tt]ask$/gm, "## Workflow");
|
|
320
|
-
return `---\nname: ${publicName}\ndescription: >\n ${command.description} Use the local \`context\` CLI for workspace writes.\ntools:\n - Bash\n---\n\n# ${command.title}\n\nPublic Context entry for agents that expose skills instead of slash commands.\n\n- Public entry: \`${publicName}\`\n- Host command names are installation-specific; use the command or skill surfaced by the current host instead of deriving a filesystem path from an example name.\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`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function readCommands(): Promise<CommandSource[]> {
|
|
324
|
-
const commandsRoot = join(pkgRoot, "plugin/commands");
|
|
325
|
-
const files = (await readdir(commandsRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
326
|
-
const commands: CommandSource[] = [];
|
|
327
|
-
for (const file of files) {
|
|
328
|
-
const raw = await readFile(join(commandsRoot, file), "utf8");
|
|
329
|
-
const { frontmatter, body } = parseFrontmatter(raw, file);
|
|
330
|
-
const slug = basename(file, ".md");
|
|
331
|
-
commands.push({
|
|
332
|
-
slug,
|
|
333
|
-
title: titleFromSlug(slug),
|
|
334
|
-
description: quotedFrontmatterValue(frontmatter, "description", file),
|
|
335
|
-
body,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
return commands;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function renderManifest(input: {
|
|
342
|
-
templatePath: string;
|
|
343
|
-
outputPath: string;
|
|
344
|
-
version: string;
|
|
345
|
-
label: string;
|
|
346
|
-
}): Promise<Record<string, unknown>> {
|
|
347
|
-
const template = await readFile(input.templatePath, "utf8");
|
|
348
|
-
const rendered = template.replace(/__VERSION__/g, input.version);
|
|
349
|
-
await mkdir(dirname(input.outputPath), { recursive: true });
|
|
350
|
-
await writeFile(input.outputPath, rendered, "utf8");
|
|
351
|
-
|
|
352
|
-
const parsed = JSON.parse(rendered) as Record<string, unknown>;
|
|
353
|
-
if (parsed.name !== PLUGIN_NAME) {
|
|
354
|
-
throw new Error(`${input.label} plugin.json name must be "${PLUGIN_NAME}", got "${String(parsed.name)}"`);
|
|
355
|
-
}
|
|
356
|
-
if (parsed.version !== input.version) {
|
|
357
|
-
throw new Error(`${input.label} plugin.json version mismatch: ${String(parsed.version)} vs ${input.version}`);
|
|
358
|
-
}
|
|
359
|
-
return parsed;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async function resetDir(dir: string): Promise<void> {
|
|
363
|
-
await rm(dir, { recursive: true, force: true });
|
|
364
|
-
await mkdir(dir, { recursive: true });
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function copyDir(from: string, to: string): Promise<void> {
|
|
368
|
-
await cp(from, to, { recursive: true });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function copyAssetsIfPresent(outputRoot: string): Promise<void> {
|
|
372
|
-
const assetsRoot = join(pkgRoot, "plugin/assets");
|
|
373
|
-
if (await pathExists(assetsRoot)) {
|
|
374
|
-
await copyDir(assetsRoot, join(outputRoot, "assets"));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
379
|
-
try {
|
|
380
|
-
await stat(path);
|
|
381
|
-
return true;
|
|
382
|
-
} catch {
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function writeGeneratedGuards(root: string, agentFile: "CLAUDE.md" | "AGENTS.md", label: string): Promise<void> {
|
|
388
|
-
await writeFile(join(root, agentFile), generatedNotice(label), "utf8");
|
|
389
|
-
await writeFile(join(root, "README.md"), generatedNotice(label), "utf8");
|
|
390
|
-
await writeFile(join(root, ".generated"), "generated by packages/context-cli/scripts/build-plugin.ts\n", "utf8");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function buildClaude(version: string): Promise<void> {
|
|
394
|
-
const out = join(PLUGINS_ROOT, "claude");
|
|
395
|
-
await resetDir(out);
|
|
396
|
-
await renderManifest({
|
|
397
|
-
label: "claude",
|
|
398
|
-
version,
|
|
399
|
-
templatePath: join(pkgRoot, "plugin/.claude-plugin/plugin.json.template"),
|
|
400
|
-
outputPath: join(out, ".claude-plugin/plugin.json"),
|
|
401
|
-
});
|
|
402
|
-
await copyDir(join(pkgRoot, "plugin/commands"), join(out, "commands"));
|
|
403
|
-
await copyDir(join(pkgRoot, "plugin/skills"), join(out, "skills"));
|
|
404
|
-
await writeGeneratedGuards(out, "CLAUDE.md", "Claude plugin build");
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async function writePublicSkills(
|
|
408
|
-
outputRoot: string,
|
|
409
|
-
commands: readonly CommandSource[],
|
|
410
|
-
skillName: (slug: string) => string,
|
|
411
|
-
): Promise<void> {
|
|
412
|
-
const skillsRoot = join(outputRoot, "skills");
|
|
413
|
-
await mkdir(skillsRoot, { recursive: true });
|
|
414
|
-
const internalSkillNames = (await readdir(join(pkgRoot, "plugin/skills")))
|
|
415
|
-
.filter((name) => name.startsWith("skill-"))
|
|
416
|
-
.sort();
|
|
417
|
-
for (const command of commands) {
|
|
418
|
-
const publicName = skillName(command.slug);
|
|
419
|
-
const skillRoot = join(skillsRoot, publicName);
|
|
420
|
-
await mkdir(skillRoot, { recursive: true });
|
|
421
|
-
await writeFile(join(skillRoot, "SKILL.md"), publicSkillBody(command, publicName), "utf8");
|
|
422
|
-
if (/`(?:context:)?skill-[a-z-]+`/.test(command.body)) {
|
|
423
|
-
const referencesRoot = join(skillRoot, INTERNAL_PROCEDURES_DIR);
|
|
424
|
-
await mkdir(referencesRoot, { recursive: true });
|
|
425
|
-
for (const skill of internalSkillNames) {
|
|
426
|
-
const sourceRoot = join(pkgRoot, "plugin/skills", skill);
|
|
427
|
-
// skill-X.md sits directly in the siblings root; depth = 0.
|
|
428
|
-
const skillBody = await readFile(join(sourceRoot, "SKILL.md"), "utf8");
|
|
429
|
-
await writeFile(
|
|
430
|
-
join(referencesRoot, `${skill}.md`),
|
|
431
|
-
rewriteFlattenedProcedureReferences(rewriteClaudePluginRoot(skillBody, 0), skill),
|
|
432
|
-
"utf8",
|
|
433
|
-
);
|
|
434
|
-
const refs = join(sourceRoot, "references");
|
|
435
|
-
if (await pathExists(refs)) {
|
|
436
|
-
await copyMarkdownTreeRewrittenForPublicEntry(
|
|
437
|
-
refs,
|
|
438
|
-
join(referencesRoot, skill, "references"),
|
|
439
|
-
referencesRoot,
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
await rewriteMarkdownFiles(referencesRoot, (content) =>
|
|
444
|
-
content.replace(
|
|
445
|
-
/(?<![\w./-])((?:\.\.\/)*)(skill-[a-z-]+)\/SKILL\.md/g,
|
|
446
|
-
(_match, prefix: string, procedure: string) => `${prefix}${procedure}.md`,
|
|
447
|
-
)
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function buildCodex(version: string, commands: readonly CommandSource[]): Promise<void> {
|
|
454
|
-
const out = join(PLUGINS_ROOT, "codex");
|
|
455
|
-
await resetDir(out);
|
|
456
|
-
const manifest = await renderManifest({
|
|
457
|
-
label: "codex",
|
|
458
|
-
version,
|
|
459
|
-
templatePath: join(pkgRoot, "plugin/.codex-plugin/plugin.json.template"),
|
|
460
|
-
outputPath: join(out, ".codex-plugin/plugin.json"),
|
|
461
|
-
});
|
|
462
|
-
if (manifest.skills !== "./skills/") {
|
|
463
|
-
throw new Error(`codex plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
|
|
464
|
-
}
|
|
465
|
-
await copyAssetsIfPresent(out);
|
|
466
|
-
await writePublicSkills(out, commands, codexSkillNameForCommand);
|
|
467
|
-
await writeGeneratedGuards(out, "AGENTS.md", "Codex plugin build");
|
|
468
|
-
await verifyNoInternalProcedureReferenceLeak(out, "codex");
|
|
469
|
-
await verifyNoClaudePluginRootLeak(out, "codex");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async function buildVercel(commands: readonly CommandSource[]): Promise<void> {
|
|
473
|
-
// Vercel-style standalone skills live directly under dist/plugins/skills/.
|
|
474
|
-
// No plugin manifest, no per-build README; the top-level dist/plugins/README.md
|
|
475
|
-
// owns onboarding for this install path.
|
|
476
|
-
const skillsRoot = join(PLUGINS_ROOT, "skills");
|
|
477
|
-
await resetDir(skillsRoot);
|
|
478
|
-
await writePublicSkills(PLUGINS_ROOT, commands, skillNameForCommand);
|
|
479
|
-
await verifyNoInternalProcedureReferenceLeak(skillsRoot, "vercel");
|
|
480
|
-
await verifyNoClaudePluginRootLeak(skillsRoot, "vercel");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async function writeCursorCommands(outRoot: string, commands: readonly CommandSource[]): Promise<void> {
|
|
484
|
-
const dest = join(outRoot, "commands");
|
|
485
|
-
await mkdir(dest, { recursive: true });
|
|
486
|
-
const sourceRoot = join(pkgRoot, "plugin/commands");
|
|
487
|
-
for (const command of commands) {
|
|
488
|
-
const sourcePath = join(sourceRoot, `${command.slug}.md`);
|
|
489
|
-
const raw = await readFile(sourcePath, "utf8");
|
|
490
|
-
const { frontmatter } = parseFrontmatter(raw, `${command.slug}.md`);
|
|
491
|
-
const rewrittenBody = rewriteClaudeSlashCommandsForCursor(
|
|
492
|
-
rewriteInternalProcedureReferences(command.body, cursorCommandSkillPath),
|
|
493
|
-
);
|
|
494
|
-
const body = stripHtmlComments(rewrittenBody).trimStart();
|
|
495
|
-
const file = `---\n${frontmatter}\n---\n\n${cursorCommandSummary(command)}\n\n---\n\n${body.trimEnd()}\n`;
|
|
496
|
-
await writeFile(join(dest, cursorCommandFileName(command.slug)), file, "utf8");
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function buildCursor(version: string, commands: readonly CommandSource[]): Promise<void> {
|
|
501
|
-
const out = join(PLUGINS_ROOT, "cursor");
|
|
502
|
-
await resetDir(out);
|
|
503
|
-
const manifest = await renderManifest({
|
|
504
|
-
label: "cursor",
|
|
505
|
-
version,
|
|
506
|
-
templatePath: join(pkgRoot, "plugin/.cursor-plugin/plugin.json.template"),
|
|
507
|
-
outputPath: join(out, ".cursor-plugin/plugin.json"),
|
|
508
|
-
});
|
|
509
|
-
if (manifest.skills !== "./skills/") {
|
|
510
|
-
throw new Error(`cursor plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
|
|
511
|
-
}
|
|
512
|
-
if (manifest.commands !== "./commands/") {
|
|
513
|
-
throw new Error(`cursor plugin.json must point commands to "./commands/", got "${String(manifest.commands)}"`);
|
|
514
|
-
}
|
|
515
|
-
await copyAssetsIfPresent(out);
|
|
516
|
-
await writeCursorCommands(out, commands);
|
|
517
|
-
// Cursor's internal skills tree is the siblings root; depth-aware rewrite
|
|
518
|
-
// turns `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` into the right number of
|
|
519
|
-
// `..` segments per file location.
|
|
520
|
-
await copyMarkdownTreeRewrittenForPublicEntry(
|
|
521
|
-
join(pkgRoot, "plugin/skills"),
|
|
522
|
-
join(out, "skills"),
|
|
523
|
-
join(out, "skills"),
|
|
524
|
-
);
|
|
525
|
-
await writeGeneratedGuards(out, "AGENTS.md", "Cursor plugin build");
|
|
526
|
-
await writeFile(
|
|
527
|
-
join(out, "README.md"),
|
|
528
|
-
[
|
|
529
|
-
generatedNotice("Cursor plugin build").trimEnd(),
|
|
530
|
-
"",
|
|
531
|
-
"Install shape:",
|
|
532
|
-
"",
|
|
533
|
-
"- Marketplace/GitHub plugin shape: this directory is a plugin root with `.cursor-plugin/plugin.json`, `commands/`, and `skills/`.",
|
|
534
|
-
"- User entries are under `commands/`; Cursor command files are prefixed as `context-*` to avoid global slash-command collisions.",
|
|
535
|
-
"- Local plugin fallback: symlink or copy this directory to `~/.cursor/plugins/local/context/` only when Marketplace import is unavailable.",
|
|
536
|
-
"",
|
|
537
|
-
"Do not copy these files into `.cursor/skills/` as the main install path. Cursor Marketplace installs this plugin root directly.",
|
|
538
|
-
"",
|
|
539
|
-
].join("\n"),
|
|
540
|
-
"utf8",
|
|
541
|
-
);
|
|
542
|
-
await rewriteMarkdownFiles(out, rewriteClaudeSlashCommandsForCursor);
|
|
543
|
-
await verifyNoInternalProcedureReferenceLeak(out, "cursor");
|
|
544
|
-
await verifyNoClaudePluginRootLeak(out, "cursor");
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async function writeMarketplaceManifests(version: string): Promise<void> {
|
|
548
|
-
const claudeMarketplace = {
|
|
549
|
-
name: MARKETPLACE_NAME,
|
|
550
|
-
owner: { name: "c4a" },
|
|
551
|
-
plugins: [{
|
|
552
|
-
name: PLUGIN_NAME,
|
|
553
|
-
source: "./claude",
|
|
554
|
-
description: "Local knowledge workspace. Initialize a project and use next-step guidance to maintain it.",
|
|
555
|
-
}],
|
|
556
|
-
};
|
|
557
|
-
const cursorMarketplace = {
|
|
558
|
-
name: MARKETPLACE_NAME,
|
|
559
|
-
owner: { name: "Context4AI", email: "support@context4ai.dev" },
|
|
560
|
-
metadata: { description: "C4A Context plugin marketplace" },
|
|
561
|
-
plugins: [{
|
|
562
|
-
name: PLUGIN_NAME,
|
|
563
|
-
source: "./cursor",
|
|
564
|
-
description: "Maintain a project-local knowledge workspace through init and continue-step guidance.",
|
|
565
|
-
}],
|
|
566
|
-
};
|
|
567
|
-
const codexMarketplace = {
|
|
568
|
-
name: MARKETPLACE_NAME,
|
|
569
|
-
interface: { displayName: "C4A Marketplace" },
|
|
570
|
-
plugins: [{
|
|
571
|
-
name: PLUGIN_NAME,
|
|
572
|
-
source: { source: "local", path: "./codex" },
|
|
573
|
-
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
|
|
574
|
-
category: "Productivity",
|
|
575
|
-
}],
|
|
576
|
-
};
|
|
577
|
-
await mkdir(join(PLUGINS_ROOT, ".claude-plugin"), { recursive: true });
|
|
578
|
-
await mkdir(join(PLUGINS_ROOT, ".cursor-plugin"), { recursive: true });
|
|
579
|
-
await mkdir(join(PLUGINS_ROOT, ".agents", "plugins"), { recursive: true });
|
|
580
|
-
await writeFile(
|
|
581
|
-
join(PLUGINS_ROOT, ".claude-plugin", "marketplace.json"),
|
|
582
|
-
`${JSON.stringify(claudeMarketplace, null, 2)}\n`,
|
|
583
|
-
"utf8",
|
|
584
|
-
);
|
|
585
|
-
await writeFile(
|
|
586
|
-
join(PLUGINS_ROOT, ".cursor-plugin", "marketplace.json"),
|
|
587
|
-
`${JSON.stringify(cursorMarketplace, null, 2)}\n`,
|
|
588
|
-
"utf8",
|
|
589
|
-
);
|
|
590
|
-
await writeFile(
|
|
591
|
-
join(PLUGINS_ROOT, ".agents", "plugins", "marketplace.json"),
|
|
592
|
-
`${JSON.stringify(codexMarketplace, null, 2)}\n`,
|
|
593
|
-
"utf8",
|
|
594
|
-
);
|
|
595
|
-
await writeFile(
|
|
596
|
-
join(PLUGINS_ROOT, "VERSION"),
|
|
597
|
-
`${version}\n`,
|
|
598
|
-
"utf8",
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async function writePluginsTopLevelDocs(): Promise<void> {
|
|
603
|
-
for (const name of ["README.md", "README_CN.md"]) {
|
|
604
|
-
const body = await readFile(join(pkgRoot, "plugin", name), "utf8");
|
|
605
|
-
await writeFile(join(PLUGINS_ROOT, name), body, "utf8");
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const assetsSrc = join(pkgRoot, "plugin/assets");
|
|
609
|
-
if (await pathExists(assetsSrc)) {
|
|
610
|
-
const assetsDst = join(PLUGINS_ROOT, "assets");
|
|
611
|
-
await rm(assetsDst, { recursive: true, force: true });
|
|
612
|
-
await copyDir(assetsSrc, assetsDst);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const gitignorePath = join(PLUGINS_ROOT, ".gitignore");
|
|
616
|
-
if (!(await pathExists(gitignorePath))) {
|
|
617
|
-
await writeFile(
|
|
618
|
-
gitignorePath,
|
|
619
|
-
[
|
|
620
|
-
".DS_Store",
|
|
621
|
-
"node_modules/",
|
|
622
|
-
".idea/",
|
|
623
|
-
".vscode/",
|
|
624
|
-
"",
|
|
625
|
-
].join("\n"),
|
|
626
|
-
"utf8",
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
async function main(): Promise<void> {
|
|
632
|
-
const pkgRaw = await readFile(resolve(pkgRoot, "package.json"), "utf-8");
|
|
633
|
-
const pkg = JSON.parse(pkgRaw) as { version?: string };
|
|
634
|
-
const version = pkg.version;
|
|
635
|
-
if (!version) throw new Error("package.json is missing `version`");
|
|
636
|
-
|
|
637
|
-
const targetLabel = "dist/plugins";
|
|
638
|
-
await resetDir(PLUGINS_ROOT);
|
|
639
|
-
|
|
640
|
-
const commands = await readCommands();
|
|
641
|
-
await buildClaude(version);
|
|
642
|
-
await buildCodex(version, commands);
|
|
643
|
-
await buildCursor(version, commands);
|
|
644
|
-
await buildVercel(commands);
|
|
645
|
-
await writeMarketplaceManifests(version);
|
|
646
|
-
await writePluginsTopLevelDocs();
|
|
647
|
-
|
|
648
|
-
process.stdout.write(`${targetLabel}/claude generated (version=${version})\n`);
|
|
649
|
-
process.stdout.write(`${targetLabel}/codex generated (version=${version}, publicSkills=${commands.length})\n`);
|
|
650
|
-
process.stdout.write(`${targetLabel}/cursor generated (version=${version}, publicSkills=${commands.length})\n`);
|
|
651
|
-
process.stdout.write(`${targetLabel}/skills generated (publicSkills=${commands.length})\n`);
|
|
652
|
-
process.stdout.write(`${targetLabel}/{marketplace.json,README.md,README_CN.md,VERSION} written\n`);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
main().catch((err) => {
|
|
656
|
-
process.stderr.write(`build-plugin failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
657
|
-
process.exit(1);
|
|
658
|
-
});
|