@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10
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 +4 -0
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
- package/claude-plugin/hooks/hooks.json +37 -0
- package/claude-plugin/hooks/session-banner.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
- package/package.json +4 -2
- package/src/cli/commands/artifact/finish/openspec.ts +1 -5
- package/src/cli/commands/companion/hub.ts +99 -0
- package/src/cli/commands/companion/link.ts +0 -5
- package/src/cli/commands/companion/tui.ts +3 -2
- package/src/cli/commands/doctor.ts +254 -170
- package/src/cli/commands/install.ts +18 -14
- package/src/cli/commands/launch/shared.ts +4 -4
- package/src/cli/commands/plugin/install.ts +94 -0
- package/src/cli/commands/plugin/plugin.ts +17 -0
- package/src/cli/commands/setup.ts +4 -6
- package/src/cli/commands/update.ts +9 -11
- package/src/cli/companion-link-wizard.tsx +22 -78
- package/src/cli/main.ts +57 -13
- package/src/cli/plugin-commands.ts +4 -4
- package/src/cli/repo-list-table.tsx +2 -7
- package/src/cli/usage.ts +7 -3
- package/src/distribution.ts +3 -5
- package/src/framework.ts +4 -36
- package/src/hooks/artifact-finish-nudge.ts +212 -0
- package/src/hooks/session-banner.ts +25 -0
- package/src/hooks/validate-artifact-path.ts +236 -0
- package/src/index.ts +4 -0
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/install.ts +31 -43
- package/src/lib/orchestrator/adapters/base.ts +7 -8
- package/src/lib/orchestrator/adapters/claude.ts +16 -0
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-hub.ts +380 -0
- package/src/lib/orchestrator/companion-store.ts +1 -35
- package/src/lib/orchestrator/config-store.ts +94 -7
- package/src/lib/orchestrator/editor.ts +4 -20
- package/src/lib/orchestrator/framework-context.ts +119 -24
- package/src/lib/orchestrator/global-config-store.ts +1 -5
- package/src/lib/orchestrator/launcher.ts +3 -9
- package/src/lib/orchestrator/migration.ts +0 -23
- package/src/lib/orchestrator/opencode-guidance.ts +1 -2
- package/src/lib/orchestrator/repo-local-registry.ts +3 -1
- package/src/lib/orchestrator/root-context.ts +117 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +5 -18
- package/src/lib/orchestrator/types.ts +27 -19
- package/src/lib/orchestrator/working-repo-store.ts +9 -3
- package/src/lib/package-paths.ts +37 -0
- package/src/lib/update-checker.ts +5 -5
- package/src/playbooks/companion-guidance.ts +7 -7
- package/src/runtime/env.ts +0 -4
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
- 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 +3541 -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 +27 -30
- 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 +50 -71
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/host.ts +2 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
- package/src/tools/setup/dynamic-plugins/install.ts +124 -109
- package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
- package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
- package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
- package/src/tools/setup/engine.ts +65 -1
- package/src/tools/setup/mate.ts +28 -31
- package/src/tools/setup/plugin.ts +81 -0
- package/src/tools/setup/plugins/gitignore.ts +19 -9
- package/src/tools/setup/plugins/guidance.ts +2 -6
- package/src/tools/setup/policy.ts +4 -7
- 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 +283 -292
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +171 -66
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +12 -7
- package/src/tui.ts +5 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
- package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
- package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
5
|
-
import {
|
|
4
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
5
|
+
import { pruneEmptyAncestors } from "../utils";
|
|
6
6
|
|
|
7
7
|
export const SKILL_SRC = path.join(
|
|
8
8
|
import.meta.dirname,
|
|
@@ -13,22 +13,6 @@ export const CLAUDE_HOOK_SRC = path.join(
|
|
|
13
13
|
"../../../templates/capabilities/react-doctor/claude/hooks/react-doctor.sh",
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
export async function applyReactDoctorSkills(skillsDir: string): Promise<void> {
|
|
17
|
-
await mergeDir(SKILL_SRC, skillsDir);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function teardownReactDoctorSkills(
|
|
21
|
-
skillsDir: string,
|
|
22
|
-
companionPath: string,
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
try {
|
|
25
|
-
await fs.rm(skillsDir, { recursive: true, force: true });
|
|
26
|
-
} catch {
|
|
27
|
-
/* not present */
|
|
28
|
-
}
|
|
29
|
-
await pruneEmptyAncestors(path.dirname(skillsDir), companionPath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
16
|
export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
33
17
|
return {
|
|
34
18
|
id: "react-doctor",
|
|
@@ -37,6 +21,41 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
37
21
|
description: "Install React Doctor skills and Claude post-edit scans.",
|
|
38
22
|
defaultSelected: true,
|
|
39
23
|
isEnabled: (config) => (config.capabilities ?? []).some((c) => c.name === "react-doctor"),
|
|
24
|
+
// Skills, session hooks, and permission pre-seeds are declared and
|
|
25
|
+
// reconciled by each runtime's Runtime Surface. Only the hook script file
|
|
26
|
+
// itself is copied imperatively below (script files are not a config
|
|
27
|
+
// format the surface owns).
|
|
28
|
+
getRuntimeContributions(ctx: SetupContext): RuntimeContributionsByRuntime {
|
|
29
|
+
const hookCommand = `sh "${ctx.companionPath}/.claude/hooks/react-doctor.sh"`;
|
|
30
|
+
const skillTrees = [{ name: "react-doctor", sourceDir: SKILL_SRC }];
|
|
31
|
+
return {
|
|
32
|
+
claude: {
|
|
33
|
+
hookGroups: [
|
|
34
|
+
// Record edits cheaply, then scan once when the edited turn finishes.
|
|
35
|
+
{
|
|
36
|
+
event: "PostToolUse",
|
|
37
|
+
marker: "react-doctor.sh",
|
|
38
|
+
group: {
|
|
39
|
+
matcher: "Write|Edit|MultiEdit|NotebookEdit|ApplyPatch",
|
|
40
|
+
hooks: [{ type: "command", command: hookCommand, timeout: 5 }],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
event: "Stop",
|
|
45
|
+
marker: "react-doctor.sh",
|
|
46
|
+
group: { hooks: [{ type: "command", command: hookCommand, timeout: 45 }] },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
permissionEntries: [
|
|
50
|
+
"Skill(react-doctor)",
|
|
51
|
+
"Bash(npx react-doctor:*)",
|
|
52
|
+
"Bash(npx react-doctor@latest *)",
|
|
53
|
+
],
|
|
54
|
+
skillTrees,
|
|
55
|
+
},
|
|
56
|
+
opencode: { skillTrees },
|
|
57
|
+
};
|
|
58
|
+
},
|
|
40
59
|
async apply(ctx: SetupContext) {
|
|
41
60
|
// Migrate: remove legacy shared .agents/skills/react-doctor/ from previous installs.
|
|
42
61
|
try {
|
|
@@ -57,18 +76,12 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
57
76
|
forProvider: {
|
|
58
77
|
claude: {
|
|
59
78
|
async apply(ctx: SetupContext) {
|
|
60
|
-
const skillsDir = path.join(ctx.companionPath, ".claude", "skills", "react-doctor");
|
|
61
|
-
await applyReactDoctorSkills(skillsDir);
|
|
62
79
|
const hookDest = path.join(ctx.companionPath, ".claude", "hooks", "react-doctor.sh");
|
|
63
80
|
await fs.mkdir(path.dirname(hookDest), { recursive: true });
|
|
64
81
|
await fs.copyFile(CLAUDE_HOOK_SRC, hookDest);
|
|
65
82
|
await fs.chmod(hookDest, 0o755);
|
|
66
83
|
},
|
|
67
84
|
async teardown(ctx: SetupContext) {
|
|
68
|
-
await teardownReactDoctorSkills(
|
|
69
|
-
path.join(ctx.companionPath, ".claude", "skills", "react-doctor"),
|
|
70
|
-
ctx.companionPath,
|
|
71
|
-
);
|
|
72
85
|
try {
|
|
73
86
|
await fs.unlink(path.join(ctx.companionPath, ".claude", "hooks", "react-doctor.sh"));
|
|
74
87
|
} catch {
|
|
@@ -80,19 +93,6 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
80
93
|
);
|
|
81
94
|
},
|
|
82
95
|
},
|
|
83
|
-
opencode: {
|
|
84
|
-
async apply(ctx: SetupContext) {
|
|
85
|
-
await applyReactDoctorSkills(
|
|
86
|
-
path.join(ctx.companionPath, ".opencode", "skills", "react-doctor"),
|
|
87
|
-
);
|
|
88
|
-
},
|
|
89
|
-
async teardown(ctx: SetupContext) {
|
|
90
|
-
await teardownReactDoctorSkills(
|
|
91
|
-
path.join(ctx.companionPath, ".opencode", "skills", "react-doctor"),
|
|
92
|
-
ctx.companionPath,
|
|
93
|
-
);
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
1
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
2
2
|
import type { InstallRequirement } from "../install-contract";
|
|
3
3
|
import { isCommandOnPath, runShellCommand, runShellCommandSilently } from "../utils";
|
|
4
4
|
import { confirm } from "../../../cli/confirm";
|
|
@@ -52,6 +52,9 @@ export function createRtkPlugin(deps: RtkDeps = {}): CapabilityPlugin {
|
|
|
52
52
|
defaultSelected: false,
|
|
53
53
|
isEnabled: (config) =>
|
|
54
54
|
(config.capabilities ?? []).some((capability) => capability.name === "rtk"),
|
|
55
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
56
|
+
return { claude: { permissionEntries: ["Bash(rtk:*)"] } };
|
|
57
|
+
},
|
|
55
58
|
getInstallRequirements: (): InstallRequirement[] => {
|
|
56
59
|
const command = checkBrew() ? RTK_INSTALL_CMD_BREW : RTK_INSTALL_CMD_FALLBACK;
|
|
57
60
|
return [
|
|
@@ -1 +1,7 @@
|
|
|
1
1
|
export const TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES = [".tokensave/"];
|
|
2
|
+
|
|
3
|
+
// Marker that identifies the tokensave installer's CLAUDE.md append block.
|
|
4
|
+
// Declared here (capability-owned) and consumed by the Claude Runtime Surface
|
|
5
|
+
// when reconciling working-repo files at launch.
|
|
6
|
+
export const TOKENSAVE_CLAUDE_MD_MARKER =
|
|
7
|
+
"## MANDATORY: No Explore Agents When Tokensave Is Available";
|
|
@@ -3,29 +3,20 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { resolveGitInfoExcludePath } from "../git-utils";
|
|
6
|
-
import type { CapabilityPlugin } from "../plugin";
|
|
6
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime } from "../plugin";
|
|
7
7
|
import { isCommandOnPath, resolveCommandOnPath, runCommand, runShellCommand } from "../utils";
|
|
8
|
+
import { TOKENSAVE_CLAUDE_MD_MARKER } from "./tokensave-shared";
|
|
8
9
|
export { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "./tokensave-shared";
|
|
9
10
|
|
|
10
11
|
export const TOKENSAVE_SUPPORTED_AGENTS = new Set(["claude", "opencode"]);
|
|
11
12
|
export const TOKENSAVE_STORE_DIR = ".tokensave";
|
|
12
13
|
export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
|
|
13
|
-
const TOKENSAVE_CLAUDE_MD_MARKER = "## MANDATORY: No Explore Agents When Tokensave Is Available";
|
|
14
14
|
const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
|
|
15
15
|
const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
|
|
16
16
|
const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
|
|
17
17
|
const TOKENSAVE_SCOOP_INSTALL_CMD =
|
|
18
18
|
"scoop bucket add tokensave https://github.com/aovestdipaperino/scoop-bucket && scoop install tokensave";
|
|
19
19
|
|
|
20
|
-
interface CompanionOpenCodeSettings {
|
|
21
|
-
mcp?: Record<string, unknown>;
|
|
22
|
-
[key: string]: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getCompanionOpenCodeConfigPath(companionPath: string): string {
|
|
26
|
-
return path.join(companionPath, ".opencode", "opencode.json");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
export interface TokensaveRunResult {
|
|
30
21
|
ok: boolean;
|
|
31
22
|
stderr: string;
|
|
@@ -34,10 +25,14 @@ export interface TokensaveRunResult {
|
|
|
34
25
|
|
|
35
26
|
export const tokensaveDeps = {
|
|
36
27
|
run(args: string[], cwd: string): TokensaveRunResult {
|
|
28
|
+
// env is passed explicitly: bun's spawnSync otherwise resolves the binary
|
|
29
|
+
// against the process's original PATH, ignoring in-process PATH changes
|
|
30
|
+
// (which test stubs and shell-integration wrappers rely on).
|
|
37
31
|
const result = spawnSync("tokensave", args, {
|
|
38
32
|
cwd,
|
|
39
33
|
encoding: "utf8",
|
|
40
34
|
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
env: process.env,
|
|
41
36
|
});
|
|
42
37
|
return {
|
|
43
38
|
ok: result.status === 0 && !result.error,
|
|
@@ -58,6 +53,7 @@ export const tokensaveDeps = {
|
|
|
58
53
|
const result = spawnSync("rustc", ["--version"], {
|
|
59
54
|
encoding: "utf8",
|
|
60
55
|
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
env: process.env,
|
|
61
57
|
});
|
|
62
58
|
return {
|
|
63
59
|
ok: result.status === 0 && !result.error,
|
|
@@ -73,41 +69,6 @@ export const tokensaveDeps = {
|
|
|
73
69
|
},
|
|
74
70
|
};
|
|
75
71
|
|
|
76
|
-
async function readCompanionOpenCodeSettings(
|
|
77
|
-
companionPath: string,
|
|
78
|
-
): Promise<CompanionOpenCodeSettings> {
|
|
79
|
-
try {
|
|
80
|
-
const parsed = JSON.parse(
|
|
81
|
-
await fs.readFile(getCompanionOpenCodeConfigPath(companionPath), "utf8"),
|
|
82
|
-
) as unknown;
|
|
83
|
-
if (parsed && typeof parsed === "object") {
|
|
84
|
-
return parsed as CompanionOpenCodeSettings;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
/* absent or malformed */
|
|
88
|
-
}
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function removeOpenCodeMcpServer(companionPath: string): Promise<void> {
|
|
93
|
-
const configPath = getCompanionOpenCodeConfigPath(companionPath);
|
|
94
|
-
const settings = await readCompanionOpenCodeSettings(companionPath);
|
|
95
|
-
if (!settings.mcp || typeof settings.mcp !== "object") {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const mcp = { ...settings.mcp };
|
|
100
|
-
delete mcp.tokensave;
|
|
101
|
-
if (Object.keys(mcp).length > 0) {
|
|
102
|
-
settings.mcp = mcp;
|
|
103
|
-
} else {
|
|
104
|
-
delete settings.mcp;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
108
|
-
await fs.writeFile(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
72
|
async function cleanupRepoLocalTokensaveArtifacts(repoPath: string): Promise<void> {
|
|
112
73
|
const claudeMdPath = path.join(repoPath, "CLAUDE.md");
|
|
113
74
|
try {
|
|
@@ -314,22 +275,55 @@ export function createTokensavePlugin(): CapabilityPlugin {
|
|
|
314
275
|
},
|
|
315
276
|
];
|
|
316
277
|
},
|
|
278
|
+
// MCP access, session hooks, and permission pre-seeds are declared below
|
|
279
|
+
// and reconciled by each runtime's Runtime Surface. The bare `tokensave`
|
|
280
|
+
// command name in the MCP entry is resolved against PATH at spawn time by
|
|
281
|
+
// each provider, so it never pins a since-moved/upgraded binary.
|
|
282
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
283
|
+
const mcpServers = [{ name: "tokensave", command: "tokensave", args: ["serve"] }];
|
|
284
|
+
// Hook commands pin the resolved binary path (hooks run without the
|
|
285
|
+
// launch environment's PATH guarantees).
|
|
286
|
+
const tokensaveCommandPath =
|
|
287
|
+
resolveCommandOnPath("tokensave", process.env.PATH ?? "") ?? "tokensave";
|
|
288
|
+
const hookGroups = [
|
|
289
|
+
{
|
|
290
|
+
event: "PreToolUse",
|
|
291
|
+
marker: "tokensave",
|
|
292
|
+
group: {
|
|
293
|
+
matcher: "Agent|Grep|Bash",
|
|
294
|
+
hooks: [
|
|
295
|
+
{ type: "command", command: tokensaveCommandPath, args: ["hook-pre-tool-use"] },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
event: "UserPromptSubmit",
|
|
301
|
+
marker: "tokensave",
|
|
302
|
+
group: {
|
|
303
|
+
hooks: [
|
|
304
|
+
{ type: "command", command: tokensaveCommandPath, args: ["hook-prompt-submit"] },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
event: "Stop",
|
|
310
|
+
marker: "tokensave",
|
|
311
|
+
group: {
|
|
312
|
+
hooks: [{ type: "command", command: tokensaveCommandPath, args: ["hook-stop"] }],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
return {
|
|
317
|
+
claude: { mcpServers, hookGroups, permissionEntries: ["mcp__tokensave__*"] },
|
|
318
|
+
opencode: { mcpServers },
|
|
319
|
+
};
|
|
320
|
+
},
|
|
317
321
|
async apply(ctx) {
|
|
318
322
|
// Presence only: make sure the tokensave binary is installed so the graph can be
|
|
319
323
|
// built later. The graph build itself (init/sync) lives in `mate cap index`, never
|
|
320
324
|
// in setup or launch-time sync — keeping launch fast and indexing an explicit step.
|
|
321
325
|
if (ctx.activeProviders.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p)).length === 0) return;
|
|
322
326
|
|
|
323
|
-
// MCP access is provider-mediated: every active hosting provider gets the
|
|
324
|
-
// server in its native config, and teardown bookkeeping removes it again.
|
|
325
|
-
const tokensaveCommandPath =
|
|
326
|
-
resolveCommandOnPath("tokensave", process.env.PATH ?? "") ?? "tokensave";
|
|
327
|
-
await ctx.mcp?.register({
|
|
328
|
-
name: "tokensave",
|
|
329
|
-
command: tokensaveCommandPath,
|
|
330
|
-
args: ["serve"],
|
|
331
|
-
});
|
|
332
|
-
|
|
333
327
|
const targetPath = ctx.repoPath ?? ctx.companionPath;
|
|
334
328
|
if (!(await ensureTokensaveInstalled(targetPath))) {
|
|
335
329
|
return;
|
|
@@ -342,20 +336,5 @@ export function createTokensavePlugin(): CapabilityPlugin {
|
|
|
342
336
|
if (!ctx.repoPath) return;
|
|
343
337
|
await teardownDriver(ctx.repoPath, ctx.activeProviders);
|
|
344
338
|
},
|
|
345
|
-
forProvider: {
|
|
346
|
-
claude: {
|
|
347
|
-
async apply(_ctx) {},
|
|
348
|
-
async teardown(_ctx) {},
|
|
349
|
-
},
|
|
350
|
-
opencode: {
|
|
351
|
-
// Registration goes through ctx.mcp now. Teardown stays as a legacy
|
|
352
|
-
// prune for entries written by releases that predate the bookkeeping
|
|
353
|
-
// manifest.
|
|
354
|
-
async apply(_ctx) {},
|
|
355
|
-
async teardown(ctx) {
|
|
356
|
-
await removeOpenCodeMcpServer(ctx.companionPath);
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
339
|
};
|
|
361
340
|
}
|
|
@@ -99,6 +99,35 @@ export async function upsertManagedBlock(
|
|
|
99
99
|
await fs.writeFile(filePath, existing + (existing ? separator : "") + block, "utf8");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Remove every managed block a plugin owns in `filePath`, except blocks whose
|
|
104
|
+
* key is in `keepKeys`. Used by contribution reconciliation: current sections
|
|
105
|
+
* are upserted (content-hashed keys), then stale keys are swept here.
|
|
106
|
+
*/
|
|
107
|
+
export async function removeManagedBlocksForPlugin(
|
|
108
|
+
filePath: string,
|
|
109
|
+
frameworkName: string,
|
|
110
|
+
pluginId: string,
|
|
111
|
+
keepKeys: Set<string> = new Set(),
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
let existing: string;
|
|
114
|
+
try {
|
|
115
|
+
existing = await fs.readFile(filePath, "utf8");
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120
|
+
const keyPattern = new RegExp(
|
|
121
|
+
`<!-- ${escape(frameworkName)}:managed:(${escape(pluginId)}:[0-9a-f]+) start -->`,
|
|
122
|
+
"g",
|
|
123
|
+
);
|
|
124
|
+
const keys = [...existing.matchAll(keyPattern)].map((match) => match[1]);
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
if (keepKeys.has(key)) continue;
|
|
127
|
+
await removeManagedBlock(filePath, frameworkName, key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
/** Remove a marker-delimited block if present. No-op when file or block is absent. */
|
|
103
132
|
export async function removeManagedBlock(
|
|
104
133
|
filePath: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../../../distribution";
|
|
2
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
3
|
import { ensureCapabilityEnabled } from "../../../cli/plugin-commands";
|
|
3
4
|
import type { CapabilityPlugin, Plugin } from "../plugin";
|
|
4
5
|
|
|
@@ -43,7 +44,7 @@ export function createPluginHost(): PluginHost {
|
|
|
43
44
|
return {
|
|
44
45
|
apiVersion: PLUGIN_API_VERSION,
|
|
45
46
|
distribution: {
|
|
46
|
-
name:
|
|
47
|
+
name: FRAMEWORK_NAME,
|
|
47
48
|
version: distribution.config.version,
|
|
48
49
|
},
|
|
49
50
|
ensureCapabilityEnabled: (name) => ensureCapabilityEnabled(name),
|
|
@@ -4,14 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
|
|
6
6
|
import { getActiveDistribution } from "../../../distribution";
|
|
7
|
-
import { FRAMEWORK_NAME
|
|
7
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
8
8
|
import { CompanionResolver } from "../../../lib/orchestrator/companion-resolver";
|
|
9
9
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
10
10
|
import { PLUGIN_DECLARATION_POLICIES } from "../../../lib/orchestrator/config-store";
|
|
11
11
|
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
12
12
|
import type { PluginRegistry } from "../registry";
|
|
13
13
|
import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./loader";
|
|
14
|
-
import { PluginPinStore } from "./pin-store";
|
|
15
14
|
|
|
16
15
|
// Packages already registered in this process; re-hydration (e.g. right after
|
|
17
16
|
// an install inside the same run) only picks up plugins that failed before.
|
|
@@ -29,14 +28,6 @@ export interface HydrateDynamicPluginsDeps extends DynamicPluginLoadDeps {
|
|
|
29
28
|
warn?: (message: string) => void;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
function commandName(): string {
|
|
33
|
-
try {
|
|
34
|
-
return frameworkCommandName();
|
|
35
|
-
} catch {
|
|
36
|
-
return FRAMEWORK_NAME;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
32
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
33
|
}
|
|
@@ -107,7 +98,7 @@ function validateDeclaration(entry: unknown): { declaration?: PluginDeclaration;
|
|
|
107
98
|
*/
|
|
108
99
|
export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}): Promise<void> {
|
|
109
100
|
const warn =
|
|
110
|
-
deps.warn ?? ((message: string) => process.stderr.write(`${
|
|
101
|
+
deps.warn ?? ((message: string) => process.stderr.write(`${FRAMEWORK_NAME}: ${message}\n`));
|
|
111
102
|
try {
|
|
112
103
|
const env = deps.env ?? process.env;
|
|
113
104
|
const companionPath =
|
|
@@ -117,7 +108,6 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
|
|
|
117
108
|
const entries = await readDeclarations(companionPath);
|
|
118
109
|
if (entries.length === 0) return;
|
|
119
110
|
|
|
120
|
-
const pins = (await new PluginPinStore(companionPath).load()).plugins;
|
|
121
111
|
const registry = deps.registry ?? getActiveDistribution().registry;
|
|
122
112
|
|
|
123
113
|
for (const entry of entries) {
|
|
@@ -129,7 +119,7 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
|
|
|
129
119
|
if (hydratedPackages.has(declaration.package)) continue;
|
|
130
120
|
|
|
131
121
|
// oxlint-disable-next-line no-await-in-loop -- declared order is part of the contract
|
|
132
|
-
const result = await loadDynamicPlugin(companionPath, declaration,
|
|
122
|
+
const result = await loadDynamicPlugin(companionPath, declaration, deps);
|
|
133
123
|
if (!result.ok) {
|
|
134
124
|
warn(result.warning);
|
|
135
125
|
continue;
|