@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { PROJECTED_BANNER_FLAG } from "../../../hooks/session-banner";
|
|
5
|
+
import { getClaudePluginRoot } from "../../../lib/package-paths";
|
|
6
|
+
import type { ClaudeHookGroup } from "./claude-format";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The bundled Claude plugin's hook wiring, read as a value. A managed launch
|
|
10
|
+
* loads it with `--plugin-dir`; an Unmanaged Session loads no plugin dir at
|
|
11
|
+
* all, so the working-target settings document has to carry it instead.
|
|
12
|
+
*
|
|
13
|
+
* A managed session loads the Working Repository's settings as its `local`
|
|
14
|
+
* source *alongside* the plugin, so every group carried over runs twice there.
|
|
15
|
+
* All three survive that: the guard, whose verdict is a function of the tool
|
|
16
|
+
* input alone; the guidance hook, which yields nothing when a launch
|
|
17
|
+
* environment is present and so cannot double the prompt the launch already
|
|
18
|
+
* appended; and the banner, whose carried copy is marked so it defers to the
|
|
19
|
+
* launch's.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const GUARD_HOOK_SHIM = "validate-artifact-path.mjs";
|
|
23
|
+
const GUIDANCE_HOOK_SHIM = "session-guidance.mjs";
|
|
24
|
+
const BANNER_HOOK_SHIM = "session-banner.mjs";
|
|
25
|
+
const PLUGIN_ROOT_TOKEN = "${CLAUDE_PLUGIN_ROOT}";
|
|
26
|
+
|
|
27
|
+
interface PluginHookWiring {
|
|
28
|
+
hooks?: Record<string, ClaudeHookGroup[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readPluginHookWiring(pluginRoot: string): PluginHookWiring {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(
|
|
34
|
+
fs.readFileSync(path.join(pluginRoot, "hooks", "hooks.json"), "utf8"),
|
|
35
|
+
) as unknown;
|
|
36
|
+
if (parsed && typeof parsed === "object") return parsed as PluginHookWiring;
|
|
37
|
+
} catch {
|
|
38
|
+
/* an installation missing its plugin assets contributes no hooks */
|
|
39
|
+
}
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveCommands(group: ClaudeHookGroup, pluginRoot: string): ClaudeHookGroup {
|
|
44
|
+
return {
|
|
45
|
+
...group,
|
|
46
|
+
hooks: (group.hooks ?? []).map((hook) => ({
|
|
47
|
+
...hook,
|
|
48
|
+
...(hook.command ? { command: hook.command.split(PLUGIN_ROOT_TOKEN).join(pluginRoot) } : {}),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runsShim(shim: string): (group: ClaudeHookGroup) => boolean {
|
|
54
|
+
return (group) => (group.hooks ?? []).some((hook) => (hook.command ?? "").includes(shim));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function groupsFor(
|
|
58
|
+
event: string,
|
|
59
|
+
shim: string,
|
|
60
|
+
pluginRoot: string = getClaudePluginRoot(),
|
|
61
|
+
): ClaudeHookGroup[] {
|
|
62
|
+
return (readPluginHookWiring(pluginRoot).hooks?.[event] ?? [])
|
|
63
|
+
.filter(runsShim(shim))
|
|
64
|
+
.map((group) => resolveCommands(group, pluginRoot));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The guard's `PreToolUse` groups with `${CLAUDE_PLUGIN_ROOT}` resolved against
|
|
69
|
+
* the running installation, so the command works with no launch environment.
|
|
70
|
+
*/
|
|
71
|
+
export function mateGuardHookGroups(pluginRoot: string = getClaudePluginRoot()): ClaudeHookGroup[] {
|
|
72
|
+
return groupsFor("PreToolUse", GUARD_HOOK_SHIM, pluginRoot);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The guidance hook's `SessionStart` groups, resolved the same way. This is the
|
|
77
|
+
* channel that replaces `--append-system-prompt` where no launch supplies one:
|
|
78
|
+
* a settings document can declare a hook, and there is no settings key for the
|
|
79
|
+
* flag.
|
|
80
|
+
*/
|
|
81
|
+
export function mateGuidanceHookGroups(
|
|
82
|
+
pluginRoot: string = getClaudePluginRoot(),
|
|
83
|
+
): ClaudeHookGroup[] {
|
|
84
|
+
return groupsFor("SessionStart", GUIDANCE_HOOK_SHIM, pluginRoot);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The banner's `SessionStart` group, resolved the same way and carrying
|
|
89
|
+
* `PROJECTED_BANNER_FLAG`. Without the marker the carried command would be
|
|
90
|
+
* byte-identical to the plugin's, and a managed session — which loads both —
|
|
91
|
+
* would print the banner twice.
|
|
92
|
+
*/
|
|
93
|
+
export function mateBannerHookGroups(
|
|
94
|
+
pluginRoot: string = getClaudePluginRoot(),
|
|
95
|
+
): ClaudeHookGroup[] {
|
|
96
|
+
return groupsFor("SessionStart", BANNER_HOOK_SHIM, pluginRoot).map((group) => ({
|
|
97
|
+
...group,
|
|
98
|
+
hooks: (group.hooks ?? []).map((hook) => ({
|
|
99
|
+
...hook,
|
|
100
|
+
...(hook.command ? { command: `${hook.command} ${PROJECTED_BANNER_FLAG}` } : {}),
|
|
101
|
+
})),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a hook group is one the working target placed. It matches a managed
|
|
107
|
+
* marker, so the entry that strips managed groups from a Working Repository has
|
|
108
|
+
* to be told to leave these alone.
|
|
109
|
+
*/
|
|
110
|
+
export function isMateGuardHookGroup(group: ClaudeHookGroup): boolean {
|
|
111
|
+
const own = new Set(
|
|
112
|
+
[...mateGuardHookGroups(), ...mateGuidanceHookGroups(), ...mateBannerHookGroups()].map(
|
|
113
|
+
(candidate) => JSON.stringify(candidate),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
return own.has(JSON.stringify(group));
|
|
117
|
+
}
|
|
@@ -3,33 +3,22 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
6
|
-
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
7
6
|
import type { FrameworkConfig } from "../../../lib/orchestrator/types";
|
|
8
7
|
import { refreshFromTemplate, stripGuidanceBlock } from "../plugins/guidance";
|
|
9
|
-
import {
|
|
10
|
-
TOKENSAVE_CLAUDE_MD_MARKER,
|
|
11
|
-
TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES,
|
|
12
|
-
} from "../capabilities/tokensave-shared";
|
|
13
8
|
import {
|
|
14
9
|
instructionBlockKey,
|
|
15
10
|
removeManagedBlocksForPlugin,
|
|
16
11
|
upsertManagedBlock,
|
|
17
12
|
} from "../context-services";
|
|
18
13
|
import type { CapabilityContributionInput, ProviderPlugin, SetupContext } from "../plugin";
|
|
14
|
+
import { surfaceRoot } from "../surface-target";
|
|
19
15
|
import { mergeDir, pruneEmptyAncestors } from "../utils";
|
|
20
|
-
import {
|
|
21
|
-
ensureWorkingRepoLocalExcludes,
|
|
22
|
-
reconcileWorkingRepoCapabilityExcludes,
|
|
23
|
-
} from "../working-repo-local-state";
|
|
24
|
-
import {
|
|
25
|
-
cutFromMarker,
|
|
26
|
-
stripSectionFromFile,
|
|
27
|
-
type RemoveHeadingSectionOptions,
|
|
28
|
-
} from "./agent-file-sections";
|
|
16
|
+
import { stripSectionFromFile, type RemoveHeadingSectionOptions } from "./agent-file-sections";
|
|
29
17
|
import { patchSkillTreeMarkdownFiles } from "./skill-tree";
|
|
30
18
|
import {
|
|
31
19
|
filterClaudeHookGroups,
|
|
32
20
|
mergeClaudeHookGroups,
|
|
21
|
+
removeManagedHookGroups,
|
|
33
22
|
readClaudeMcpConfig,
|
|
34
23
|
readClaudeSettings,
|
|
35
24
|
toClaudeMcpEntry,
|
|
@@ -68,19 +57,6 @@ const COMPANION_GITIGNORE_ENTRIES = [
|
|
|
68
57
|
".claude/state/",
|
|
69
58
|
];
|
|
70
59
|
|
|
71
|
-
// Command substrings that mark a working-repo hook group as Mate-managed.
|
|
72
|
-
// The mate plugin hooks (validate-artifact-path, mate-session-banner,
|
|
73
|
-
// mate-artifact-finish.sh) now ship in the bundled Claude plugin; their
|
|
74
|
-
// markers are retained migration-only so stale managed groups written by
|
|
75
|
-
// earlier releases keep being stripped, and are never re-added.
|
|
76
|
-
const MANAGED_HOOK_MARKERS = [
|
|
77
|
-
"validate-artifact-path",
|
|
78
|
-
"mate-session-banner",
|
|
79
|
-
"react-doctor.sh",
|
|
80
|
-
"mate-artifact-finish.sh",
|
|
81
|
-
"tokensave",
|
|
82
|
-
];
|
|
83
|
-
|
|
84
60
|
// Hook files earlier releases copied into the companion; the bundled Claude
|
|
85
61
|
// plugin replaced them. Setup and launch sync delete stale copies.
|
|
86
62
|
const LEGACY_MATE_HOOK_FILES = [
|
|
@@ -126,51 +102,6 @@ function getAllManagedPermissionEntries(companionPath: string): Set<string> {
|
|
|
126
102
|
]);
|
|
127
103
|
}
|
|
128
104
|
|
|
129
|
-
function isManagedHookGroup(group: ClaudeHookGroup, extraMarkers: string[] = []): boolean {
|
|
130
|
-
return (group.hooks ?? []).some((hook) =>
|
|
131
|
-
[...MANAGED_HOOK_MARKERS, ...extraMarkers].some((marker) =>
|
|
132
|
-
(hook.command ?? "").includes(marker),
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function removeManagedHookGroups(
|
|
138
|
-
settings: ClaudeSettings,
|
|
139
|
-
extraMarkers: string[] = [],
|
|
140
|
-
): ClaudeSettings {
|
|
141
|
-
const hooks = filterClaudeHookGroups(
|
|
142
|
-
settings.hooks ?? {},
|
|
143
|
-
(group) => !isManagedHookGroup(group, extraMarkers),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const next = { ...settings };
|
|
147
|
-
if (Object.keys(hooks).length > 0) {
|
|
148
|
-
next.hooks = hooks;
|
|
149
|
-
} else {
|
|
150
|
-
delete next.hooks;
|
|
151
|
-
}
|
|
152
|
-
return next;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function stripTokensaveClaudeMdAppend(workingRepoPath: string): Promise<boolean> {
|
|
156
|
-
const claudeMdPath = path.join(workingRepoPath, "CLAUDE.md");
|
|
157
|
-
let content: string;
|
|
158
|
-
try {
|
|
159
|
-
content = await fs.readFile(claudeMdPath, "utf8");
|
|
160
|
-
} catch {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const stripped = cutFromMarker(content, TOKENSAVE_CLAUDE_MD_MARKER);
|
|
165
|
-
if (stripped === content) return false;
|
|
166
|
-
if (stripped.length > 0) {
|
|
167
|
-
await fs.writeFile(claudeMdPath, stripped, "utf8");
|
|
168
|
-
} else {
|
|
169
|
-
await fs.unlink(claudeMdPath);
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
105
|
// Resolved path of the companion-owned Claude settings document. This is the
|
|
175
106
|
// single source of truth for Mate-managed Claude hooks, permissions, and MCP
|
|
176
107
|
// servers; it is loaded at launch via `claude --settings`.
|
|
@@ -185,11 +116,42 @@ export function getCompanionClaudeMcpConfigPath(companionPath: string): string {
|
|
|
185
116
|
return path.join(companionPath, ".mcp.json");
|
|
186
117
|
}
|
|
187
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The Mate-managed `permissions.allow` entries of one pass, in emitted order.
|
|
121
|
+
* Rendered separately from the settings document so a working-target pass can
|
|
122
|
+
* obtain them without a write.
|
|
123
|
+
*/
|
|
124
|
+
export function renderManagedClaudePermissionEntries(
|
|
125
|
+
companionPath: string,
|
|
126
|
+
contributions: CapabilityContributionInput[] = [],
|
|
127
|
+
): string[] {
|
|
128
|
+
return [
|
|
129
|
+
...getBaseManagedPermissionEntries(companionPath),
|
|
130
|
+
...contributions
|
|
131
|
+
.filter((input) => input.enabled)
|
|
132
|
+
.flatMap((input) => input.contributions.permissionEntries ?? []),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The Mate-managed `.mcp.json` server map of one pass, as a value. */
|
|
137
|
+
export function renderManagedClaudeMcpServers(
|
|
138
|
+
contributions: CapabilityContributionInput[] = [],
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
const servers: Record<string, unknown> = {};
|
|
141
|
+
for (const input of contributions) {
|
|
142
|
+
if (!input.enabled) continue;
|
|
143
|
+
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
144
|
+
servers[descriptor.name] = toClaudeMcpEntry(descriptor);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return servers;
|
|
148
|
+
}
|
|
149
|
+
|
|
188
150
|
// Reconcile the Mate-managed Claude settings shape (hooks, permissions, MCP
|
|
189
151
|
// servers) on top of whatever already exists in the settings document. Managed
|
|
190
152
|
// groups/entries always lead their arrays so the emitted shape stays stable
|
|
191
153
|
// across syncs; unmanaged content is preserved untouched.
|
|
192
|
-
function
|
|
154
|
+
export function renderManagedClaudeSettings(
|
|
193
155
|
existing: ClaudeSettings,
|
|
194
156
|
companionPath: string,
|
|
195
157
|
config: FrameworkConfig,
|
|
@@ -203,12 +165,8 @@ function buildManagedClaudeSettings(
|
|
|
203
165
|
const declaredPermissionEntries = contributions.flatMap(
|
|
204
166
|
(input) => input.contributions.permissionEntries ?? [],
|
|
205
167
|
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.flatMap((input) => input.contributions.permissionEntries ?? []);
|
|
209
|
-
|
|
210
|
-
// Mate's own hooks (artifact-path guard, session banner, archive-finish
|
|
211
|
-
// nudge) ship in the bundled Claude plugin loaded at launch; settings-sync
|
|
168
|
+
// Mate's own hooks (artifact-path guard, session banner, companion
|
|
169
|
+
// guidance) ship in the bundled Claude plugin loaded at launch; settings-sync
|
|
212
170
|
// only strips their legacy managed groups (via removeManagedHookGroups) and
|
|
213
171
|
// reconciles the capability hooks that remain settings-delivered.
|
|
214
172
|
const hooks = removeManagedHookGroups(existing, declaredMarkers).hooks ?? {};
|
|
@@ -233,10 +191,7 @@ function buildManagedClaudeSettings(
|
|
|
233
191
|
...getAllManagedPermissionEntries(companionPath),
|
|
234
192
|
...declaredPermissionEntries,
|
|
235
193
|
]);
|
|
236
|
-
const managedAllow =
|
|
237
|
-
...getBaseManagedPermissionEntries(companionPath),
|
|
238
|
-
...enabledDeclaredPermissionEntries,
|
|
239
|
-
];
|
|
194
|
+
const managedAllow = renderManagedClaudePermissionEntries(companionPath, contributions);
|
|
240
195
|
const existingPermissions = existing.permissions ?? {};
|
|
241
196
|
const existingAllow = Array.isArray(existingPermissions.allow) ? existingPermissions.allow : [];
|
|
242
197
|
const nextAllow = [
|
|
@@ -284,7 +239,7 @@ export async function syncCompanionClaudeSettings(
|
|
|
284
239
|
): Promise<void> {
|
|
285
240
|
const settingsPath = getCompanionClaudeSettingsPath(companionPath);
|
|
286
241
|
const existing = await readClaudeSettings(settingsPath);
|
|
287
|
-
const settings =
|
|
242
|
+
const settings = renderManagedClaudeSettings(existing, companionPath, config, contributions);
|
|
288
243
|
|
|
289
244
|
await writeClaudeSettings(settingsPath, settings);
|
|
290
245
|
|
|
@@ -302,11 +257,13 @@ export async function reconcileClaudeContributions(
|
|
|
302
257
|
ctx: SetupContext,
|
|
303
258
|
inputs: CapabilityContributionInput[],
|
|
304
259
|
): Promise<void> {
|
|
305
|
-
|
|
260
|
+
/** Resolved once per pass; every write site below is relative to it. */
|
|
261
|
+
const root = surfaceRoot(ctx);
|
|
262
|
+
const { config } = ctx;
|
|
306
263
|
|
|
307
264
|
if (ctx.scope === "hub") {
|
|
308
|
-
await reconcileClaudeMcpContributions(
|
|
309
|
-
await reconcileClaudeAgentDefinitionContributions(
|
|
265
|
+
await reconcileClaudeMcpContributions(root, inputs);
|
|
266
|
+
await reconcileClaudeAgentDefinitionContributions(root, inputs);
|
|
310
267
|
return;
|
|
311
268
|
}
|
|
312
269
|
|
|
@@ -314,17 +271,17 @@ export async function reconcileClaudeContributions(
|
|
|
314
271
|
// runs while the Claude runtime is active; a deactivated runtime's pass is
|
|
315
272
|
// teardown-only and must not resurrect files the provider teardown removed.
|
|
316
273
|
if (ctx.activeProviders.includes("claude")) {
|
|
317
|
-
await syncCompanionClaudeSettings(
|
|
274
|
+
await syncCompanionClaudeSettings(root, config, inputs);
|
|
318
275
|
}
|
|
319
276
|
|
|
320
|
-
await reconcileClaudeMcpContributions(
|
|
321
|
-
await reconcileClaudeAgentDefinitionContributions(
|
|
277
|
+
await reconcileClaudeMcpContributions(root, inputs);
|
|
278
|
+
await reconcileClaudeAgentDefinitionContributions(root, inputs);
|
|
322
279
|
|
|
323
280
|
for (const input of inputs) {
|
|
324
281
|
// Guidance sections are managed blocks in CLAUDE.md. Current sections are
|
|
325
282
|
// upserted, then stale keys (content changes, disabled capability) are
|
|
326
283
|
// swept. CLAUDE.md is Claude-exclusive, so no shared-file guard applies.
|
|
327
|
-
const guidancePath = path.join(
|
|
284
|
+
const guidancePath = path.join(root, "CLAUDE.md");
|
|
328
285
|
const sections = input.enabled ? (input.contributions.guidanceSections ?? []) : [];
|
|
329
286
|
const keepKeys = new Set<string>();
|
|
330
287
|
for (const section of sections) {
|
|
@@ -337,12 +294,12 @@ export async function reconcileClaudeContributions(
|
|
|
337
294
|
}
|
|
338
295
|
|
|
339
296
|
for (const skillTree of input.contributions.skillTrees ?? []) {
|
|
340
|
-
const skillDir = path.join(
|
|
297
|
+
const skillDir = path.join(root, ".claude", "skills", skillTree.name);
|
|
341
298
|
if (input.enabled) {
|
|
342
299
|
await mergeDir(skillTree.sourceDir, skillDir);
|
|
343
300
|
} else {
|
|
344
301
|
await fs.rm(skillDir, { recursive: true, force: true });
|
|
345
|
-
await pruneEmptyAncestors(path.join(
|
|
302
|
+
await pruneEmptyAncestors(path.join(root, ".claude", "skills"), root);
|
|
346
303
|
}
|
|
347
304
|
}
|
|
348
305
|
}
|
|
@@ -350,13 +307,13 @@ export async function reconcileClaudeContributions(
|
|
|
350
307
|
|
|
351
308
|
/** Reconcile only MCP entries without touching Claude settings or guidance. */
|
|
352
309
|
async function reconcileClaudeMcpContributions(
|
|
353
|
-
|
|
310
|
+
root: string,
|
|
354
311
|
inputs: CapabilityContributionInput[],
|
|
355
312
|
): Promise<void> {
|
|
356
313
|
for (const input of inputs) {
|
|
357
314
|
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
358
315
|
await updateClaudeMcpServer(
|
|
359
|
-
getCompanionClaudeMcpConfigPath(
|
|
316
|
+
getCompanionClaudeMcpConfigPath(root),
|
|
360
317
|
descriptor.name,
|
|
361
318
|
input.enabled ? toClaudeMcpEntry(descriptor) : null,
|
|
362
319
|
);
|
|
@@ -370,10 +327,10 @@ async function reconcileClaudeMcpContributions(
|
|
|
370
327
|
* (no shared-file merge), so it needs no companion-only surface.
|
|
371
328
|
*/
|
|
372
329
|
async function reconcileClaudeAgentDefinitionContributions(
|
|
373
|
-
|
|
330
|
+
root: string,
|
|
374
331
|
inputs: CapabilityContributionInput[],
|
|
375
332
|
): Promise<void> {
|
|
376
|
-
const agentsDir = path.join(
|
|
333
|
+
const agentsDir = path.join(root, ".claude", "agents");
|
|
377
334
|
for (const input of inputs) {
|
|
378
335
|
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
379
336
|
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
@@ -382,7 +339,7 @@ async function reconcileClaudeAgentDefinitionContributions(
|
|
|
382
339
|
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
383
340
|
} else {
|
|
384
341
|
await fs.rm(agentPath, { force: true });
|
|
385
|
-
await pruneEmptyAncestors(agentsDir,
|
|
342
|
+
await pruneEmptyAncestors(agentsDir, root);
|
|
386
343
|
}
|
|
387
344
|
}
|
|
388
345
|
}
|
|
@@ -401,128 +358,6 @@ async function syncCompanionClaudeMcpConfig(companionPath: string): Promise<void
|
|
|
401
358
|
});
|
|
402
359
|
}
|
|
403
360
|
|
|
404
|
-
// Reconcile the working repo for a Claude launch. Mate-managed Claude settings
|
|
405
|
-
// now live in companion settings (see `syncCompanionClaudeSettings`) and are
|
|
406
|
-
// loaded at launch via `--settings`, so Mate no longer writes them into the
|
|
407
|
-
// working repo. Existing Mate-managed hooks are removed during migration;
|
|
408
|
-
// user-authored settings remain in place. This also keeps the git exclude for
|
|
409
|
-
// that file current and strips the TokenSave CLAUDE.md append the installer may
|
|
410
|
-
// have added.
|
|
411
|
-
export async function syncWorkingRepoClaudeSettings(
|
|
412
|
-
workingRepoPath: string,
|
|
413
|
-
companionPath: string,
|
|
414
|
-
config: FrameworkConfig,
|
|
415
|
-
globalConfigStore = new GlobalConfigStore(),
|
|
416
|
-
): Promise<void> {
|
|
417
|
-
await ensureWorkingRepoLocalExcludes(workingRepoPath);
|
|
418
|
-
const tokensaveEnabled = (config.capabilities ?? []).some(
|
|
419
|
-
(capability) => capability.name === "tokensave",
|
|
420
|
-
);
|
|
421
|
-
await reconcileWorkingRepoCapabilityExcludes(
|
|
422
|
-
workingRepoPath,
|
|
423
|
-
tokensaveEnabled ? TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES : [],
|
|
424
|
-
TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES,
|
|
425
|
-
);
|
|
426
|
-
await syncWorkingRepoClaudeAdditionalDirectories(
|
|
427
|
-
workingRepoPath,
|
|
428
|
-
companionPath,
|
|
429
|
-
globalConfigStore,
|
|
430
|
-
);
|
|
431
|
-
await stripTokensaveClaudeMdAppend(workingRepoPath);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function syncWorkingRepoClaudeAdditionalDirectories(
|
|
435
|
-
workingRepoPath: string,
|
|
436
|
-
companionPath: string,
|
|
437
|
-
globalConfigStore: GlobalConfigStore,
|
|
438
|
-
): Promise<void> {
|
|
439
|
-
const settingsPath = path.join(workingRepoPath, ".claude", "settings.local.json");
|
|
440
|
-
// Migrate hooks only. Existing permissions and MCP entries stay in the
|
|
441
|
-
// working repo; additionalDirectories is reconciled below.
|
|
442
|
-
const existing = removeManagedHookGroups(await readClaudeSettings(settingsPath));
|
|
443
|
-
const permissions = { ...existing.permissions };
|
|
444
|
-
const existingAdditionalDirectories = Array.isArray(permissions.additionalDirectories)
|
|
445
|
-
? permissions.additionalDirectories
|
|
446
|
-
: [];
|
|
447
|
-
const registeredCompanionPaths = new Set(
|
|
448
|
-
(await globalConfigStore.list()).map((registeredCompanionPath) =>
|
|
449
|
-
path.resolve(registeredCompanionPath),
|
|
450
|
-
),
|
|
451
|
-
);
|
|
452
|
-
const resolvedCompanionPath = path.resolve(companionPath);
|
|
453
|
-
const additionalDirectories = Array.from(
|
|
454
|
-
new Set(
|
|
455
|
-
existingAdditionalDirectories
|
|
456
|
-
.map((directory) => path.resolve(directory))
|
|
457
|
-
.filter(
|
|
458
|
-
(directory) =>
|
|
459
|
-
!registeredCompanionPaths.has(directory) || directory === resolvedCompanionPath,
|
|
460
|
-
)
|
|
461
|
-
.concat(resolvedCompanionPath),
|
|
462
|
-
),
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
const settings: ClaudeSettings = {
|
|
466
|
-
...existing,
|
|
467
|
-
permissions: {
|
|
468
|
-
...permissions,
|
|
469
|
-
additionalDirectories,
|
|
470
|
-
},
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export async function cleanupWorkingRepoClaudeSettings(
|
|
477
|
-
workingRepoPath: string,
|
|
478
|
-
registeredCompanionPaths: string[],
|
|
479
|
-
): Promise<boolean> {
|
|
480
|
-
const settingsPath = path.join(workingRepoPath, ".claude", "settings.local.json");
|
|
481
|
-
let existing: ClaudeSettings;
|
|
482
|
-
try {
|
|
483
|
-
const parsed = JSON.parse(await fs.readFile(settingsPath, "utf8")) as unknown;
|
|
484
|
-
if (!parsed || typeof parsed !== "object") return stripTokensaveClaudeMdAppend(workingRepoPath);
|
|
485
|
-
existing = parsed as ClaudeSettings;
|
|
486
|
-
} catch (error) {
|
|
487
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
488
|
-
return stripTokensaveClaudeMdAppend(workingRepoPath);
|
|
489
|
-
}
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const settings = removeManagedHookGroups(existing);
|
|
494
|
-
const permissions = { ...settings.permissions };
|
|
495
|
-
const managedPaths = new Set(
|
|
496
|
-
registeredCompanionPaths.map((candidate) => path.resolve(candidate)),
|
|
497
|
-
);
|
|
498
|
-
const currentDirectories = Array.isArray(permissions.additionalDirectories)
|
|
499
|
-
? permissions.additionalDirectories
|
|
500
|
-
: [];
|
|
501
|
-
const additionalDirectories = currentDirectories.filter(
|
|
502
|
-
(candidate) => !managedPaths.has(path.resolve(candidate)),
|
|
503
|
-
);
|
|
504
|
-
if (additionalDirectories.length > 0) {
|
|
505
|
-
permissions.additionalDirectories = additionalDirectories;
|
|
506
|
-
} else {
|
|
507
|
-
delete permissions.additionalDirectories;
|
|
508
|
-
}
|
|
509
|
-
if (Object.keys(permissions).length > 0) {
|
|
510
|
-
settings.permissions = permissions;
|
|
511
|
-
} else {
|
|
512
|
-
delete settings.permissions;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const settingsChanged = JSON.stringify(settings) !== JSON.stringify(existing);
|
|
516
|
-
if (settingsChanged && Object.keys(settings).length === 0) {
|
|
517
|
-
await fs.unlink(settingsPath);
|
|
518
|
-
await pruneEmptyAncestors(path.join(workingRepoPath, ".claude"), workingRepoPath);
|
|
519
|
-
} else if (settingsChanged) {
|
|
520
|
-
await writeClaudeSettings(settingsPath, settings);
|
|
521
|
-
}
|
|
522
|
-
const guidanceChanged = await stripTokensaveClaudeMdAppend(workingRepoPath);
|
|
523
|
-
return settingsChanged || guidanceChanged;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
361
|
async function configureClaude(companionPath: string): Promise<void> {
|
|
527
362
|
// Mate hooks are delivered by the bundled Claude plugin at launch; nothing
|
|
528
363
|
// is copied into `companion/.claude/hooks/` anymore. Stale copies from
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
upsertManagedBlock,
|
|
19
19
|
} from "../context-services";
|
|
20
20
|
import type { CapabilityContributionInput, ProviderPlugin, SetupContext } from "../plugin";
|
|
21
|
+
import { surfaceRoot } from "../surface-target";
|
|
21
22
|
import { mergeDir, pruneEmptyAncestors } from "../utils";
|
|
22
23
|
import {
|
|
23
24
|
getOpenCodePluginReferences,
|
|
@@ -424,22 +425,23 @@ export async function reconcileOpenCodeContributions(
|
|
|
424
425
|
ctx: SetupContext,
|
|
425
426
|
inputs: CapabilityContributionInput[],
|
|
426
427
|
): Promise<void> {
|
|
427
|
-
|
|
428
|
+
/** Resolved once per pass; every write site below is relative to it. */
|
|
429
|
+
const root = surfaceRoot(ctx);
|
|
428
430
|
|
|
429
431
|
if (ctx.scope === "hub") {
|
|
430
|
-
await reconcileOpenCodeMcpContributions(
|
|
431
|
-
await reconcileOpenCodeAgentDefinitionContributions(
|
|
432
|
+
await reconcileOpenCodeMcpContributions(root, inputs);
|
|
433
|
+
await reconcileOpenCodeAgentDefinitionContributions(root, inputs);
|
|
432
434
|
return;
|
|
433
435
|
}
|
|
434
436
|
|
|
435
|
-
await reconcileOpenCodeMcpContributions(
|
|
436
|
-
await reconcileOpenCodeAgentDefinitionContributions(
|
|
437
|
+
await reconcileOpenCodeMcpContributions(root, inputs);
|
|
438
|
+
await reconcileOpenCodeAgentDefinitionContributions(root, inputs);
|
|
437
439
|
|
|
438
440
|
for (const input of inputs) {
|
|
439
441
|
for (const pluginReference of input.contributions.pluginReferences ?? []) {
|
|
440
442
|
const configFiles = pluginReference.configFiles ?? OPENCODE_CONTRIBUTION_CONFIG_FILES;
|
|
441
443
|
for (const name of configFiles) {
|
|
442
|
-
const configPath = path.join(
|
|
444
|
+
const configPath = path.join(root, ".opencode", name);
|
|
443
445
|
const { present, config } = await readOpenCodeConfig(configPath);
|
|
444
446
|
if (!input.enabled && !present) continue;
|
|
445
447
|
const preserved = getOpenCodePluginReferences(config).filter(
|
|
@@ -456,7 +458,7 @@ export async function reconcileOpenCodeContributions(
|
|
|
456
458
|
// Guidance sections are managed blocks in AGENTS.md. Capability disable
|
|
457
459
|
// strips them here; runtime teardown of the shared AGENTS.md is guarded in
|
|
458
460
|
// the provider teardown paths (kept while another active runtime uses it).
|
|
459
|
-
const guidancePath = path.join(
|
|
461
|
+
const guidancePath = path.join(root, "AGENTS.md");
|
|
460
462
|
const sections = input.enabled ? (input.contributions.guidanceSections ?? []) : [];
|
|
461
463
|
const keepKeys = new Set<string>();
|
|
462
464
|
for (const section of sections) {
|
|
@@ -469,12 +471,12 @@ export async function reconcileOpenCodeContributions(
|
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
for (const skillTree of input.contributions.skillTrees ?? []) {
|
|
472
|
-
const skillDir = path.join(
|
|
474
|
+
const skillDir = path.join(root, ".opencode", "skills", skillTree.name);
|
|
473
475
|
if (input.enabled) {
|
|
474
476
|
await mergeDir(skillTree.sourceDir, skillDir);
|
|
475
477
|
} else {
|
|
476
478
|
await fs.rm(skillDir, { recursive: true, force: true });
|
|
477
|
-
await pruneEmptyAncestors(path.join(
|
|
479
|
+
await pruneEmptyAncestors(path.join(root, ".opencode", "skills"), root);
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
}
|
|
@@ -482,13 +484,13 @@ export async function reconcileOpenCodeContributions(
|
|
|
482
484
|
|
|
483
485
|
/** Reconcile only MCP entries without touching OpenCode plugins or guidance. */
|
|
484
486
|
async function reconcileOpenCodeMcpContributions(
|
|
485
|
-
|
|
487
|
+
root: string,
|
|
486
488
|
inputs: CapabilityContributionInput[],
|
|
487
489
|
): Promise<void> {
|
|
488
490
|
for (const input of inputs) {
|
|
489
491
|
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
490
492
|
await updateOpenCodeMcpServer(
|
|
491
|
-
getCompanionOpenCodeConfigPath(
|
|
493
|
+
getCompanionOpenCodeConfigPath(root),
|
|
492
494
|
descriptor.name,
|
|
493
495
|
input.enabled ? toOpenCodeMcpEntry(descriptor) : null,
|
|
494
496
|
);
|
|
@@ -496,6 +498,31 @@ async function reconcileOpenCodeMcpContributions(
|
|
|
496
498
|
}
|
|
497
499
|
}
|
|
498
500
|
|
|
501
|
+
/** The Mate-managed `mcp` map of one pass, as a value. */
|
|
502
|
+
export function renderManagedOpenCodeMcpServers(
|
|
503
|
+
contributions: CapabilityContributionInput[] = [],
|
|
504
|
+
): Record<string, unknown> {
|
|
505
|
+
const servers: Record<string, unknown> = {};
|
|
506
|
+
for (const input of contributions) {
|
|
507
|
+
if (!input.enabled) continue;
|
|
508
|
+
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
509
|
+
servers[descriptor.name] = toOpenCodeMcpEntry(descriptor);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return servers;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The Mate-managed `permission.external_directory` map, as a value. Rendered
|
|
517
|
+
* once for both destinations a session can reach it through — the projected
|
|
518
|
+
* document and the launch environment — so the two cannot drift.
|
|
519
|
+
*/
|
|
520
|
+
export function renderCompanionExternalDirectoryPermissions(
|
|
521
|
+
companionPath: string,
|
|
522
|
+
): Record<string, string> {
|
|
523
|
+
return { [companionPath]: "allow", [`${companionPath}/**`]: "allow" };
|
|
524
|
+
}
|
|
525
|
+
|
|
499
526
|
/**
|
|
500
527
|
* Reconcile declared agent definition files under `.opencode/agents/`. Runs
|
|
501
528
|
* in both companion and hub scope — an agent definition is fully
|
|
@@ -503,10 +530,10 @@ async function reconcileOpenCodeMcpContributions(
|
|
|
503
530
|
* surface.
|
|
504
531
|
*/
|
|
505
532
|
async function reconcileOpenCodeAgentDefinitionContributions(
|
|
506
|
-
|
|
533
|
+
root: string,
|
|
507
534
|
inputs: CapabilityContributionInput[],
|
|
508
535
|
): Promise<void> {
|
|
509
|
-
const agentsDir = path.join(
|
|
536
|
+
const agentsDir = path.join(root, ".opencode", "agents");
|
|
510
537
|
for (const input of inputs) {
|
|
511
538
|
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
512
539
|
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
@@ -515,7 +542,7 @@ async function reconcileOpenCodeAgentDefinitionContributions(
|
|
|
515
542
|
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
516
543
|
} else {
|
|
517
544
|
await fs.rm(agentPath, { force: true });
|
|
518
|
-
await pruneEmptyAncestors(agentsDir,
|
|
545
|
+
await pruneEmptyAncestors(agentsDir, root);
|
|
519
546
|
}
|
|
520
547
|
}
|
|
521
548
|
}
|