@uniqbit/mate-core 0.15.5 → 0.16.0-canary.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/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +6 -4
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +47 -0
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
|
|
32
32
|
// Storage contract: <companionPath>/.graphify/<repositoryId>/graphify-out/
|
|
33
33
|
export const GRAPHIFY_STORE_SEGMENT = ".graphify";
|
|
34
|
+
|
|
35
|
+
/** Skill tree the external `graphify install` CLI writes into a runtime directory. */
|
|
36
|
+
export const GRAPHIFY_SKILL_NAME = "graphify";
|
|
34
37
|
export const GRAPHIFY_OUTPUT_SUBDIR = "graphify-out";
|
|
35
38
|
|
|
36
39
|
const GRAPHIFY_INSTALL_CMD = `uv tool install graphifyy`;
|
|
@@ -271,9 +274,12 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
271
274
|
// every setup/sync so it survives graphifyy regenerating the skill.
|
|
272
275
|
const patchSkillTree =
|
|
273
276
|
providerId === "claude" ? patchClaudeSkillTree : patchOpenCodeSkillTree;
|
|
274
|
-
await patchSkillTree(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
await patchSkillTree(
|
|
278
|
+
ctx.companionPath,
|
|
279
|
+
GRAPHIFY_SKILL_NAME,
|
|
280
|
+
rewriteGraphifySkillOutputPaths,
|
|
281
|
+
{ excludeFiles: GRAPHIFY_SKILL_REWRITE_EXCLUDE },
|
|
282
|
+
);
|
|
277
283
|
},
|
|
278
284
|
|
|
279
285
|
async teardown(ctx: SetupContext) {
|
|
@@ -282,10 +288,13 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
282
288
|
// Remove the Graphify-managed skills directory (written by the
|
|
283
289
|
// external CLI, so removed here rather than via a declaration).
|
|
284
290
|
try {
|
|
285
|
-
await fs.rm(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
291
|
+
await fs.rm(
|
|
292
|
+
path.join(ctx.companionPath, providerDir, "skills", GRAPHIFY_SKILL_NAME),
|
|
293
|
+
{
|
|
294
|
+
recursive: true,
|
|
295
|
+
force: true,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
289
298
|
} catch {
|
|
290
299
|
/* not present */
|
|
291
300
|
}
|
|
@@ -32,14 +32,17 @@ export const OPENSPEC_SKILLS = [
|
|
|
32
32
|
"openspec-archive-change",
|
|
33
33
|
] as const;
|
|
34
34
|
|
|
35
|
-
// Mate-authored skills (not generated by the openspec CLI)
|
|
36
|
-
//
|
|
37
|
-
const LEGACY_CLAUDE_STATE_PATTERN = /^mate-artifact-finish\..+\.json$/;
|
|
35
|
+
// Mate-authored skills (not generated by the openspec CLI) are sourced from the
|
|
36
|
+
// shared Mate skill tree and installed per active provider while OpenSpec is enabled.
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
/** Every Mate-owned schema is installed for availability; only one is ever activated as the companion default. */
|
|
39
|
+
const MATE_SCHEMA_NAMES = ["mate-v1", "mate-minimal"] as const;
|
|
40
|
+
|
|
41
|
+
const MATE_SCHEMA_SOURCE_ROOT = path.join(
|
|
40
42
|
import.meta.dirname,
|
|
41
|
-
"../../../templates/capabilities/openspec-cap
|
|
43
|
+
"../../../templates/capabilities/openspec-cap",
|
|
42
44
|
);
|
|
45
|
+
const MATE_CONVENTIONS_SOURCE = path.join(MATE_SCHEMA_SOURCE_ROOT, "openspec-conventions.yaml");
|
|
43
46
|
|
|
44
47
|
const OPENSPEC_TOOL_DIRS = {
|
|
45
48
|
claude: ".claude",
|
|
@@ -57,6 +60,7 @@ export interface OpenSpecPluginDeps {
|
|
|
57
60
|
isCommandOnPath?: typeof isCommandOnPath;
|
|
58
61
|
confirm?: typeof confirm;
|
|
59
62
|
pathValue?: () => string;
|
|
63
|
+
wrapperBinPath?: () => string;
|
|
60
64
|
getInstalledVersion?: (pathValue: string) => Promise<string | undefined>;
|
|
61
65
|
fetchLatestVersion?: () => Promise<string | undefined>;
|
|
62
66
|
}
|
|
@@ -95,8 +99,12 @@ function getOpenSpecConfigPath(companionPath: string): string {
|
|
|
95
99
|
return path.join(companionPath, "openspec", "config.yaml");
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
function
|
|
99
|
-
return path.join(companionPath, "openspec", "schemas",
|
|
102
|
+
function getMateSchemaDir(companionPath: string, schemaName: string): string {
|
|
103
|
+
return path.join(companionPath, "openspec", "schemas", schemaName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMateConventionsPath(companionPath: string): string {
|
|
107
|
+
return path.join(companionPath, "openspec", "mate-conventions.yaml");
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
export async function teardownOpenspecSkills(
|
|
@@ -154,21 +162,15 @@ async function reconcileOpenSpecTools(
|
|
|
154
162
|
|
|
155
163
|
// The openspec CLI generates its own workflow skills above; layer the
|
|
156
164
|
// mate-authored skills on top for the same active providers.
|
|
157
|
-
|
|
158
|
-
await applyMateOpenspecSkills(ctx.companionPath, tools);
|
|
159
|
-
} else {
|
|
160
|
-
for (const tool of tools) {
|
|
161
|
-
await teardownMateSkills(getSkillsDir(ctx.companionPath, tool), ctx.companionPath);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
165
|
+
await applyMateOpenspecSkills(ctx.companionPath, tools);
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
async function
|
|
168
|
+
async function updateOpenSpecIfOutdated(
|
|
167
169
|
ctx: SetupContext,
|
|
168
170
|
deps: Required<
|
|
169
171
|
Pick<
|
|
170
172
|
OpenSpecPluginDeps,
|
|
171
|
-
"installCommand" | "
|
|
173
|
+
"installCommand" | "pathValue" | "getInstalledVersion" | "fetchLatestVersion"
|
|
172
174
|
>
|
|
173
175
|
>,
|
|
174
176
|
): Promise<void> {
|
|
@@ -178,21 +180,9 @@ async function warnIfOpenSpecOutdated(
|
|
|
178
180
|
const latest = await deps.fetchLatestVersion();
|
|
179
181
|
if (!latest || !isNewer(latest, installed)) return;
|
|
180
182
|
|
|
181
|
-
if (ctx.mode !== "setup") {
|
|
182
|
-
process.stderr.write(
|
|
183
|
-
`openspec: installed version ${installed} is outdated (latest ${latest}). Run \`${OPENSPEC_INSTALL_COMMAND}\` to upgrade.\n`,
|
|
184
|
-
);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
183
|
process.stdout.write(
|
|
189
|
-
`openspec: installed version ${installed} is outdated (latest ${latest})
|
|
184
|
+
`openspec: installed version ${installed} is outdated (latest ${latest}). Updating now:\n ${OPENSPEC_INSTALL_COMMAND}\n`,
|
|
190
185
|
);
|
|
191
|
-
const ok = await deps.confirm("Upgrade openspec now?");
|
|
192
|
-
if (!ok) {
|
|
193
|
-
process.stderr.write("openspec: upgrade skipped - continuing with the installed version\n");
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
186
|
|
|
197
187
|
try {
|
|
198
188
|
await deps.installCommand("npm", ["install", "-g", "@fission-ai/openspec@latest"], {
|
|
@@ -218,7 +208,7 @@ async function ensureOpenSpecInstalled(
|
|
|
218
208
|
>,
|
|
219
209
|
): Promise<boolean> {
|
|
220
210
|
if (deps.isCommandOnPath("openspec", deps.pathValue())) {
|
|
221
|
-
await
|
|
211
|
+
await updateOpenSpecIfOutdated(ctx, deps);
|
|
222
212
|
return true;
|
|
223
213
|
}
|
|
224
214
|
|
|
@@ -266,11 +256,8 @@ async function ensureOpenSpecInstalled(
|
|
|
266
256
|
return true;
|
|
267
257
|
}
|
|
268
258
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await fs.rm(schemaDir, { recursive: true, force: true });
|
|
272
|
-
await pruneEmptyAncestors(schemaDir, companionPath);
|
|
273
|
-
|
|
259
|
+
/** Only the config Mate itself wrote is removed; a user-edited config.yaml is left alone. */
|
|
260
|
+
async function removeManagedOpenSpecConfig(companionPath: string): Promise<void> {
|
|
274
261
|
const configPath = getOpenSpecConfigPath(companionPath);
|
|
275
262
|
try {
|
|
276
263
|
const content = await fs.readFile(configPath, "utf8");
|
|
@@ -283,13 +270,39 @@ async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
|
|
|
283
270
|
}
|
|
284
271
|
}
|
|
285
272
|
|
|
273
|
+
async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
|
|
274
|
+
for (const schemaName of MATE_SCHEMA_NAMES) {
|
|
275
|
+
const schemaDir = getMateSchemaDir(companionPath, schemaName);
|
|
276
|
+
await fs.rm(schemaDir, { recursive: true, force: true });
|
|
277
|
+
await pruneEmptyAncestors(schemaDir, companionPath);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await fs.rm(getMateConventionsPath(companionPath), { force: true });
|
|
281
|
+
await removeManagedOpenSpecConfig(companionPath);
|
|
282
|
+
}
|
|
283
|
+
|
|
286
284
|
async function reconcileOpenSpecSchema(ctx: SetupContext): Promise<void> {
|
|
285
|
+
await Promise.all(
|
|
286
|
+
MATE_SCHEMA_NAMES.map((schemaName) =>
|
|
287
|
+
mergeDir(
|
|
288
|
+
path.join(MATE_SCHEMA_SOURCE_ROOT, schemaName),
|
|
289
|
+
getMateSchemaDir(ctx.companionPath, schemaName),
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
await fs.rm(
|
|
294
|
+
path.join(getMateSchemaDir(ctx.companionPath, "mate-v1"), "templates", "explore-brief.md"),
|
|
295
|
+
{ force: true },
|
|
296
|
+
);
|
|
297
|
+
await fs.copyFile(MATE_CONVENTIONS_SOURCE, getMateConventionsPath(ctx.companionPath));
|
|
298
|
+
|
|
299
|
+
// Availability is unconditional so any change may select a Mate schema;
|
|
300
|
+
// activation stays opt-in through the companion-level setup selection.
|
|
287
301
|
if (getOpenSpecSchemaSelection(ctx.config.capabilities ?? []) !== "mate-v1") {
|
|
288
|
-
await
|
|
302
|
+
await removeManagedOpenSpecConfig(ctx.companionPath);
|
|
289
303
|
return;
|
|
290
304
|
}
|
|
291
305
|
|
|
292
|
-
await mergeDir(MATE_V1_SCHEMA_SOURCE, getMateV1SchemaDir(ctx.companionPath));
|
|
293
306
|
const configPath = getOpenSpecConfigPath(ctx.companionPath);
|
|
294
307
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
295
308
|
|
|
@@ -310,6 +323,7 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
310
323
|
isCommandOnPath: deps.isCommandOnPath ?? isCommandOnPath,
|
|
311
324
|
confirm: deps.confirm ?? confirm,
|
|
312
325
|
pathValue: deps.pathValue ?? (() => process.env.PATH ?? ""),
|
|
326
|
+
wrapperBinPath: deps.wrapperBinPath ?? getWrapperBinPath,
|
|
313
327
|
getInstalledVersion: deps.getInstalledVersion ?? defaultGetInstalledVersion,
|
|
314
328
|
fetchLatestVersion: deps.fetchLatestVersion ?? defaultFetchLatestVersion,
|
|
315
329
|
};
|
|
@@ -333,11 +347,18 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
333
347
|
"Skill(openspec-propose)",
|
|
334
348
|
"Skill(openspec-apply-change)",
|
|
335
349
|
"Skill(openspec-archive-change)",
|
|
336
|
-
"Skill(mate-artifact-
|
|
350
|
+
"Skill(mate-artifact-publish)",
|
|
337
351
|
"Skill(mate-openspec-backfill)",
|
|
352
|
+
"Skill(mate-interview-me)",
|
|
353
|
+
"Skill(mate-grill-me)",
|
|
354
|
+
"Skill(mate-grilling)",
|
|
355
|
+
"Skill(mate-grill-with-docs)",
|
|
356
|
+
"Skill(mate-domain-modeling)",
|
|
357
|
+
"Skill(mate-show-me)",
|
|
358
|
+
"Skill(mate-simplify-code)",
|
|
338
359
|
"Bash(openspec:*)",
|
|
339
360
|
`Bash(${FRAMEWORK_NAME} cap graphify:*)`,
|
|
340
|
-
`Bash(${path.join(
|
|
361
|
+
`Bash(${path.join(depsWithDefaults.wrapperBinPath(), "openspec")}:*)`,
|
|
341
362
|
],
|
|
342
363
|
},
|
|
343
364
|
};
|
|
@@ -375,23 +396,9 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
375
396
|
},
|
|
376
397
|
forProvider: {
|
|
377
398
|
claude: {
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
async apply(ctx: SetupContext) {
|
|
382
|
-
const stateDir = path.join(ctx.companionPath, ".claude", "state");
|
|
383
|
-
try {
|
|
384
|
-
const entries = await fs.readdir(stateDir);
|
|
385
|
-
await Promise.all(
|
|
386
|
-
entries
|
|
387
|
-
.filter((entry) => LEGACY_CLAUDE_STATE_PATTERN.test(entry))
|
|
388
|
-
.map((entry) => fs.unlink(path.join(stateDir, entry))),
|
|
389
|
-
);
|
|
390
|
-
await pruneEmptyAncestors(stateDir, ctx.companionPath);
|
|
391
|
-
} catch {
|
|
392
|
-
/* no legacy state */
|
|
393
|
-
}
|
|
394
|
-
},
|
|
399
|
+
// Publication is explicit (`mate-artifact-publish`), so openspec
|
|
400
|
+
// contributes no archive-triggered Claude hook and no hook state.
|
|
401
|
+
async apply() {},
|
|
395
402
|
async teardown(ctx: SetupContext) {
|
|
396
403
|
await teardownToolRuntime(ctx.companionPath, "claude");
|
|
397
404
|
},
|
|
@@ -10,7 +10,10 @@ export { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "./tokensave-shared";
|
|
|
10
10
|
|
|
11
11
|
export const TOKENSAVE_SUPPORTED_AGENTS = new Set(["claude", "opencode"]);
|
|
12
12
|
export const TOKENSAVE_STORE_DIR = ".tokensave";
|
|
13
|
+
export const TOKENSAVE_STORE_CONFIG_FILE = "config.json";
|
|
13
14
|
export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
|
|
15
|
+
/** Single-graph posture Mate owns in the per-project store config; every other key is left as found. */
|
|
16
|
+
const TOKENSAVE_OWNED_POSTURE = { auto_track: false, suppress_scope_warning: true } as const;
|
|
14
17
|
const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
|
|
15
18
|
const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
|
|
16
19
|
const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
|
|
@@ -230,6 +233,49 @@ export async function ensureTokensaveStoreExcluded(repoPath: string): Promise<vo
|
|
|
230
233
|
await fs.writeFile(excludePath, lines.join("\n") + "\n", "utf8");
|
|
231
234
|
}
|
|
232
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Reconciles Mate's single-graph branching posture into the working repo's
|
|
238
|
+
* git-excluded `.tokensave/config.json`. Best-effort by design: the file is
|
|
239
|
+
* created by `tokensave init`, so an absent, unreadable, or unparseable config
|
|
240
|
+
* is left untouched rather than treated as an error.
|
|
241
|
+
*/
|
|
242
|
+
export async function ensureTokensaveBranchingPosture(repoPath: string): Promise<void> {
|
|
243
|
+
const configPath = path.join(repoPath, TOKENSAVE_STORE_DIR, TOKENSAVE_STORE_CONFIG_FILE);
|
|
244
|
+
|
|
245
|
+
let raw: string;
|
|
246
|
+
try {
|
|
247
|
+
raw = await fs.readFile(configPath, "utf8");
|
|
248
|
+
} catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let parsed: unknown;
|
|
253
|
+
try {
|
|
254
|
+
parsed = JSON.parse(raw);
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const next = JSON.stringify(
|
|
263
|
+
{ ...(parsed as Record<string, unknown>), ...TOKENSAVE_OWNED_POSTURE },
|
|
264
|
+
null,
|
|
265
|
+
2,
|
|
266
|
+
);
|
|
267
|
+
const serialized = `${next}\n`;
|
|
268
|
+
if (serialized === raw) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await fs.writeFile(configPath, serialized, "utf8");
|
|
274
|
+
} catch {
|
|
275
|
+
/** Best-effort: the store config is not Mate's to guarantee. */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
233
279
|
async function teardownDriver(repoPath: string, providers: string[]) {
|
|
234
280
|
const agents = providers.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p));
|
|
235
281
|
if (agents.length === 0) return;
|
|
@@ -328,6 +374,7 @@ export function createTokensavePlugin(): CapabilityPlugin {
|
|
|
328
374
|
if (!(await ensureTokensaveInstalled(targetPath))) {
|
|
329
375
|
return;
|
|
330
376
|
}
|
|
377
|
+
await ensureTokensaveBranchingPosture(targetPath);
|
|
331
378
|
if (ctx.mode === "setup") {
|
|
332
379
|
installTokensaveAgentIntegrations(ctx.activeProviders, targetPath);
|
|
333
380
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// oxlint-disable no-await-in-loop
|
|
2
2
|
import { getActiveDistribution } from "../../distribution";
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
|
+
import type { RenderedRuntimeDocument } from "../../lib/orchestrator/projection-types";
|
|
4
5
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
5
6
|
import { collectHostingProviders, ContextServiceMediator } from "./context-services";
|
|
6
7
|
import type {
|
|
@@ -12,6 +13,7 @@ import type {
|
|
|
12
13
|
} from "./plugin";
|
|
13
14
|
import { reconcileClaudeContributions } from "./providers/claude";
|
|
14
15
|
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
16
|
+
import { renderDocumentsForTarget } from "./runtime-documents";
|
|
15
17
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
16
18
|
|
|
17
19
|
export interface SetupInstallationPlanAction {
|
|
@@ -189,16 +191,14 @@ const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
|
189
191
|
};
|
|
190
192
|
|
|
191
193
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* providers are always done first. Disabled capabilities participate with
|
|
195
|
-
* `enabled: false` and drive teardown of their managed entries.
|
|
194
|
+
* The declared Capability contributions of one pass, keyed by runtime. Shared
|
|
195
|
+
* by both targets: what a companion pass writes is what a working pass renders.
|
|
196
196
|
*/
|
|
197
|
-
async function
|
|
197
|
+
async function collectContributionInputs(
|
|
198
198
|
ctx: SetupContext,
|
|
199
199
|
plugins: Plugin[],
|
|
200
200
|
plan: SetupInstallationPlan,
|
|
201
|
-
): Promise<
|
|
201
|
+
): Promise<Map<string, CapabilityContributionInput[]>> {
|
|
202
202
|
// The plan already resolved enablement (policy, saved selection, package
|
|
203
203
|
// manager requirements); mirror it instead of recomputing.
|
|
204
204
|
const enabledByPluginId = new Map(
|
|
@@ -225,10 +225,38 @@ async function reconcileCapabilityContributions(
|
|
|
225
225
|
inputsByRuntime.set(runtimeId, inputs);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
return inputsByRuntime;
|
|
229
|
+
}
|
|
228
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Reconcile declared Capability contributions through every active runtime's
|
|
233
|
+
* Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
|
|
234
|
+
* providers are always done first. Disabled capabilities participate with
|
|
235
|
+
* `enabled: false` and drive teardown of their managed entries.
|
|
236
|
+
*/
|
|
237
|
+
async function reconcileCapabilityContributions(
|
|
238
|
+
ctx: SetupContext,
|
|
239
|
+
plugins: Plugin[],
|
|
240
|
+
plan: SetupInstallationPlan,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const inputsByRuntime = await collectContributionInputs(ctx, plugins, plan);
|
|
229
243
|
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
230
244
|
const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
|
|
231
245
|
if (!reconcile || inputs.length === 0) continue;
|
|
232
246
|
await reconcile(ctx, inputs);
|
|
233
247
|
}
|
|
234
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The same declarations, rendered rather than written: a working-target pass
|
|
252
|
+
* produces the documents and the Projection Root places them, so no Runtime
|
|
253
|
+
* Surface reaches inside a Working Repository.
|
|
254
|
+
*/
|
|
255
|
+
export async function renderRuntimeDocuments(
|
|
256
|
+
ctx: SetupContext,
|
|
257
|
+
registrations: PluginRegistration[],
|
|
258
|
+
plan: SetupInstallationPlan,
|
|
259
|
+
): Promise<RenderedRuntimeDocument[]> {
|
|
260
|
+
const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
|
|
261
|
+
return renderDocumentsForTarget(ctx, await collectContributionInputs(ctx, plugins, plan));
|
|
262
|
+
}
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -3,18 +3,22 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { pruneEmptyAncestors } from "./utils";
|
|
5
5
|
|
|
6
|
-
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-
|
|
6
|
+
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-publish"] as const;
|
|
7
7
|
export const MATE_SKILLS = [
|
|
8
|
-
"mate-artifact-
|
|
8
|
+
"mate-artifact-publish",
|
|
9
9
|
"mate-create-report",
|
|
10
10
|
"mate-openspec-backfill",
|
|
11
|
+
"mate-interview-me",
|
|
12
|
+
"mate-grill-me",
|
|
13
|
+
"mate-grilling",
|
|
14
|
+
"mate-grill-with-docs",
|
|
15
|
+
"mate-domain-modeling",
|
|
16
|
+
"mate-show-me",
|
|
17
|
+
"mate-simplify-code",
|
|
11
18
|
] as const;
|
|
12
|
-
const LEGACY_MATE_SKILLS = ["mate-openspec-artifact-finish"] as const;
|
|
19
|
+
const LEGACY_MATE_SKILLS = ["mate-artifact-finish", "mate-openspec-artifact-finish"] as const;
|
|
13
20
|
|
|
14
|
-
const MATE_SKILLS_SOURCE = path.join(
|
|
15
|
-
import.meta.dirname,
|
|
16
|
-
"../../templates/capabilities/openspec-cap/mate-skills",
|
|
17
|
-
);
|
|
21
|
+
const MATE_SKILLS_SOURCE = path.join(import.meta.dirname, "../../templates/mate-skills");
|
|
18
22
|
|
|
19
23
|
const MATE_CREATE_REPORT_SKILL = `---
|
|
20
24
|
name: mate-create-report
|
|
@@ -55,7 +59,27 @@ Write a JSON document with this shape:
|
|
|
55
59
|
}
|
|
56
60
|
\`\`\`
|
|
57
61
|
|
|
58
|
-
Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`, \`statuses\`, and \`
|
|
62
|
+
Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`, \`statuses\`, \`text\`, \`diagram\`, and \`diff\`. Use only strings, numbers, booleans, and null values. Section IDs must be unique. Validate required fields before invoking the CLI.
|
|
63
|
+
|
|
64
|
+
## Visual Sections
|
|
65
|
+
|
|
66
|
+
A \`diagram\` section carries exactly one payload. \`mermaid\` holds diagram source the report draws as a picture; \`text\` holds a monospace ASCII sketch or pseudocode block, kept with its line breaks and indentation. Supplying both payloads or neither fails validation.
|
|
67
|
+
|
|
68
|
+
\`\`\`json
|
|
69
|
+
{ "id": "structure", "title": "Structure", "type": "diagram", "mermaid": "classDiagram\\n Report --> Renderer" }
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
\`\`\`json
|
|
73
|
+
{ "id": "sketch", "title": "Sketch", "type": "diagram", "text": "root\\n child" }
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
A \`diff\` section carries a unified diff patch as produced by \`git diff\`. The report groups it by file, marks added, removed, and context lines, and keeps those markings legible in print. An empty patch renders an explicit no-data message.
|
|
77
|
+
|
|
78
|
+
\`\`\`json
|
|
79
|
+
{ "id": "changes", "title": "Changes", "type": "diff", "patch": "diff --git a/src/acme.ts b/src/acme.ts\\n..." }
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
A report carrying a mermaid payload inlines the diagram runtime; a report without one does not. Either way the page stays a single self-contained file that makes no network request.
|
|
59
83
|
|
|
60
84
|
## Invocation
|
|
61
85
|
|
|
@@ -87,10 +111,8 @@ const DEFAULT_MATE_SKILLS_BUCKET = "agents";
|
|
|
87
111
|
/**
|
|
88
112
|
* A skill's source lives under a provider bucket so its folder name always
|
|
89
113
|
* matches its `name:` frontmatter: `<tool>/<skill>/` when that tool needs its
|
|
90
|
-
* own behavior
|
|
91
|
-
*
|
|
92
|
-
* `agents/<skill>/` — the shared default every other tool (e.g. opencode,
|
|
93
|
-
* which pushes automatically) uses.
|
|
114
|
+
* own behavior, falling back to `agents/<skill>/` — the shared default every
|
|
115
|
+
* other tool uses.
|
|
94
116
|
*/
|
|
95
117
|
async function resolveMateSkillSource(skill: string, tool: string): Promise<string> {
|
|
96
118
|
const providerDir = path.join(MATE_SKILLS_SOURCE, tool, skill);
|
|
@@ -98,8 +120,15 @@ async function resolveMateSkillSource(skill: string, tool: string): Promise<stri
|
|
|
98
120
|
await fs.access(providerDir);
|
|
99
121
|
return providerDir;
|
|
100
122
|
} catch {
|
|
101
|
-
|
|
123
|
+
const sharedDir = path.join(MATE_SKILLS_SOURCE, DEFAULT_MATE_SKILLS_BUCKET, skill);
|
|
124
|
+
try {
|
|
125
|
+
await fs.access(sharedDir);
|
|
126
|
+
return sharedDir;
|
|
127
|
+
} catch {
|
|
128
|
+
/* source is missing */
|
|
129
|
+
}
|
|
102
130
|
}
|
|
131
|
+
throw new Error(`Mate skill source not found: ${skill}`);
|
|
103
132
|
}
|
|
104
133
|
|
|
105
134
|
export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
|
|
@@ -4,6 +4,13 @@ import type { ClaudeHookGroup } from "./providers/claude-format";
|
|
|
4
4
|
|
|
5
5
|
export type SetupScope = "companion" | "hub";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Where a reconcile pass lands its documents. `companion` writes the material's
|
|
9
|
+
* own repository; `working` renders the documents an Agent Runtime discovers by
|
|
10
|
+
* its own upward walk and leaves the placing to the Projection Root.
|
|
11
|
+
*/
|
|
12
|
+
export type ProjectionTarget = "companion" | "working";
|
|
13
|
+
|
|
7
14
|
export interface SetupContext {
|
|
8
15
|
companionPath: string;
|
|
9
16
|
config: FrameworkConfig;
|
|
@@ -13,6 +20,8 @@ export interface SetupContext {
|
|
|
13
20
|
scope?: SetupScope;
|
|
14
21
|
/** Working repository path when syncing from a linked repo (e.g. during launch). */
|
|
15
22
|
repoPath?: string;
|
|
23
|
+
/** Defaults to `companion`, so a caller that names no target is unchanged. */
|
|
24
|
+
target?: ProjectionTarget;
|
|
16
25
|
/**
|
|
17
26
|
* Provider-mediated context services. Always populated when the engine
|
|
18
27
|
* executes a plugin; optional so hand-built contexts (tests, direct calls)
|
|
@@ -104,8 +104,18 @@ export async function refreshFromTemplate(
|
|
|
104
104
|
|
|
105
105
|
remaining = removeGraphifySection(remaining).trimEnd();
|
|
106
106
|
|
|
107
|
+
const template = templateContent.trimEnd();
|
|
108
|
+
/**
|
|
109
|
+
* A template carrying no managed markers cannot be stripped by marker above,
|
|
110
|
+
* so without this it would be prepended again on every refresh. Consuming
|
|
111
|
+
* every leading copy also heals a file an earlier refresh already grew.
|
|
112
|
+
*/
|
|
113
|
+
while (template && remaining.startsWith(template)) {
|
|
114
|
+
remaining = remaining.slice(template.length).trimStart();
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
const sections = [
|
|
108
|
-
|
|
118
|
+
template,
|
|
109
119
|
...(normalizedOptions.appendSections ?? []).map((section) => section.trim()).filter(Boolean),
|
|
110
120
|
];
|
|
111
121
|
if (remaining) {
|
|
@@ -13,9 +13,9 @@ export interface McpEntryDescriptor {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Claude runtime config format primitives: parse/merge/serialize for
|
|
16
|
-
// `settings.local.json` (hook maps, permissions) and `.mcp.json`.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// `settings.local.json` (hook maps, permissions) and `.mcp.json`. Which hook
|
|
17
|
+
// groups are Mate-managed lives here too, because the companion-side reconcile
|
|
18
|
+
// and the working-repo projection entry must strip the same set.
|
|
19
19
|
|
|
20
20
|
export interface ClaudeHookCommand {
|
|
21
21
|
type?: string;
|
|
@@ -82,6 +82,46 @@ export function filterClaudeHookGroups(
|
|
|
82
82
|
return filtered;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Command substrings that mark a hook group as Mate-managed. The mate plugin
|
|
86
|
+
// hooks (validate-artifact-path, mate-session-banner) now ship in the bundled
|
|
87
|
+
// Claude plugin, and the retired mate-artifact-finish.sh archive hook is gone
|
|
88
|
+
// entirely; their markers are retained migration-only so stale managed groups
|
|
89
|
+
// written by earlier releases keep being stripped, and are never re-added.
|
|
90
|
+
export const MANAGED_HOOK_MARKERS = [
|
|
91
|
+
"validate-artifact-path",
|
|
92
|
+
"mate-session-banner",
|
|
93
|
+
"react-doctor.sh",
|
|
94
|
+
"mate-artifact-finish.sh",
|
|
95
|
+
"tokensave",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
export function isManagedHookGroup(group: ClaudeHookGroup, extraMarkers: string[] = []): boolean {
|
|
99
|
+
return (group.hooks ?? []).some((hook) =>
|
|
100
|
+
[...MANAGED_HOOK_MARKERS, ...extraMarkers].some((marker) =>
|
|
101
|
+
(hook.command ?? "").includes(marker),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Drops every managed hook group, and the `hooks` key itself when none remain. */
|
|
107
|
+
export function removeManagedHookGroups(
|
|
108
|
+
settings: ClaudeSettings,
|
|
109
|
+
extraMarkers: string[] = [],
|
|
110
|
+
): ClaudeSettings {
|
|
111
|
+
const hooks = filterClaudeHookGroups(
|
|
112
|
+
settings.hooks ?? {},
|
|
113
|
+
(group) => !isManagedHookGroup(group, extraMarkers),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const next = { ...settings };
|
|
117
|
+
if (Object.keys(hooks).length > 0) {
|
|
118
|
+
next.hooks = hooks;
|
|
119
|
+
} else {
|
|
120
|
+
delete next.hooks;
|
|
121
|
+
}
|
|
122
|
+
return next;
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
/** Tolerant read: absent or malformed settings read as an empty object. */
|
|
86
126
|
export async function readClaudeSettings(settingsPath: string): Promise<ClaudeSettings> {
|
|
87
127
|
try {
|
|
@@ -95,12 +135,17 @@ export async function readClaudeSettings(settingsPath: string): Promise<ClaudeSe
|
|
|
95
135
|
return {};
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
/** The exact bytes a settings document is written as; comparable against a read. */
|
|
139
|
+
export function serializeClaudeSettings(settings: ClaudeSettings): string {
|
|
140
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
141
|
+
}
|
|
142
|
+
|
|
98
143
|
export async function writeClaudeSettings(
|
|
99
144
|
settingsPath: string,
|
|
100
145
|
settings: ClaudeSettings,
|
|
101
146
|
): Promise<void> {
|
|
102
147
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
103
|
-
await fs.writeFile(settingsPath,
|
|
148
|
+
await fs.writeFile(settingsPath, serializeClaudeSettings(settings), "utf8");
|
|
104
149
|
}
|
|
105
150
|
|
|
106
151
|
/** Tolerant read of `.mcp.json`; `present` distinguishes absent from empty. */
|