@jaybeeuu/agent-cortex 2.0.0 → 2.2.0
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/CHANGELOG.md +437 -0
- package/README.md +46 -15
- package/agents/README.md +22 -10
- package/agents/plan/agent.md +2 -2
- package/agents/ralph/agent.md +128 -91
- package/agents/ralph/claude/frontmatter.json +3 -3
- package/agents/ralph/claude/wake.md +46 -0
- package/agents/ralph/pi/{polling.md → wake.md} +35 -20
- package/agents/ralph-plan/agent.md +23 -11
- package/bin/agent-cortex.mjs +66 -4
- package/bin/installers/claude.mjs +262 -77
- package/bin/installers/pi.mjs +200 -24
- package/extensions/agent-guardrails/README.md +1 -1
- package/extensions/agent-guardrails/guardrails.ts +1 -1
- package/extensions/agent-guardrails/index.ts +1 -1
- package/extensions/agent-modes/discover.test.ts +2 -2
- package/extensions/output-style/README.md +58 -0
- package/extensions/output-style/index.ts +23 -0
- package/extensions/output-style/output-style.md +43 -0
- package/extensions/output-style/output-style.test.ts +50 -0
- package/extensions/output-style/output-style.ts +32 -0
- package/extensions/output-style/package.json +19 -0
- package/hooks/claude/scripts/notify.mjs +8 -1
- package/lib/cli.mjs +91 -2
- package/package.json +29 -4
- package/scripts/check-changeset.mjs +4 -0
- package/scripts/check-no-sync.mjs +5 -1
- package/scripts/check-skill-frontmatter.mjs +21 -0
- package/scripts/lib/compose-agent.mjs +69 -9
- package/scripts/lib/fs.mjs +10 -2
- package/scripts/lib/migrate.mjs +736 -0
- package/scripts/lib/release-gates.mjs +21 -1
- package/scripts/lib/skill-frontmatter.mjs +152 -0
- package/scripts/lib/skill-links.mjs +52 -0
- package/scripts/lib/verify-worker-tree.mjs +25 -0
- package/scripts/resolve-publish-command.mjs +4 -0
- package/skills/engineering/design-an-interface/SKILL.md +3 -3
- package/skills/engineering/grill-with-docs/SKILL.md +3 -5
- package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
- package/skills/engineering/tdd/SKILL.md +5 -7
- package/skills/engineering/technical-direction/SKILL.md +5 -5
- package/skills/planning/classify-bead/REFERENCE.md +3 -3
- package/skills/planning/classify-bead/SKILL.md +14 -9
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
- package/skills/planning/create-task/REFERENCE.md +52 -3
- package/skills/planning/create-task/SKILL.md +70 -26
- package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
- package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
- package/skills/planning/flesh-out-idea/SKILL.md +146 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
- package/skills/planning/prd-to-tasks/SKILL.md +22 -16
- package/skills/planning/record-idea/FORMAT.md +66 -17
- package/skills/planning/record-idea/SKILL.md +48 -24
- package/skills/planning/request-refactor-plan/SKILL.md +4 -4
- package/skills/planning/spec-to-tickets/SKILL.md +5 -5
- package/skills/planning/write-a-prd/SKILL.md +8 -9
- package/skills/planning/write-a-ticket/SKILL.md +4 -6
- package/skills/productivity/bd-tool/SKILL.md +15 -10
- package/skills/productivity/grill-me/SKILL.md +5 -0
- package/skills/productivity/init-beads/SKILL.md +3 -3
- package/skills/productivity/migration/REFERENCE.md +227 -0
- package/skills/productivity/migration/SKILL.md +188 -0
- package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
- package/skills/productivity/write-a-skill/SKILL.md +5 -5
- package/skills/review/maintain-agent-docs/SKILL.md +5 -7
- package/skills/review/refactor-skill/SKILL.md +4 -3
- package/skills/review/review-security/SKILL.md +3 -5
- package/skills/style/style-code/SKILL.md +5 -10
- package/skills/style/style-comms/SKILL.md +5 -7
- package/skills/style/style-documentation/SKILL.md +5 -6
- package/skills/style/style-tests/SKILL.md +5 -7
- package/skills/workflow/git-workflow/SKILL.md +28 -4
- package/skills/workflow/hitl-collab/SKILL.md +4 -6
- package/skills/workflow/plan/SKILL.md +41 -30
- package/skills/workflow/ralph/REFERENCE.md +209 -55
- package/skills/workflow/ralph/SKILL.md +89 -50
- package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
- package/token-map.README.md +9 -4
- package/token-map.json +13 -6
- package/.agents/skills/style-versioning/SKILL.md +0 -172
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.github/workflows/ci.yml +0 -302
- package/.junie/guidelines.md +0 -87
- package/.junie/mcp/mcp.json +0 -10
- package/AGENTS.md +0 -151
- package/agents/ralph/claude/polling.md +0 -31
- package/agents-native/ralph.md +0 -193
- package/docs/claude-hooks.md +0 -128
- package/docs/commit-signing.md +0 -231
- package/docs/ideas/auto-model-selection.md +0 -75
- package/docs/ideas/ban-content-assertion-tests.md +0 -75
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
- package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
- package/docs/ideas/dedicated-review-tests-skill.md +0 -71
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
- package/docs/ideas/extension-manifests.md +0 -121
- package/docs/ideas/improve-pi-system-prompt.md +0 -37
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
- package/docs/ideas/model-selector.md +0 -40
- package/docs/ideas/pi-task-memory.md +0 -34
- package/docs/ideas/qa-extension.md +0 -43
- package/docs/ideas/record-idea-implementation-questions.md +0 -68
- package/docs/ideas/record-ideas-as-beads.md +0 -115
- package/docs/ideas/self-flushing-session.md +0 -72
- package/docs/ideas/state-in-beads-not-md.md +0 -69
- package/docs/ideas/subagent-visibility.md +0 -72
- package/docs/ideas/task-session-context.local.md +0 -95
- package/docs/ideas/task-session-context.md +0 -89
- package/docs/ideas/test-data-creation-helpers.md +0 -92
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
- package/docs/ideas/unified-ci-pipeline.md +0 -28
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
- package/docs/inspirations.md +0 -33
- package/docs/research/disable-model-invocation.md +0 -82
- package/docs/research/gas-city-research.md +0 -256
- package/docs/research/skill-design-research.md +0 -286
- package/docs/research/skill-improvements-analysis.md +0 -117
- package/docs/research/skill-patterns-research.md +0 -24
- package/docs/skills/skill-anatomy.md +0 -197
- package/docs/technical-direction/harness-config.md +0 -177
- package/docs/technical-direction/migration-execution-labels.md +0 -177
- package/docs/user-preferences.md +0 -17
- package/extensions/agent-guardrails/tsconfig.json +0 -12
- package/extensions/agent-modes/tsconfig.json +0 -12
- package/extensions/session-start/tsconfig.json +0 -12
- package/extensions/skill-stats/pnpm-lock.yaml +0 -329
- package/extensions/skill-stats/tsconfig.json +0 -12
- package/extensions/subagent/agents/planner.md +0 -37
- package/extensions/subagent/agents/reviewer.md +0 -35
- package/extensions/subagent/agents/scout.md +0 -50
- package/extensions/subagent/agents/worker.md +0 -24
- package/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/extensions/subagent/prompts/implement.md +0 -10
- package/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/extensions/subagent/tsconfig.json +0 -12
- package/pi/keybindings.json +0 -3
- package/pi/settings.json +0 -34
- package/pnpm-workspace.yaml +0 -14
- package/skills/planning/create-task/scripts/package.json +0 -14
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
- package/skills/planning/create-task/scripts/tsconfig.json +0 -12
- package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
- package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
- package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
- package/test/classify-bead.test.mjs +0 -277
- package/test/claude-installer.test.mjs +0 -308
- package/test/cli.test.mjs +0 -314
- package/test/create-chores.test.mjs +0 -85
- package/test/fake-bd.test.mjs +0 -72
- package/test/fs.test.mjs +0 -76
- package/test/helpers/fake-bd.mjs +0 -87
- package/test/helpers/fake-claude.mjs +0 -152
- package/test/helpers/fake-pi.mjs +0 -132
- package/test/installer.test.mjs +0 -664
- package/test/notify-hook.test.mjs +0 -63
- package/test/pi-installer.test.mjs +0 -785
- package/test/pi-register.test.mjs +0 -342
- package/test/pi-sweep.test.mjs +0 -619
- package/test/release-gates.test.mjs +0 -190
- package/test/verify-worker-tree.test.mjs +0 -120
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
// agent.md + claude/ harness dirs,
|
|
12
12
|
// {{TOOL:...}}/{{PATH:...}}
|
|
13
13
|
// substituted against token-map.json's
|
|
14
|
-
// claude column)
|
|
15
|
-
// (ralph verbatim)
|
|
14
|
+
// claude column)
|
|
16
15
|
// <output>/skills/<name>/ skill dirs COPIED flat (one dir per
|
|
17
16
|
// skill, as Claude's <name>/SKILL.md
|
|
18
17
|
// plugin scan expects) with .md files
|
|
19
18
|
// token-substituted — no symlinks, so
|
|
20
19
|
// literal {{TOOL:...}}/{{PATH:...}}
|
|
21
|
-
// never reaches Claude
|
|
20
|
+
// never reaches Claude. Cross-skill
|
|
21
|
+
// links are rewritten for the flat
|
|
22
|
+
// layout (scripts/lib/skill-links.mjs),
|
|
23
|
+
// so a grouped-tree handoff still
|
|
24
|
+
// resolves in the plugin.
|
|
22
25
|
// <output>/hooks.json + hooks/<scripts> from hooks/claude/ (support files
|
|
23
26
|
// referenced via ${CLAUDE_PLUGIN_ROOT})
|
|
24
27
|
// <output>/.mcp.json + scripts/ hand-authored extras copied from
|
|
@@ -60,10 +63,33 @@ import { fileURLToPath } from "node:url";
|
|
|
60
63
|
import { homedir } from "node:os";
|
|
61
64
|
import { composeAgent, loadTokenMap, substituteTokens } from "../../scripts/lib/compose-agent.mjs";
|
|
62
65
|
import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
|
|
66
|
+
import { flattenSkillLinks } from "../../scripts/lib/skill-links.mjs";
|
|
63
67
|
|
|
64
68
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
65
69
|
const PACKAGE_ROOT = join(MODULE_DIR, "..", "..");
|
|
66
70
|
|
|
71
|
+
/** @typedef {import("../../scripts/lib/compose-agent.mjs").TokenMap} TokenMap */
|
|
72
|
+
/** @typedef {(msg: string) => void} Warn */
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Result of writing (or planning to write) a managed file.
|
|
76
|
+
* @typedef {object} ManagedFileResult
|
|
77
|
+
* @property {string} path
|
|
78
|
+
* @property {"would-remove-symlink" | "unchanged" | "would-write" | "removed-symlink" | "written" | "skipped-unmanaged"} action
|
|
79
|
+
* @property {boolean} removedSymlink
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Registration plan/outcome returned by registerClaude.
|
|
84
|
+
* @typedef {object} RegisterClaudeResult
|
|
85
|
+
* @property {string} marketplace
|
|
86
|
+
* @property {string} plugin
|
|
87
|
+
* @property {string[][]} commands
|
|
88
|
+
* @property {boolean} dryRun
|
|
89
|
+
* @property {boolean} registered
|
|
90
|
+
* @property {string[]} skipped
|
|
91
|
+
*/
|
|
92
|
+
|
|
67
93
|
// Home-scoped plugin install root. The parent dir (~/.agent-cortex) is the
|
|
68
94
|
// marketplace root: it carries .claude-plugin/marketplace.json exposing
|
|
69
95
|
// ./claude, so `claude plugin marketplace add ~/.agent-cortex` points Claude
|
|
@@ -79,6 +105,14 @@ const DEFAULT_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
|
79
105
|
// The committed user-settings template.
|
|
80
106
|
const SETTINGS_TEMPLATE = "claude/settings.json";
|
|
81
107
|
|
|
108
|
+
// Claude user memory — the global prompt. The installer owns only the marked
|
|
109
|
+
// section; everything outside the markers is the user's own text.
|
|
110
|
+
const DEFAULT_INSTRUCTIONS_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
111
|
+
const INSTRUCTIONS_BEGIN = "<!-- BEGIN agent-cortex:output-style -->";
|
|
112
|
+
const INSTRUCTIONS_END = "<!-- END agent-cortex:output-style -->";
|
|
113
|
+
// Canonical prose, shared with the pi extension (extensions/output-style).
|
|
114
|
+
const INSTRUCTIONS_SOURCE = join("extensions", "output-style", "output-style.md");
|
|
115
|
+
|
|
82
116
|
// The only keys the installer owns outright: everything else in the live file
|
|
83
117
|
// is personal config the merge must preserve.
|
|
84
118
|
const SETTINGS_OWNED_KEYS = ["enabledPlugins", "extraKnownMarketplaces"];
|
|
@@ -90,27 +124,27 @@ const CONTRACT_VERSION = 1;
|
|
|
90
124
|
|
|
91
125
|
const CLAUDE = "claude";
|
|
92
126
|
|
|
93
|
-
const DEFAULT_WARN = (msg) => console.warn(`[claude-installer] ${msg}`);
|
|
94
|
-
|
|
95
|
-
// Agents NOT transformed from the composable sources. `ralph` is authored natively for Claude
|
|
96
|
-
// instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
|
|
97
|
-
// section composition from the poll-loop sources.
|
|
98
|
-
const DEFER = new Set(["ralph"]);
|
|
127
|
+
const DEFAULT_WARN = (/** @type {string} */ msg) => console.warn(`[claude-installer] ${msg}`);
|
|
99
128
|
|
|
100
|
-
// Ralph-coupled pipeline skills that don't apply to the lean Claude Ralph (its completion
|
|
101
|
-
// handling is event-driven, not the pi orchestrator's). Not copied into the Claude plugin.
|
|
102
|
-
const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
|
|
103
129
|
|
|
104
130
|
const NAME_PREFIX = "agent-cortex:";
|
|
105
131
|
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} a
|
|
134
|
+
* @param {string} b
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
106
137
|
function byName(a, b) {
|
|
107
138
|
return a.localeCompare(b);
|
|
108
139
|
}
|
|
109
140
|
|
|
110
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Compose every agent from agents/<name>/ — all harnesses share the composable sources.
|
|
143
|
+
* @param {string} root
|
|
144
|
+
* @returns {Promise<{ files: { file: string, content: string }[] }>}
|
|
145
|
+
*/
|
|
111
146
|
async function buildAgents(root) {
|
|
112
147
|
const files = [];
|
|
113
|
-
const skipped = [];
|
|
114
148
|
const entries = (await readdir(join(root, "agents"), { withFileTypes: true })).sort((a, b) =>
|
|
115
149
|
byName(a.name, b.name),
|
|
116
150
|
);
|
|
@@ -120,11 +154,6 @@ async function buildAgents(root) {
|
|
|
120
154
|
const dir = join(root, "agents", entry.name);
|
|
121
155
|
if (!(await isFile(join(dir, "agent.md"))) || !(await isFile(join(dir, "claude", "frontmatter.json")))) continue;
|
|
122
156
|
|
|
123
|
-
if (DEFER.has(entry.name)) {
|
|
124
|
-
skipped.push(entry.name);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
157
|
const fm = await composeAgent(root, entry.name, CLAUDE);
|
|
129
158
|
const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
|
|
130
159
|
const header =
|
|
@@ -136,18 +165,7 @@ async function buildAgents(root) {
|
|
|
136
165
|
`---\n`;
|
|
137
166
|
files.push({ file: `${slug}.md`, content: `${header}\n${fm.body}\n` });
|
|
138
167
|
}
|
|
139
|
-
return { files
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Copy hand-authored Claude-native agents into agents/ verbatim. */
|
|
143
|
-
async function buildNatives(root, nativeSrc) {
|
|
144
|
-
const files = [];
|
|
145
|
-
if (!(await isDirectory(nativeSrc))) return files;
|
|
146
|
-
const names = (await readdir(nativeSrc)).filter((f) => f.endsWith(".md")).sort(byName);
|
|
147
|
-
for (const file of names) {
|
|
148
|
-
files.push({ file, content: await readFile(join(nativeSrc, file), "utf-8") });
|
|
149
|
-
}
|
|
150
|
-
return files;
|
|
168
|
+
return { files };
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
/**
|
|
@@ -156,36 +174,63 @@ async function buildNatives(root, nativeSrc) {
|
|
|
156
174
|
* under skills/), token-substituting .md files against token-map.json's claude
|
|
157
175
|
* column (the same materialisation as bin/installers/pi.mjs: no symlinks, so
|
|
158
176
|
* literal {{TOOL:...}}/{{PATH:...}} never survives; non-.md files verbatim).
|
|
177
|
+
* Flattening drops the group segment an authored cross-skill link relies on, so
|
|
178
|
+
* each .md file's skill links are rewritten to the flat sibling path.
|
|
179
|
+
* @param {string} root
|
|
180
|
+
* @param {string} skillOut
|
|
181
|
+
* @param {{ dryRun: boolean, pluginRoot: string | undefined, tokenMap: TokenMap, warn: Warn }} options
|
|
182
|
+
* @returns {Promise<{ names: string[], md: number, files: number, dir: string }>}
|
|
159
183
|
*/
|
|
160
184
|
async function buildSkills(root, skillOut, { dryRun, pluginRoot, tokenMap, warn }) {
|
|
185
|
+
// Names first: flattening needs the full set to recognise a skill segment in a
|
|
186
|
+
// link, and every name is unique once flattened.
|
|
187
|
+
const sources = [];
|
|
161
188
|
const seen = new Map();
|
|
162
|
-
const transform = (content) =>
|
|
163
|
-
substituteTokens(content, CLAUDE, tokenMap, { dropNullTools: true, pluginRoot, resolveRelativePaths: true, warn });
|
|
164
|
-
|
|
165
|
-
const names = [];
|
|
166
|
-
let md = 0;
|
|
167
|
-
let files = 0;
|
|
168
189
|
for (const group of (await readdir(join(root, "skills"))).sort(byName)) {
|
|
169
190
|
const groupDir = join(root, "skills", group);
|
|
170
191
|
if (!(await isDirectory(groupDir))) continue;
|
|
171
192
|
for (const name of (await readdir(groupDir)).sort(byName)) {
|
|
172
193
|
const skillDir = join(groupDir, name);
|
|
173
194
|
if (!(await isFile(join(skillDir, "SKILL.md")))) continue;
|
|
174
|
-
if (SKILL_EXCLUDE.has(name)) continue;
|
|
175
195
|
if (seen.has(name)) {
|
|
176
196
|
throw new Error(`duplicate skill name "${name}" (${seen.get(name)} and ${group}) — names must be unique when flattened`);
|
|
177
197
|
}
|
|
178
198
|
seen.set(name, group);
|
|
179
|
-
|
|
180
|
-
const stats = await copyTree(skillDir, join(skillOut, name), transform, dryRun);
|
|
181
|
-
md += stats.md;
|
|
182
|
-
files += stats.files;
|
|
199
|
+
sources.push({ name, dir: skillDir });
|
|
183
200
|
}
|
|
184
201
|
}
|
|
202
|
+
|
|
203
|
+
const names = sources.map((source) => source.name);
|
|
204
|
+
const nameSet = new Set(names);
|
|
205
|
+
let md = 0;
|
|
206
|
+
let files = 0;
|
|
207
|
+
for (const source of sources) {
|
|
208
|
+
/** @param {string} content @returns {string} */
|
|
209
|
+
const transform = (content) =>
|
|
210
|
+
flattenSkillLinks(
|
|
211
|
+
substituteTokens(content, CLAUDE, tokenMap, {
|
|
212
|
+
dropNullTools: true,
|
|
213
|
+
pluginRoot,
|
|
214
|
+
resolveRelativePaths: true,
|
|
215
|
+
warn,
|
|
216
|
+
}),
|
|
217
|
+
nameSet,
|
|
218
|
+
);
|
|
219
|
+
const stats = await copyTree(source.dir, join(skillOut, source.name), transform, dryRun);
|
|
220
|
+
md += stats.md;
|
|
221
|
+
files += stats.files;
|
|
222
|
+
}
|
|
185
223
|
return { names, md, files, dir: skillOut };
|
|
186
224
|
}
|
|
187
225
|
|
|
188
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Recursively copy a tree; transform .md file contents, copy everything else verbatim.
|
|
228
|
+
* @param {string} src
|
|
229
|
+
* @param {string} dest
|
|
230
|
+
* @param {(content: string) => string} transform
|
|
231
|
+
* @param {boolean} dryRun
|
|
232
|
+
* @returns {Promise<{ md: number, files: number }>}
|
|
233
|
+
*/
|
|
189
234
|
async function copyTree(src, dest, transform, dryRun) {
|
|
190
235
|
let md = 0;
|
|
191
236
|
let files = 0;
|
|
@@ -208,7 +253,11 @@ async function copyTree(src, dest, transform, dryRun) {
|
|
|
208
253
|
return { md, files };
|
|
209
254
|
}
|
|
210
255
|
|
|
211
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* plugin.json manifest; version tracks the package so it can never go stale.
|
|
258
|
+
* @param {string} root
|
|
259
|
+
* @returns {Promise<string>}
|
|
260
|
+
*/
|
|
212
261
|
async function buildPluginJson(root) {
|
|
213
262
|
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
|
|
214
263
|
return (
|
|
@@ -234,6 +283,10 @@ async function buildPluginJson(root) {
|
|
|
234
283
|
* install root ~/.agent-cortex doubles as the marketplace root), so `claude
|
|
235
284
|
* plugin marketplace add ~/.agent-cortex` resolves ./claude relative to the
|
|
236
285
|
* manifest's directory.
|
|
286
|
+
* @param {string} root
|
|
287
|
+
* @param {string} outputName
|
|
288
|
+
* @returns {Promise<{ name: string, owner: { name: string }, metadata: { description: string },
|
|
289
|
+
* plugins: { name: string, source: string, description: string }[] }>}
|
|
237
290
|
*/
|
|
238
291
|
async function buildMarketplaceManifest(root, outputName) {
|
|
239
292
|
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
|
|
@@ -255,11 +308,19 @@ async function buildMarketplaceManifest(root, outputName) {
|
|
|
255
308
|
* Hook support files (hooks/claude/ except hooks.json) bundled into
|
|
256
309
|
* hooks/ so hook commands can reference them via ${CLAUDE_PLUGIN_ROOT}.
|
|
257
310
|
* hooks.json itself is special-cased to the plugin root (hooks.json).
|
|
311
|
+
* @param {string} root
|
|
312
|
+
* @returns {Promise<{ rel: string, content: string }[]>}
|
|
258
313
|
*/
|
|
259
314
|
async function buildHookFiles(root) {
|
|
315
|
+
/** @type {{ rel: string, content: string }[]} */
|
|
260
316
|
const files = [];
|
|
261
317
|
const srcDir = join(root, "hooks", "claude");
|
|
262
318
|
if (!(await isDirectory(srcDir))) return files;
|
|
319
|
+
/**
|
|
320
|
+
* @param {string} dir
|
|
321
|
+
* @param {string} rel
|
|
322
|
+
* @returns {Promise<void>}
|
|
323
|
+
*/
|
|
263
324
|
const walk = async (dir, rel) => {
|
|
264
325
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
265
326
|
entries.sort((a, b) => byName(a.name, b.name));
|
|
@@ -281,8 +342,11 @@ async function buildHookFiles(root) {
|
|
|
281
342
|
* at read time (not path-referenced) so the write-phase cleanup of the
|
|
282
343
|
* output dir can never destroy the sources (outputs normally go to
|
|
283
344
|
* ~/.agent-cortex/claude or a tmp dir, never into claude-extras/).
|
|
345
|
+
* @param {string} root
|
|
346
|
+
* @returns {Promise<{ rel: string, content: string | Buffer, mode: number }[]>}
|
|
284
347
|
*/
|
|
285
348
|
async function buildHandAuthored(root) {
|
|
349
|
+
/** @type {{ rel: string, content: string | Buffer, mode: number }[]} */
|
|
286
350
|
const items = [];
|
|
287
351
|
const srcExtras = join(root, "claude-extras");
|
|
288
352
|
if (await isFile(join(srcExtras, ".mcp.json"))) {
|
|
@@ -312,6 +376,8 @@ async function buildHandAuthored(root) {
|
|
|
312
376
|
* - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned: they are
|
|
313
377
|
* rewritten from the template outright. `permissions`, `hooks`, `env`,
|
|
314
378
|
* `statusLine` and every other key are never touched.
|
|
379
|
+
* @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
|
|
380
|
+
* @returns {Promise<ManagedFileResult | null>}
|
|
315
381
|
*/
|
|
316
382
|
async function installSettings({ root, target, dryRun, warn }) {
|
|
317
383
|
const template = await readTemplateJson(join(root, SETTINGS_TEMPLATE), warn);
|
|
@@ -324,15 +390,19 @@ async function installSettings({ root, target, dryRun, warn }) {
|
|
|
324
390
|
}
|
|
325
391
|
|
|
326
392
|
const content = `${JSON.stringify(merged, null, 2)}\n`;
|
|
327
|
-
return
|
|
393
|
+
return writeManagedFile({ target, content, dryRun });
|
|
328
394
|
}
|
|
329
395
|
|
|
330
396
|
/**
|
|
331
397
|
* Merge the committed template over the live file. Template keys fill in
|
|
332
398
|
* defaults, live values win (deeply), and keys the template does not know about
|
|
333
399
|
* are preserved — re-installing never discards personal or Claude-written config.
|
|
400
|
+
* @param {Record<string, any>} template
|
|
401
|
+
* @param {Record<string, any>} existing
|
|
402
|
+
* @returns {Record<string, any>}
|
|
334
403
|
*/
|
|
335
404
|
function mergeSettings(template, existing) {
|
|
405
|
+
/** @type {Record<string, any>} */
|
|
336
406
|
const merged = {};
|
|
337
407
|
for (const key of Object.keys(template)) {
|
|
338
408
|
const templateValue = template[key];
|
|
@@ -350,12 +420,14 @@ function mergeSettings(template, existing) {
|
|
|
350
420
|
}
|
|
351
421
|
|
|
352
422
|
/**
|
|
353
|
-
* Write
|
|
354
|
-
*
|
|
355
|
-
*
|
|
423
|
+
* Write a managed file. A legacy symlink is replaced with a real file by writing
|
|
424
|
+
* a sibling temp file and renaming it over the target — rename replaces the link
|
|
425
|
+
* itself, so a committed template is never written through.
|
|
356
426
|
* Returns the action taken so the CLI can report the plan (dry-run included).
|
|
427
|
+
* @param {{ target: string, content: string, dryRun: boolean }} options
|
|
428
|
+
* @returns {Promise<ManagedFileResult>}
|
|
357
429
|
*/
|
|
358
|
-
async function
|
|
430
|
+
async function writeManagedFile({ target, content, dryRun }) {
|
|
359
431
|
const symlink = await isSymlink(target);
|
|
360
432
|
const current = await readFileMaybe(target);
|
|
361
433
|
|
|
@@ -382,6 +454,62 @@ async function writeManagedJson({ target, content, dryRun }) {
|
|
|
382
454
|
return { path: target, action: symlink ? "removed-symlink" : "written", removedSymlink: symlink };
|
|
383
455
|
}
|
|
384
456
|
|
|
457
|
+
// ─── Claude user memory (CLAUDE.md) ──────────
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Materialise the output-style block into the managed section of ~/.claude/CLAUDE.md.
|
|
461
|
+
*
|
|
462
|
+
* The section is delimited by HTML comments, so a re-install replaces only the
|
|
463
|
+
* text between them and every other line of the user's memory file survives. A
|
|
464
|
+
* file with exactly one marker (hand-edited or truncated) is left untouched with
|
|
465
|
+
* a warning, never guessed at.
|
|
466
|
+
* @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
|
|
467
|
+
* @returns {Promise<ManagedFileResult | null>}
|
|
468
|
+
*/
|
|
469
|
+
async function installInstructions({ root, target, dryRun, warn }) {
|
|
470
|
+
const block = await readFileMaybe(join(root, INSTRUCTIONS_SOURCE));
|
|
471
|
+
if (block === null) {
|
|
472
|
+
warn(`no ${INSTRUCTIONS_SOURCE} source in the package — skipping CLAUDE.md`);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const content = mergeManagedSection(await readFileMaybe(target), block);
|
|
476
|
+
if (content === null) {
|
|
477
|
+
warn(`${target} has an unterminated agent-cortex section — left unchanged; fix the markers by hand`);
|
|
478
|
+
return { path: target, action: "skipped-unmanaged", removedSymlink: false };
|
|
479
|
+
}
|
|
480
|
+
return writeManagedFile({ target, content, dryRun });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Return `existing` with the managed section replaced by `block`, or null when
|
|
485
|
+
* the file carries unmatched markers. A file without markers gets the section
|
|
486
|
+
* appended, so personal memory text is never disturbed.
|
|
487
|
+
* @param {string | null} existing
|
|
488
|
+
* @param {string} block
|
|
489
|
+
* @returns {string | null}
|
|
490
|
+
*/
|
|
491
|
+
function mergeManagedSection(existing, block) {
|
|
492
|
+
const managed = `${INSTRUCTIONS_BEGIN}\n${block.trimEnd()}\n${INSTRUCTIONS_END}`;
|
|
493
|
+
if (existing === null) return `${managed}\n`;
|
|
494
|
+
|
|
495
|
+
const start = existing.indexOf(INSTRUCTIONS_BEGIN);
|
|
496
|
+
const end = existing.indexOf(INSTRUCTIONS_END);
|
|
497
|
+
if (start === -1 && end === -1) {
|
|
498
|
+
const head = existing.replace(/\s*$/, "");
|
|
499
|
+
return head === "" ? `${managed}\n` : `${head}\n\n${managed}\n`;
|
|
500
|
+
}
|
|
501
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
502
|
+
|
|
503
|
+
const before = existing.slice(0, start).replace(/\s*$/, "");
|
|
504
|
+
const after = existing.slice(end + INSTRUCTIONS_END.length).replace(/^\s*/, "").replace(/\s*$/, "");
|
|
505
|
+
return `${[before, managed, after].filter((part) => part !== "").join("\n\n")}\n`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @param {string} filePath
|
|
510
|
+
* @param {Warn} warn
|
|
511
|
+
* @returns {Promise<Record<string, any> | null>}
|
|
512
|
+
*/
|
|
385
513
|
async function readTemplateJson(filePath, warn) {
|
|
386
514
|
const raw = await readFileMaybe(filePath);
|
|
387
515
|
if (raw === null) {
|
|
@@ -393,6 +521,11 @@ async function readTemplateJson(filePath, warn) {
|
|
|
393
521
|
return parsed;
|
|
394
522
|
}
|
|
395
523
|
|
|
524
|
+
/**
|
|
525
|
+
* @param {string} filePath
|
|
526
|
+
* @param {Warn} warn
|
|
527
|
+
* @returns {Promise<Record<string, any>>}
|
|
528
|
+
*/
|
|
396
529
|
async function readJsonObject(filePath, warn) {
|
|
397
530
|
const raw = await readFileMaybe(filePath);
|
|
398
531
|
if (raw === null) return {};
|
|
@@ -404,6 +537,10 @@ async function readJsonObject(filePath, warn) {
|
|
|
404
537
|
return parsed;
|
|
405
538
|
}
|
|
406
539
|
|
|
540
|
+
/**
|
|
541
|
+
* @param {string} raw
|
|
542
|
+
* @returns {Record<string, any> | null}
|
|
543
|
+
*/
|
|
407
544
|
function tryParseJsonObject(raw) {
|
|
408
545
|
try {
|
|
409
546
|
const parsed = JSON.parse(raw);
|
|
@@ -413,19 +550,31 @@ function tryParseJsonObject(raw) {
|
|
|
413
550
|
}
|
|
414
551
|
}
|
|
415
552
|
|
|
553
|
+
/**
|
|
554
|
+
* @param {unknown} value
|
|
555
|
+
* @returns {value is Record<string, any>}
|
|
556
|
+
*/
|
|
416
557
|
function isPlainObject(value) {
|
|
417
558
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
418
559
|
}
|
|
419
560
|
|
|
561
|
+
/**
|
|
562
|
+
* @param {string} filePath
|
|
563
|
+
* @returns {Promise<string | null>}
|
|
564
|
+
*/
|
|
420
565
|
async function readFileMaybe(filePath) {
|
|
421
566
|
try {
|
|
422
567
|
return await readFile(filePath, "utf-8");
|
|
423
568
|
} catch (err) {
|
|
424
|
-
if (err.code === "ENOENT") return null;
|
|
569
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") return null;
|
|
425
570
|
throw err;
|
|
426
571
|
}
|
|
427
572
|
}
|
|
428
573
|
|
|
574
|
+
/**
|
|
575
|
+
* @param {string} filePath
|
|
576
|
+
* @returns {Promise<boolean>}
|
|
577
|
+
*/
|
|
429
578
|
async function isSymlink(filePath) {
|
|
430
579
|
try {
|
|
431
580
|
return (await lstat(filePath)).isSymbolicLink();
|
|
@@ -450,15 +599,20 @@ async function isSymlink(filePath) {
|
|
|
450
599
|
* (default ~/.claude/settings.json). Passing it
|
|
451
600
|
* explicitly materialises settings even with
|
|
452
601
|
* --output — the test/CI seam.
|
|
602
|
+
* @param {string} [options.instructionsPath] Claude user-memory target
|
|
603
|
+
* (default ~/.claude/CLAUDE.md). Passing it
|
|
604
|
+
* explicitly materialises the section even
|
|
605
|
+
* with --output — the test/CI seam.
|
|
453
606
|
* @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
|
|
454
|
-
* @returns {{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
|
|
455
|
-
* marketplaceManifest: object|null, agents: string[],
|
|
607
|
+
* @returns {Promise<{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
|
|
608
|
+
* marketplaceManifest: object|null, agents: string[],
|
|
456
609
|
* skills: {names: string[], md: number, files: number, dir: string},
|
|
457
610
|
* hooks: boolean, hookFiles: string[], handAuthored: string[],
|
|
458
611
|
* settings: {path: string, action: string, removedSymlink: boolean}|null,
|
|
459
|
-
*
|
|
612
|
+
* instructions: {path: string, action: string, removedSymlink: boolean}|null,
|
|
613
|
+
* pluginVersion: string, warnings: string[], dryRun: boolean }>}
|
|
460
614
|
*/
|
|
461
|
-
export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, warn = DEFAULT_WARN } = {}) {
|
|
615
|
+
export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
|
|
462
616
|
const tokenMap = await loadTokenMap(root);
|
|
463
617
|
if (typeof tokenMap.version === "number" && tokenMap.version > CONTRACT_VERSION) {
|
|
464
618
|
throw new Error(
|
|
@@ -466,8 +620,8 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
466
620
|
);
|
|
467
621
|
}
|
|
468
622
|
|
|
469
|
-
const warnings = [];
|
|
470
|
-
const warnSink = (msg) => {
|
|
623
|
+
const warnings = /** @type {string[]} */ ([]);
|
|
624
|
+
const warnSink = (/** @type {string} */ msg) => {
|
|
471
625
|
warnings.push(msg);
|
|
472
626
|
warn(msg);
|
|
473
627
|
};
|
|
@@ -477,9 +631,9 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
477
631
|
// --output is generate-only (tests/CI), matching its no-registration semantics.
|
|
478
632
|
const isDefaultInstall = output === undefined;
|
|
479
633
|
const marketplaceRoot = isDefaultInstall ? dirname(out) : null;
|
|
480
|
-
const manifestPath = isDefaultInstall ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
|
|
634
|
+
const manifestPath = isDefaultInstall && marketplaceRoot !== null ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
|
|
481
635
|
|
|
482
|
-
const rootPlugin = pluginRoot ?? tokenMap.paths.plugin_root?.[CLAUDE];
|
|
636
|
+
const rootPlugin = pluginRoot ?? /** @type {string | undefined} */ (tokenMap.paths.plugin_root?.[CLAUDE]);
|
|
483
637
|
|
|
484
638
|
// Read hand-authored extras from the canonical claude-extras/ dir BEFORE
|
|
485
639
|
// the cleanup below (defensive: the output is normally ~/.agent-cortex/claude
|
|
@@ -498,17 +652,23 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
498
652
|
await mkdir(out, { recursive: true });
|
|
499
653
|
}
|
|
500
654
|
|
|
501
|
-
// The default install owns ~/.claude/settings.json
|
|
502
|
-
// generate-only form and leaves the
|
|
503
|
-
//
|
|
655
|
+
// The default install owns ~/.claude/settings.json and the managed section of
|
|
656
|
+
// ~/.claude/CLAUDE.md; --output is the generate-only form and leaves the
|
|
657
|
+
// user's Claude config alone. Explicit paths request the write regardless
|
|
658
|
+
// (the test/CI seam).
|
|
504
659
|
const settingsTarget = settingsPath ?? DEFAULT_SETTINGS_PATH;
|
|
505
660
|
const settings =
|
|
506
661
|
isDefaultInstall || settingsPath !== undefined
|
|
507
662
|
? await installSettings({ root, target: settingsTarget, dryRun, warn: warnSink })
|
|
508
663
|
: null;
|
|
509
664
|
|
|
665
|
+
const instructionsTarget = instructionsPath ?? DEFAULT_INSTRUCTIONS_PATH;
|
|
666
|
+
const instructions =
|
|
667
|
+
isDefaultInstall || instructionsPath !== undefined
|
|
668
|
+
? await installInstructions({ root, target: instructionsTarget, dryRun, warn: warnSink })
|
|
669
|
+
: null;
|
|
670
|
+
|
|
510
671
|
const agents = await buildAgents(root);
|
|
511
|
-
const natives = await buildNatives(root, join(root, "agents-native"));
|
|
512
672
|
const skillOut = join(out, "skills");
|
|
513
673
|
const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
|
|
514
674
|
const pluginJson = await buildPluginJson(root);
|
|
@@ -524,22 +684,19 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
524
684
|
marketplaceManifest,
|
|
525
685
|
pluginVersion: JSON.parse(pluginJson).version,
|
|
526
686
|
agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
|
|
527
|
-
natives: natives.map((f) => f.file.replace(/\.md$/, "")),
|
|
528
687
|
skills: { names: skills.names, md: skills.md, files: skills.files, dir: skillOut },
|
|
529
688
|
hooks: hooksJson !== null,
|
|
530
689
|
hookFiles: hookFiles.map((f) => f.rel),
|
|
531
690
|
handAuthored: handAuthored.map((f) => f.rel),
|
|
532
691
|
settings,
|
|
692
|
+
instructions,
|
|
533
693
|
warnings,
|
|
534
694
|
dryRun,
|
|
535
695
|
};
|
|
536
696
|
|
|
537
|
-
if (agents.skipped.length) console.log(`Skipped composable source(s): ${agents.skipped.join(", ")}`);
|
|
538
|
-
|
|
539
697
|
if (dryRun) {
|
|
540
698
|
console.log(`DRY-RUN — would generate into ${out}, no files written:`);
|
|
541
699
|
console.log(` agents: ${summary.agents.join(", ")}`);
|
|
542
|
-
if (summary.natives.length) console.log(` native agents: ${summary.natives.join(", ")}`);
|
|
543
700
|
console.log(` skills: ${summary.skills.names.length} copied → ${skillOut}/<name>/ (token-substituted)`);
|
|
544
701
|
console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
|
|
545
702
|
if (isDefaultInstall && manifestPath) {
|
|
@@ -551,18 +708,21 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
551
708
|
if (summary.handAuthored.length)
|
|
552
709
|
console.log(` hand-authored files: ${summary.handAuthored.join(", ")} (copied from repo claude-extras/)`);
|
|
553
710
|
if (summary.settings) console.log(` settings → ${summary.settings.path} (${summary.settings.action})`);
|
|
711
|
+
if (summary.instructions)
|
|
712
|
+
console.log(` CLAUDE.md → ${summary.instructions.path} (${summary.instructions.action})`);
|
|
554
713
|
return summary;
|
|
555
714
|
}
|
|
556
715
|
|
|
557
716
|
// Write phase — the cleanup before buildSkills already cleared stale children.
|
|
558
717
|
if (settings) console.log(`Materialised Claude settings → ${settings.path} (${settings.action})`);
|
|
718
|
+
if (instructions && instructions.action !== "skipped-unmanaged")
|
|
719
|
+
console.log(`Materialised CLAUDE.md section → ${instructions.path} (${instructions.action})`);
|
|
559
720
|
const agentOut = join(out, "agents");
|
|
560
721
|
await mkdir(agentOut, { recursive: true });
|
|
561
|
-
for (const { file, content } of
|
|
722
|
+
for (const { file, content } of agents.files) {
|
|
562
723
|
await writeFile(join(agentOut, file), content);
|
|
563
724
|
}
|
|
564
725
|
console.log(`Generated ${summary.agents.length} Claude agent(s): ${summary.agents.join(", ")}`);
|
|
565
|
-
if (summary.natives.length) console.log(`Copied ${summary.natives.length} native Claude agent(s): ${summary.natives.join(", ")}`);
|
|
566
726
|
|
|
567
727
|
await mkdir(skillOut, { recursive: true });
|
|
568
728
|
console.log(
|
|
@@ -580,7 +740,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
580
740
|
console.log(`Wrote ${manifestPath} (plugin agent-cortex @ ./${basename(out)})`);
|
|
581
741
|
}
|
|
582
742
|
|
|
583
|
-
if (
|
|
743
|
+
if (hooksJson !== null) {
|
|
584
744
|
await writeFile(join(out, "hooks.json"), hooksJson);
|
|
585
745
|
console.log(`Wrote ${join(out, "hooks.json")} (from ${hooksSrc})`);
|
|
586
746
|
|
|
@@ -615,8 +775,13 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
615
775
|
// The `claude` CLI on PATH. Overridable in tests via the claudeBin option.
|
|
616
776
|
const DEFAULT_CLAUDE_BIN = "claude";
|
|
617
777
|
|
|
618
|
-
/**
|
|
619
|
-
*
|
|
778
|
+
/**
|
|
779
|
+
* Validate a marketplace manifest object; returns the marketplace and plugin
|
|
780
|
+
* names that `claude plugin` commands address.
|
|
781
|
+
* @param {any} manifest
|
|
782
|
+
* @param {string} label
|
|
783
|
+
* @returns {{ marketplace: string, plugin: string }}
|
|
784
|
+
*/
|
|
620
785
|
function parseMarketplaceManifest(manifest, label) {
|
|
621
786
|
const marketplace = manifest.name;
|
|
622
787
|
const plugin = manifest.plugins?.[0]?.name;
|
|
@@ -631,6 +796,8 @@ function parseMarketplaceManifest(manifest, label) {
|
|
|
631
796
|
/**
|
|
632
797
|
* Read the marketplace manifest that exposes the plugin at install root
|
|
633
798
|
* (<root>/.claude-plugin/marketplace.json).
|
|
799
|
+
* @param {string} root
|
|
800
|
+
* @returns {Promise<{ manifest: any, path: string }>}
|
|
634
801
|
*/
|
|
635
802
|
async function readMarketplaceManifest(root) {
|
|
636
803
|
const manifestPath = join(root, ".claude-plugin", "marketplace.json");
|
|
@@ -639,7 +806,7 @@ async function readMarketplaceManifest(root) {
|
|
|
639
806
|
manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
640
807
|
} catch (err) {
|
|
641
808
|
throw new Error(
|
|
642
|
-
`no Claude marketplace manifest at ${manifestPath} (${err.message}) — ` +
|
|
809
|
+
`no Claude marketplace manifest at ${manifestPath} (${/** @type {Error} */ (err).message}) — ` +
|
|
643
810
|
`"agent-cortex install claude" registers the plugin by pointing Claude Code at the install root`,
|
|
644
811
|
);
|
|
645
812
|
}
|
|
@@ -651,6 +818,9 @@ async function readMarketplaceManifest(root) {
|
|
|
651
818
|
* where <source> is the marketplace manifest's plugin source) — the signature a
|
|
652
819
|
* directory-source marketplace exposes to `claude plugin`. Read when the version
|
|
653
820
|
* is not passed explicitly; undefined when it cannot be determined.
|
|
821
|
+
* @param {string} root
|
|
822
|
+
* @param {any} manifest
|
|
823
|
+
* @returns {Promise<string | undefined>}
|
|
654
824
|
*/
|
|
655
825
|
async function readMaterialisedPluginVersion(root, manifest) {
|
|
656
826
|
const source = manifest.plugins?.[0]?.source;
|
|
@@ -663,7 +833,10 @@ async function readMaterialisedPluginVersion(root, manifest) {
|
|
|
663
833
|
}
|
|
664
834
|
}
|
|
665
835
|
|
|
666
|
-
/** Run one claude plugin command, inheriting its output; rejects on failure.
|
|
836
|
+
/** Run one claude plugin command, inheriting its output; rejects on failure.
|
|
837
|
+
* @param {string} claudeBin
|
|
838
|
+
* @param {string[]} args
|
|
839
|
+
* @returns {Promise<void>} */
|
|
667
840
|
function runClaudeCommand(claudeBin, args) {
|
|
668
841
|
const label = `${claudeBin} ${args.join(" ")}`;
|
|
669
842
|
return new Promise((resolve, reject) => {
|
|
@@ -682,6 +855,8 @@ function runClaudeCommand(claudeBin, args) {
|
|
|
682
855
|
* `claude plugin --help` with output suppressed. Resolves `{ ok: true }` when
|
|
683
856
|
* it exits 0, otherwise `{ ok: false, message }` naming the failure (missing
|
|
684
857
|
* binary vs missing subcommand) so registration can give targeted guidance.
|
|
858
|
+
* @param {string} claudeBin
|
|
859
|
+
* @returns {Promise<{ ok: true } | { ok: false, message: string }>}
|
|
685
860
|
*/
|
|
686
861
|
function probePluginSupport(claudeBin) {
|
|
687
862
|
return new Promise((resolve) => {
|
|
@@ -705,7 +880,12 @@ function probePluginSupport(claudeBin) {
|
|
|
705
880
|
});
|
|
706
881
|
}
|
|
707
882
|
|
|
708
|
-
/**
|
|
883
|
+
/**
|
|
884
|
+
* Run a claude plugin command capturing stdout; null when the command fails.
|
|
885
|
+
* @param {string} claudeBin
|
|
886
|
+
* @param {string[]} args
|
|
887
|
+
* @returns {Promise<string | null>}
|
|
888
|
+
*/
|
|
709
889
|
function queryClaudeCommand(claudeBin, args) {
|
|
710
890
|
const label = `${claudeBin} ${args.join(" ")}`;
|
|
711
891
|
return new Promise((resolve, reject) => {
|
|
@@ -725,6 +905,9 @@ function queryClaudeCommand(claudeBin, args) {
|
|
|
725
905
|
* Whether `claude plugin marketplace list` already registers `marketplace`.
|
|
726
906
|
* Unparseable output counts as not-registered: the fallback `marketplace add`
|
|
727
907
|
* is itself idempotent (the CLI reports "already on disk", exit 0).
|
|
908
|
+
* @param {string} claudeBin
|
|
909
|
+
* @param {string} marketplace
|
|
910
|
+
* @returns {Promise<boolean>}
|
|
728
911
|
*/
|
|
729
912
|
async function marketplaceRegistered(claudeBin, marketplace) {
|
|
730
913
|
const out = await queryClaudeCommand(claudeBin, ["plugin", "marketplace", "list", "--json"]);
|
|
@@ -741,6 +924,9 @@ async function marketplaceRegistered(claudeBin, marketplace) {
|
|
|
741
924
|
* Installed version of `pluginId` per `claude plugin list --json`; null when
|
|
742
925
|
* not installed or the output is unreadable (the caller falls back to the
|
|
743
926
|
* idempotent `plugin install`).
|
|
927
|
+
* @param {string} claudeBin
|
|
928
|
+
* @param {string} pluginId
|
|
929
|
+
* @returns {Promise<string | null>}
|
|
744
930
|
*/
|
|
745
931
|
async function installedPluginVersion(claudeBin, pluginId) {
|
|
746
932
|
const out = await queryClaudeCommand(claudeBin, ["plugin", "list", "--json"]);
|
|
@@ -790,8 +976,7 @@ async function installedPluginVersion(claudeBin, pluginId) {
|
|
|
790
976
|
* @param {string} [options.pluginVersion] Materialised plugin version (default: read from plugin.json)
|
|
791
977
|
* @param {boolean} [options.requireRegister] Exit non-zero when registration cannot run
|
|
792
978
|
* @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
|
|
793
|
-
* @returns {
|
|
794
|
-
* dryRun: boolean, registered: boolean, skipped: string[] }}
|
|
979
|
+
* @returns {Promise<RegisterClaudeResult>}
|
|
795
980
|
*/
|
|
796
981
|
export async function registerClaude({
|
|
797
982
|
root = PACKAGE_ROOT,
|
|
@@ -809,7 +994,7 @@ export async function registerClaude({
|
|
|
809
994
|
const version = pluginVersion ?? (await readMaterialisedPluginVersion(root, manifestObj));
|
|
810
995
|
const home = resolve(root);
|
|
811
996
|
|
|
812
|
-
const summary = { marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] };
|
|
997
|
+
const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] });
|
|
813
998
|
console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
|
|
814
999
|
|
|
815
1000
|
const manualCommands = [
|