@uniqbit/mate-core 0.15.4-canary.8 → 0.15.4
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/hooks/artifact-finish-nudge.mjs +5 -3
- package/claude-plugin/hooks/session-banner.mjs +5 -3
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +5 -3
- package/package.json +1 -1
- package/src/cli/commands/cap/openspec.ts +20 -1
- package/src/cli/commands/companion/hub.ts +1 -0
- package/src/cli/commands/plugin/install.ts +15 -3
- package/src/hooks/artifact-finish-nudge.ts +35 -3
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-git-sync.ts +67 -8
- package/src/lib/orchestrator/companion-hub.ts +28 -6
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/package-paths.ts +1 -0
- package/src/opencode/companion-hooks.ts +33 -12
- package/src/playbooks/companion-guidance.ts +1 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md +65 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +1 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3612 -0
- package/src/tools/setup/capabilities/context-mode.ts +57 -83
- package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
- package/src/tools/setup/capabilities/graphify.ts +86 -295
- package/src/tools/setup/capabilities/openspec.ts +34 -1
- package/src/tools/setup/capabilities/react-doctor.ts +37 -37
- package/src/tools/setup/capabilities/rtk.ts +4 -1
- package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
- package/src/tools/setup/capabilities/tokensave.ts +51 -72
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +15 -1
- package/src/tools/setup/engine.ts +68 -1
- package/src/tools/setup/mate.ts +10 -2
- package/src/tools/setup/plugin.ts +105 -0
- package/src/tools/setup/plugins/gitignore.ts +7 -4
- package/src/tools/setup/plugins/guidance.ts +1 -1
- package/src/tools/setup/providers/agent-file-sections.ts +78 -0
- package/src/tools/setup/providers/claude-format.ts +159 -0
- package/src/tools/setup/providers/claude.ts +281 -227
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +209 -60
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +7 -56
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// oxlint-disable no-await-in-loop -- config files are reconciled sequentially with shared ownership state
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
@@ -11,18 +10,21 @@ import {
|
|
|
11
10
|
validateContextModePackage,
|
|
12
11
|
} from "../../../lib/context-mode-package";
|
|
13
12
|
import { warmOpenCodePackageCache } from "../../../lib/opencode-plugin-package";
|
|
14
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
CapabilityPlugin,
|
|
15
|
+
LaunchPreflightContext,
|
|
16
|
+
RuntimeContributionsByRuntime,
|
|
17
|
+
SetupContext,
|
|
18
|
+
} from "../plugin";
|
|
19
|
+
import { readClaudeMcpConfig } from "../providers/claude-format";
|
|
20
|
+
import { getOpenCodePluginReferences, readOpenCodeConfig } from "../providers/opencode-format";
|
|
15
21
|
import { pruneEmptyAncestors } from "../utils";
|
|
16
22
|
|
|
17
|
-
type Config = Record<string, unknown>;
|
|
18
23
|
interface ContextModePluginDeps {
|
|
19
24
|
installPackage?: typeof installContextModePackage;
|
|
20
25
|
validatePackage?: typeof validateContextModePackage;
|
|
21
26
|
warmOpenCodePackage?: typeof warmOpenCodePackageCache;
|
|
22
27
|
}
|
|
23
|
-
interface OwnershipState {
|
|
24
|
-
opencodeConfigs: string[];
|
|
25
|
-
}
|
|
26
28
|
|
|
27
29
|
export const CONTEXT_MODE_OPENCODE_GUIDANCE = `## Context Mode
|
|
28
30
|
|
|
@@ -30,47 +32,19 @@ Use Context Mode tools for commands or tool calls whose output may be large, for
|
|
|
30
32
|
|
|
31
33
|
Canonical Mate policy and any more specific tool-routing instructions remain authoritative. In particular, follow explicit requirements to use TokenSave for codebase exploration, Context7 for library documentation, and specialized tools for their assigned operations; Context Mode complements rather than replaces them.`;
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
// Legacy ownership sidecar from before the managed-marker scheme; removed on
|
|
36
|
+
// sight (managed identity is `isContextModePackageReference` now).
|
|
37
|
+
function getLegacyOwnershipPath(companionPath: string): string {
|
|
34
38
|
return path.join(companionPath, ".mate", "state", "context-mode.json");
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return { opencodeConfigs: [] };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function writeOwnership(companionPath: string, state: OwnershipState): Promise<void> {
|
|
46
|
-
const statePath = getOwnershipPath(companionPath);
|
|
47
|
-
if (state.opencodeConfigs.length === 0) {
|
|
48
|
-
await fs.rm(statePath, { force: true });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
|
52
|
-
await fs.writeFile(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isRecord(value: unknown): value is Config {
|
|
56
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
// Legacy install location from before the relocate-distribution-deps-to-plugins-local
|
|
42
|
+
// change; self-heals on every apply the same way the ownership sidecar above does.
|
|
43
|
+
function getLegacyInstallDir(companionPath: string): string {
|
|
44
|
+
return path.join(companionPath, ".mate", "dependencies", CONTEXT_MODE_PACKAGE_NAME);
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const parsed = JSON.parse(await fs.readFile(filePath, "utf8")) as unknown;
|
|
62
|
-
return isRecord(parsed) ? parsed : {};
|
|
63
|
-
} catch {
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function hasContextModeMcp(config: Config): boolean {
|
|
69
|
-
const servers = isRecord(config.mcpServers)
|
|
70
|
-
? config.mcpServers
|
|
71
|
-
: isRecord(config.mcp)
|
|
72
|
-
? config.mcp
|
|
73
|
-
: {};
|
|
47
|
+
function hasContextModeMcp(servers: Record<string, unknown>): boolean {
|
|
74
48
|
return Object.entries(servers).some(
|
|
75
49
|
([name, value]) =>
|
|
76
50
|
name.toLowerCase().includes("context-mode") || JSON.stringify(value).includes("context-mode"),
|
|
@@ -78,58 +52,30 @@ function hasContextModeMcp(config: Config): boolean {
|
|
|
78
52
|
}
|
|
79
53
|
|
|
80
54
|
async function assertNoMcpConflict(ctx: SetupContext, provider: "claude" | "opencode") {
|
|
55
|
+
const servers =
|
|
56
|
+
provider === "claude"
|
|
57
|
+
? ((await readClaudeMcpConfig(path.join(ctx.companionPath, ".mcp.json"))).config.mcpServers ??
|
|
58
|
+
{})
|
|
59
|
+
: (((await readOpenCodeConfig(path.join(ctx.companionPath, ".opencode", "opencode.json")))
|
|
60
|
+
.config.mcp as Record<string, unknown>) ?? {});
|
|
81
61
|
const configPath =
|
|
82
62
|
provider === "claude"
|
|
83
63
|
? path.join(ctx.companionPath, ".mcp.json")
|
|
84
64
|
: path.join(ctx.companionPath, ".opencode", "opencode.json");
|
|
85
|
-
if (hasContextModeMcp(
|
|
65
|
+
if (hasContextModeMcp(servers)) {
|
|
86
66
|
throw new Error(
|
|
87
67
|
`Cannot enable context-mode for ${provider}: ${configPath} already contains a context-mode MCP registration. Remove the duplicate registration or deselect the context-mode capability; Mate did not modify it.`,
|
|
88
68
|
);
|
|
89
69
|
}
|
|
90
70
|
}
|
|
91
71
|
|
|
92
|
-
async function updateOpenCodeConfig(ctx: SetupContext, enabled: boolean): Promise<void> {
|
|
93
|
-
const reference = getContextModePackageReference();
|
|
94
|
-
const ownership = await readOwnership(ctx.companionPath);
|
|
95
|
-
for (const name of ["opencode.json", "tui.json"]) {
|
|
96
|
-
const configPath = path.join(ctx.companionPath, ".opencode", name);
|
|
97
|
-
const owned = ownership.opencodeConfigs.includes(name);
|
|
98
|
-
if (!enabled && !owned) continue;
|
|
99
|
-
const config = await readConfig(configPath);
|
|
100
|
-
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
101
|
-
const contextModeReferences = plugins.filter(isContextModePackageReference);
|
|
102
|
-
if (enabled && contextModeReferences.some((entry) => entry !== reference)) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Cannot enable ${reference}: ${configPath} contains user-owned context-mode reference ${contextModeReferences.join(", ")}. Mate did not modify it.`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
const preserved = plugins.filter((entry) => !(entry === reference && owned));
|
|
108
|
-
if (enabled) {
|
|
109
|
-
if (!contextModeReferences.includes(reference)) {
|
|
110
|
-
config.plugin = [...plugins, reference];
|
|
111
|
-
ownership.opencodeConfigs.push(name);
|
|
112
|
-
}
|
|
113
|
-
} else if (preserved.length > 0) {
|
|
114
|
-
config.plugin = preserved;
|
|
115
|
-
} else {
|
|
116
|
-
delete config.plugin;
|
|
117
|
-
}
|
|
118
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
119
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
120
|
-
}
|
|
121
|
-
if (!enabled) ownership.opencodeConfigs = [];
|
|
122
|
-
await writeOwnership(ctx.companionPath, ownership);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
72
|
async function validateOpenCodeReferences(ctx: LaunchPreflightContext): Promise<string[]> {
|
|
126
73
|
const expected = getContextModePackageReference();
|
|
127
74
|
const diagnostics: string[] = [];
|
|
128
75
|
for (const name of ["opencode.json", "tui.json"]) {
|
|
129
76
|
const configPath = path.join(ctx.companionPath, ".opencode", name);
|
|
130
|
-
const config = await
|
|
131
|
-
|
|
132
|
-
if (!plugins.includes(expected)) {
|
|
77
|
+
const { config } = await readOpenCodeConfig(configPath);
|
|
78
|
+
if (!getOpenCodePluginReferences(config).includes(expected)) {
|
|
133
79
|
diagnostics.push(
|
|
134
80
|
`Missing or stale context-mode package reference in ${configPath}; expected ${expected}.`,
|
|
135
81
|
);
|
|
@@ -150,7 +96,36 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
150
96
|
defaultSelected: false,
|
|
151
97
|
isEnabled: (config) =>
|
|
152
98
|
(config.capabilities ?? []).some((capability) => capability.name === "context-mode"),
|
|
99
|
+
// The pinned OpenCode plugin reference is declared and reconciled by the
|
|
100
|
+
// OpenCode Runtime Surface into both config files. Any context-mode
|
|
101
|
+
// reference counts as Mate-managed (marker scheme), so stale pins are
|
|
102
|
+
// replaced and teardown removes only context-mode entries.
|
|
103
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
104
|
+
return {
|
|
105
|
+
claude: {
|
|
106
|
+
// The context-mode Claude plugin exposes its skill and MCP tools
|
|
107
|
+
// under the plugin namespace; pre-seed both so routine routing
|
|
108
|
+
// doesn't prompt.
|
|
109
|
+
permissionEntries: [
|
|
110
|
+
"Skill(context-mode:context-mode)",
|
|
111
|
+
"mcp__plugin_context-mode_context-mode__*",
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
opencode: {
|
|
115
|
+
pluginReferences: [
|
|
116
|
+
{
|
|
117
|
+
reference: getContextModePackageReference(),
|
|
118
|
+
isManagedReference: isContextModePackageReference,
|
|
119
|
+
configFiles: ["opencode.json", "tui.json"],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
153
125
|
async apply(ctx) {
|
|
126
|
+
const legacyInstallDir = getLegacyInstallDir(ctx.companionPath);
|
|
127
|
+
await fs.rm(legacyInstallDir, { recursive: true, force: true });
|
|
128
|
+
await pruneEmptyAncestors(path.dirname(legacyInstallDir), ctx.companionPath);
|
|
154
129
|
if (ctx.mode === "setup") {
|
|
155
130
|
await installPackage(ctx.companionPath);
|
|
156
131
|
} else {
|
|
@@ -159,10 +134,12 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
159
134
|
await ctx.instructions?.append(CONTEXT_MODE_OPENCODE_GUIDANCE, {
|
|
160
135
|
providers: ["opencode"],
|
|
161
136
|
});
|
|
137
|
+
await fs.rm(getLegacyOwnershipPath(ctx.companionPath), { force: true });
|
|
162
138
|
},
|
|
163
139
|
async teardown(ctx) {
|
|
164
140
|
await fs.rm(getContextModeInstallDir(ctx.companionPath), { recursive: true, force: true });
|
|
165
141
|
await pruneEmptyAncestors(getContextModeInstallDir(ctx.companionPath), ctx.companionPath);
|
|
142
|
+
await fs.rm(getLegacyOwnershipPath(ctx.companionPath), { force: true });
|
|
166
143
|
},
|
|
167
144
|
forProvider: {
|
|
168
145
|
claude: {
|
|
@@ -175,7 +152,6 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
175
152
|
preflight: validateOpenCodeReferences,
|
|
176
153
|
async apply(ctx) {
|
|
177
154
|
await assertNoMcpConflict(ctx, "opencode");
|
|
178
|
-
await updateOpenCodeConfig(ctx, true);
|
|
179
155
|
if (ctx.mode === "setup") {
|
|
180
156
|
const result = await warmOpenCodePackage(
|
|
181
157
|
CONTEXT_MODE_PACKAGE_NAME,
|
|
@@ -188,9 +164,7 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
188
164
|
}
|
|
189
165
|
}
|
|
190
166
|
},
|
|
191
|
-
async teardown(
|
|
192
|
-
await updateOpenCodeConfig(ctx, false);
|
|
193
|
-
},
|
|
167
|
+
async teardown() {},
|
|
194
168
|
},
|
|
195
169
|
},
|
|
196
170
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
|
+
import {
|
|
3
|
+
removeHeadingSection,
|
|
4
|
+
type RemoveHeadingSectionOptions,
|
|
5
|
+
} from "../providers/agent-file-sections";
|
|
6
|
+
|
|
7
|
+
// Identity of the agent-file section `graphify install` writes. Declared in a
|
|
8
|
+
// leaf module (capability-owned) so both the graphify capability and the
|
|
9
|
+
// guidance plugin can consume it without an import cycle.
|
|
10
|
+
|
|
11
|
+
// Marker names derive from the framework identity, never the invocation name.
|
|
12
|
+
export const GRAPHIFY_START = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:START -->`;
|
|
13
|
+
export const GRAPHIFY_END = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:END -->`;
|
|
14
|
+
|
|
15
|
+
export function isGraphifyHeading(line: string): boolean {
|
|
16
|
+
return /^#{1,6}\s+graphify\s*$/i.test(line);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Section identity passed to the Runtime Surface escape hatch. */
|
|
20
|
+
export function graphifySectionOptions(): RemoveHeadingSectionOptions {
|
|
21
|
+
return { isHeading: isGraphifyHeading, markerLines: [GRAPHIFY_START(), GRAPHIFY_END()] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Removes the graphify section from content, collapsing leftover blank lines.
|
|
25
|
+
export function removeGraphifySection(content: string): string {
|
|
26
|
+
return removeHeadingSection(content, graphifySectionOptions());
|
|
27
|
+
}
|