@uniqbit/mate-core 0.15.3 → 0.15.4-canary.3

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.
Files changed (52) hide show
  1. package/package.json +3 -2
  2. package/src/cli/commands/companion/hub.ts +99 -0
  3. package/src/cli/commands/companion/tui.ts +3 -1
  4. package/src/cli/commands/doctor.ts +3 -6
  5. package/src/cli/commands/install.ts +10 -10
  6. package/src/cli/commands/launch/shared.ts +4 -4
  7. package/src/cli/commands/plugin/install.ts +94 -0
  8. package/src/cli/commands/plugin/plugin.ts +17 -0
  9. package/src/cli/commands/setup.ts +2 -2
  10. package/src/cli/commands/update.ts +9 -11
  11. package/src/cli/main.ts +25 -9
  12. package/src/cli/plugin-commands.ts +4 -4
  13. package/src/cli/usage.ts +7 -3
  14. package/src/distribution.ts +3 -5
  15. package/src/framework.ts +4 -36
  16. package/src/index.ts +4 -0
  17. package/src/lib/install.ts +1 -5
  18. package/src/lib/orchestrator/adapters/base.ts +3 -4
  19. package/src/lib/orchestrator/companion-hub.ts +374 -0
  20. package/src/lib/orchestrator/config-store.ts +72 -0
  21. package/src/lib/orchestrator/editor.ts +4 -20
  22. package/src/lib/orchestrator/framework-context.ts +119 -23
  23. package/src/lib/orchestrator/global-config-store.ts +1 -5
  24. package/src/lib/orchestrator/migration.ts +0 -23
  25. package/src/lib/orchestrator/setup-preflight.ts +4 -17
  26. package/src/lib/orchestrator/types.ts +26 -4
  27. package/src/lib/orchestrator/working-repo-store.ts +2 -2
  28. package/src/lib/update-checker.ts +5 -5
  29. package/src/playbooks/companion-guidance.ts +6 -6
  30. package/src/runtime/env.ts +0 -1
  31. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +79 -3
  32. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
  33. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
  34. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
  35. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
  36. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +5 -5
  37. package/src/tools/setup/capabilities/openspec.ts +1 -1
  38. package/src/tools/setup/capabilities/tokensave.ts +4 -4
  39. package/src/tools/setup/dynamic-plugins/host.ts +2 -1
  40. package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
  41. package/src/tools/setup/dynamic-plugins/install.ts +124 -109
  42. package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
  43. package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
  44. package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
  45. package/src/tools/setup/mate.ts +28 -31
  46. package/src/tools/setup/plugins/gitignore.ts +15 -8
  47. package/src/tools/setup/plugins/guidance.ts +1 -5
  48. package/src/tools/setup/providers/claude.ts +20 -8
  49. package/src/tools/setup/providers/opencode.ts +3 -4
  50. package/src/tools/setup.ts +2 -3
  51. package/src/tui.ts +5 -0
  52. package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
 
5
- import { FRAMEWORK_NAME, frameworkCommandName } from "../../../framework";
6
+ import { FRAMEWORK_NAME } from "../../../framework";
6
7
  import type { PluginDeclaration } from "../../../lib/orchestrator/types";
7
8
  import type { Plugin } from "../plugin";
8
9
  import { resolveEffectivePluginConfig } from "./config-resolution";
@@ -12,14 +13,9 @@ import {
12
13
  type CreatePlugin,
13
14
  type PluginHost,
14
15
  } from "./host";
15
- import { pluginPackageRoot } from "./paths";
16
- import type { PluginPin } from "./pin-store";
16
+ import { dynamicPluginsWorkspaceRoot, pluginPackageRoot } from "./paths";
17
17
 
18
18
  interface PluginManifest {
19
- version?: string;
20
- main?: string;
21
- module?: string;
22
- exports?: unknown;
23
19
  mate?: { pluginApiVersion?: unknown };
24
20
  }
25
21
 
@@ -35,38 +31,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
35
31
  return typeof value === "object" && value !== null && !Array.isArray(value);
36
32
  }
37
33
 
38
- function commandName(): string {
39
- try {
40
- return frameworkCommandName();
41
- } catch {
42
- return FRAMEWORK_NAME;
43
- }
44
- }
45
-
46
- function resolveEntryFromExports(exportsField: unknown): string | undefined {
47
- if (typeof exportsField === "string") return exportsField;
48
- if (!isRecord(exportsField)) return undefined;
49
- const dot = "." in exportsField ? exportsField["."] : exportsField;
50
- if (typeof dot === "string") return dot;
51
- if (!isRecord(dot)) return undefined;
52
- for (const condition of ["bun", "import", "default", "require"]) {
53
- const candidate = dot[condition];
54
- if (typeof candidate === "string") return candidate;
55
- if (isRecord(candidate) && typeof candidate.default === "string") return candidate.default;
56
- }
57
- return undefined;
58
- }
59
-
60
34
  /**
61
- * Loads one declared plugin from its installed tree: pin verification, API
62
- * version gate before import, dynamic import, factory resolution, effective
63
- * config, factory invocation. Every failure class returns a single warning
64
- * instead of throwing, so one broken plugin never takes down the CLI.
35
+ * Loads one declared plugin from the shared workspace: installed-presence
36
+ * check, API version gate before import, entry-point resolution via the
37
+ * shared workspace's own module resolver, dynamic import, factory
38
+ * resolution, effective config, factory invocation. Every failure class
39
+ * returns a single warning instead of throwing, so one broken plugin never
40
+ * takes down the CLI.
65
41
  */
66
42
  export async function loadDynamicPlugin(
67
43
  companionPath: string,
68
44
  declaration: PluginDeclaration,
69
- pins: PluginPin[],
70
45
  deps: DynamicPluginLoadDeps = {},
71
46
  ): Promise<DynamicPluginLoadResult> {
72
47
  const name = declaration.package;
@@ -80,27 +55,7 @@ export async function loadDynamicPlugin(
80
55
  } catch {
81
56
  return {
82
57
  ok: false,
83
- warning: `plugin "${name}" is not installed; run \`${commandName()} install\`.`,
84
- };
85
- }
86
-
87
- const pin = pins.find((candidate) => candidate.package === name);
88
- if (!pin) {
89
- return {
90
- ok: false,
91
- warning: `plugin "${name}" has no recorded pin; run \`${commandName()} install\`.`,
92
- };
93
- }
94
- if (pin.declaredVersion !== declaration.version) {
95
- return {
96
- ok: false,
97
- warning: `plugin "${name}" declares version "${declaration.version}" but was pinned from "${pin.declaredVersion}"; run \`${commandName()} install\`.`,
98
- };
99
- }
100
- if (manifest.version !== pin.resolvedVersion) {
101
- return {
102
- ok: false,
103
- warning: `plugin "${name}" has version ${manifest.version ?? "unknown"} installed but ${pin.resolvedVersion} pinned; run \`${commandName()} install\`.`,
58
+ warning: `plugin "${name}" is not installed; run \`${FRAMEWORK_NAME} install\`.`,
104
59
  };
105
60
  }
106
61
 
@@ -121,9 +76,19 @@ export async function loadDynamicPlugin(
121
76
  };
122
77
  }
123
78
 
124
- const entryRelative =
125
- resolveEntryFromExports(manifest.exports) ?? manifest.module ?? manifest.main ?? "index.js";
126
- const entryPath = path.resolve(packageRoot, entryRelative);
79
+ let entryPath: string;
80
+ try {
81
+ const workspaceRequire = createRequire(
82
+ pathToFileURL(path.join(dynamicPluginsWorkspaceRoot(companionPath), "package.json")),
83
+ );
84
+ entryPath = workspaceRequire.resolve(name);
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ warning: `plugin "${name}" could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
89
+ };
90
+ }
91
+
127
92
  const importModule =
128
93
  deps.importModule ??
129
94
  ((specifier: string) => import(specifier) as Promise<Record<string, unknown>>);
@@ -2,34 +2,20 @@ import path from "node:path";
2
2
 
3
3
  import { FRAMEWORK_NAME } from "../../../framework";
4
4
 
5
- /** Flattens an npm package name into a single directory segment. */
6
- export function sanitizePluginDirName(packageName: string): string {
7
- return packageName.replace(/^@/, "").replace(/\//g, "-");
5
+ /** Shared workspace root: one package.json + node_modules for every declared plugin. */
6
+ export function dynamicPluginsWorkspaceRoot(companionPath: string): string {
7
+ return path.join(companionPath, `.${FRAMEWORK_NAME}`, "plugins");
8
8
  }
9
9
 
10
- export function dynamicPluginsRoot(companionPath: string): string {
11
- return path.join(companionPath, `.${FRAMEWORK_NAME}`, "dependencies", "plugins");
12
- }
13
-
14
- /** Per-plugin install workspace holding a private package.json and node_modules. */
15
- export function pluginInstallDir(companionPath: string, packageName: string): string {
16
- return path.join(dynamicPluginsRoot(companionPath), sanitizePluginDirName(packageName));
17
- }
18
-
19
- /** Root of the installed plugin package itself. */
10
+ /** Root of one installed plugin package inside the shared workspace's node_modules. */
20
11
  export function pluginPackageRoot(companionPath: string, packageName: string): string {
21
12
  return path.join(
22
- pluginInstallDir(companionPath, packageName),
13
+ dynamicPluginsWorkspaceRoot(companionPath),
23
14
  "node_modules",
24
15
  ...packageName.split("/"),
25
16
  );
26
17
  }
27
18
 
28
- /** Committed pin file recording resolved plugin versions. */
29
- export function pluginPinFilePath(companionPath: string): string {
30
- return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "plugins.lock.yaml");
31
- }
32
-
33
19
  /** Gitignored per-machine override file deep-merged over committed plugin config. */
34
20
  export function pluginLocalOverridesPath(companionPath: string): string {
35
21
  return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "plugins.local.yaml");
@@ -37,3 +23,6 @@ export function pluginLocalOverridesPath(companionPath: string): string {
37
23
 
38
24
  /** Companion-relative gitignore entry for the local override file. */
39
25
  export const PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/config/plugins.local.yaml`;
26
+
27
+ /** Companion-relative gitignore entry for the shared workspace's local, never-committed registry credentials. */
28
+ export const PLUGIN_WORKSPACE_NPMRC_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/plugins/.npmrc`;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A generic, actionable hint printed on install failure — never guesses at
3
+ * the actual registry URL/host. Private registries stay a plain, standard
4
+ * npm operation: a one-time `npm config set --global`, or (since the shared
5
+ * plugin workspace is a real npm project) a project-local, gitignored
6
+ * `.npmrc` dropped directly in it — either is picked up by `npm install`
7
+ * with nothing Mate-injected.
8
+ */
9
+ export function registryConfigHint(packageName: string): string {
10
+ const scope = packageName.startsWith("@") ? packageName.split("/")[0] : undefined;
11
+ return scope
12
+ ? `If "${scope}" lives in a private registry, configure it once with:\n npm config set "${scope}:registry" "<registry-url>" --global\n npm config set "//<registry-host>/:_authToken" "<token>" --global`
13
+ : `If this package lives in a private registry, configure it once via \`npm config set\` (see the npm docs for scoped registries).`;
14
+ }
@@ -1,13 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkCommandName } from "../../framework";
5
4
  import { pruneEmptyAncestors } from "./utils";
6
5
 
7
6
  export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
8
7
  export const MATE_SKILLS = ["mate-artifact-finish", "mate-create-report"] as const;
9
8
 
10
- const MATE_COMMAND_PLACEHOLDER = "{{MATE_COMMAND}}";
11
9
  const MATE_SKILLS_SOURCE = path.join(
12
10
  import.meta.dirname,
13
11
  "../../templates/capabilities/openspec-cap/mate-skills",
@@ -16,9 +14,9 @@ const MATE_SKILLS_SOURCE = path.join(
16
14
  const MATE_CREATE_REPORT_SKILL = `---
17
15
  name: mate-create-report
18
16
  description: Create a browser-first Mate report from an explicit structured ReportDocument. Use when a skill needs to present supplied metrics, tables, statuses, metadata, or narrative text.
19
- allowed-tools: Bash({{MATE_COMMAND}}:*)
17
+ allowed-tools: Bash(mate:*)
20
18
  license: MIT
21
- compatibility: Requires the {{MATE_COMMAND}} CLI and the OpenSpec capability enabled.
19
+ compatibility: Requires the mate CLI and the OpenSpec capability enabled.
22
20
  metadata:
23
21
  author: mate
24
22
  version: "1.0"
@@ -57,22 +55,14 @@ Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`,
57
55
  ## Invocation
58
56
 
59
57
  1. Serialize the complete document to a temporary file or pipe it to stdin.
60
- 2. Run \`{{MATE_COMMAND}} report --input <file-or->\`.
58
+ 2. Run \`mate report --input <file-or->\`.
61
59
  3. Add \`--json\` when the caller needs normalized JSON instead of browser delivery.
62
60
 
63
61
  The default path writes self-contained HTML to a unique OS temporary directory and opens it in the default browser. The report includes a visible "Print / Save as PDF" control that calls the browser's native print dialog. If HTML delivery fails, the CLI warns on stderr and emits the complete report document as JSON on stdout.
64
62
 
65
- The built-in \`{{MATE_COMMAND}} report\` path collects Mate usage data and adapts it to the same contract and renderer.
63
+ The built-in \`mate report\` path collects Mate usage data and adapts it to the same contract and renderer.
66
64
  `;
67
65
 
68
- function currentMateCommand(): string {
69
- try {
70
- return frameworkCommandName();
71
- } catch {
72
- return "mate";
73
- }
74
- }
75
-
76
66
  export async function deployMateSkillDir(src: string, dest: string): Promise<void> {
77
67
  await fs.mkdir(dest, { recursive: true });
78
68
  const entries = await fs.readdir(src, { withFileTypes: true });
@@ -83,31 +73,38 @@ export async function deployMateSkillDir(src: string, dest: string): Promise<voi
83
73
  await deployMateSkillDir(srcPath, destPath);
84
74
  continue;
85
75
  }
86
- const content = await fs.readFile(srcPath, "utf8");
87
- if (content.includes(MATE_COMMAND_PLACEHOLDER)) {
88
- await fs.writeFile(
89
- destPath,
90
- content.replaceAll(MATE_COMMAND_PLACEHOLDER, currentMateCommand()),
91
- "utf8",
92
- );
93
- } else {
94
- await fs.copyFile(srcPath, destPath);
95
- }
76
+ await fs.copyFile(srcPath, destPath);
77
+ }
78
+ }
79
+
80
+ const DEFAULT_MATE_SKILLS_BUCKET = "agents";
81
+
82
+ /**
83
+ * A skill's source lives under a provider bucket so its folder name always
84
+ * matches its `name:` frontmatter: `<tool>/<skill>/` when that tool needs its
85
+ * own behavior (e.g. Claude Code always confirms before `mate artifact
86
+ * finish` pushes, since a push is a shared-state action), falling back to
87
+ * `agents/<skill>/` — the shared default every other tool (e.g. opencode,
88
+ * which pushes automatically) uses.
89
+ */
90
+ async function resolveMateSkillSource(skill: string, tool: string): Promise<string> {
91
+ const providerDir = path.join(MATE_SKILLS_SOURCE, tool, skill);
92
+ try {
93
+ await fs.access(providerDir);
94
+ return providerDir;
95
+ } catch {
96
+ return path.join(MATE_SKILLS_SOURCE, DEFAULT_MATE_SKILLS_BUCKET, skill);
96
97
  }
97
98
  }
98
99
 
99
- export async function applyMateSkills(skillsDir: string): Promise<void> {
100
+ export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
100
101
  for (const skill of MATE_SKILLS) {
101
102
  const destination = path.join(skillsDir, skill);
102
103
  if (skill === "mate-create-report") {
103
104
  await fs.mkdir(destination, { recursive: true });
104
- await fs.writeFile(
105
- path.join(destination, "SKILL.md"),
106
- MATE_CREATE_REPORT_SKILL.replaceAll(MATE_COMMAND_PLACEHOLDER, currentMateCommand()),
107
- "utf8",
108
- );
105
+ await fs.writeFile(path.join(destination, "SKILL.md"), MATE_CREATE_REPORT_SKILL, "utf8");
109
106
  } else {
110
- await deployMateSkillDir(path.join(MATE_SKILLS_SOURCE, skill), destination);
107
+ await deployMateSkillDir(await resolveMateSkillSource(skill, tool), destination);
111
108
  }
112
109
  }
113
110
  }
@@ -1,6 +1,9 @@
1
1
  import { getActiveDistribution } from "../../../distribution";
2
2
  import { FRAMEWORK_NAME } from "../../../framework";
3
- import { PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY } from "../dynamic-plugins/paths";
3
+ import {
4
+ PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY,
5
+ PLUGIN_WORKSPACE_NPMRC_GITIGNORE_ENTRY,
6
+ } from "../dynamic-plugins/paths";
4
7
  import type { Plugin, SetupContext } from "../plugin";
5
8
 
6
9
  const MANAGED_START = (name: string) => `# ${name} managed: start`;
@@ -58,22 +61,26 @@ export async function writeManagedGitignoreBlock(
58
61
 
59
62
  export function collectManagedGitignoreEntries(ctx: SetupContext, plugins: Plugin[]): string[] {
60
63
  // Baseline for every companion: node_modules never versions wherever a tool
61
- // materializes it, and everything under the dependencies tree (dynamic
62
- // plugins, context-mode, future consumers) is regenerated by setup/install —
64
+ // materializes it, and everything directly under the dependencies tree
65
+ // (context-mode, future consumers) is regenerated by setup/install —
63
66
  // version pins live in .mate/config and in code, never in the tree itself.
64
67
  const entries = [
65
68
  "node_modules/",
66
- `.${FRAMEWORK_NAME}/dependencies/`,
69
+ `.${FRAMEWORK_NAME}/dependencies/*`,
67
70
  ".mcp.json*",
68
71
  ...plugins
69
72
  .filter((p) => p.kind !== "root" && (p.isEnabled(ctx.config) || p.persistGitignoreEntries))
70
73
  .flatMap((p) => p.gitignoreEntries?.(ctx) ?? []),
71
74
  ];
72
- // Dynamic-plugin loading artifacts: the local override file never versions;
73
- // the pin file (plugins.lock.yaml) stays committed and is deliberately not
74
- // listed here.
75
+ // Dynamic-plugin loading artifacts: the local override file and the shared
76
+ // workspace's local registry credentials never version. The workspace's
77
+ // installed tree (node_modules) is already covered by the baseline
78
+ // node_modules/ rule above, since the workspace lives outside the
79
+ // dependencies tree wildcard; its package.json and committed
80
+ // package-lock.json are the pin/reproducibility record and are never
81
+ // touched by any ignore rule.
75
82
  if (ctx.config.plugins?.length) {
76
- entries.push(PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY);
83
+ entries.push(PLUGIN_WORKSPACE_NPMRC_GITIGNORE_ENTRY, PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY);
77
84
  }
78
85
  return entries;
79
86
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { FRAMEWORK_NAME, frameworkConfig } from "../../../framework";
4
+ import { FRAMEWORK_NAME } from "../../../framework";
5
5
  import { removeGraphifySection } from "../capabilities/graphify";
6
6
 
7
7
  // Marker names derive from the framework identity, never the invocation name.
@@ -12,9 +12,6 @@ function allMateStarts(): string[] {
12
12
  `<!-- ${upperName()}:COMPANION:START -->`,
13
13
  "<!-- COMPANION:GUIDANCE:START -->",
14
14
  `<!-- ${upperName()}:COMPANION:GUIDANCE:START -->`,
15
- ...frameworkConfig.legacyNames.map(
16
- (n) => `<!-- ${n.toUpperCase()}:COMPANION:GUIDANCE:START -->`,
17
- ),
18
15
  `<!-- ${upperName()}:GRAPHIFY:START -->`,
19
16
  ];
20
17
  }
@@ -24,7 +21,6 @@ function allMateEnds(): string[] {
24
21
  `<!-- ${upperName()}:COMPANION:END -->`,
25
22
  "<!-- COMPANION:GUIDANCE:END -->",
26
23
  `<!-- ${upperName()}:COMPANION:GUIDANCE:END -->`,
27
- ...frameworkConfig.legacyNames.map((n) => `<!-- ${n.toUpperCase()}:COMPANION:GUIDANCE:END -->`),
28
24
  `<!-- ${upperName()}:GRAPHIFY:END -->`,
29
25
  ];
30
26
  }
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { frameworkCommandName } from "../../../framework";
5
+ import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
7
7
  import { getWrapperBinPath } from "../../../lib/package-paths";
8
8
  import type { FrameworkConfig } from "../../../lib/orchestrator/types";
@@ -57,11 +57,7 @@ const MANAGED_HOOK_MARKERS = [
57
57
  // every file. Claude Code ignores `Glob()` rules for file-permission checks
58
58
  // (only Read/Edit rules gate file tools), so no Glob entry is emitted.
59
59
  function getBaseManagedPermissionEntries(companionPath: string): string[] {
60
- return [
61
- `Bash(${frameworkCommandName()}:*)`,
62
- `Read(${companionPath}/**)`,
63
- `Edit(${companionPath}/**)`,
64
- ];
60
+ return [`Bash(${FRAMEWORK_NAME}:*)`, `Read(${companionPath}/**)`, `Edit(${companionPath}/**)`];
65
61
  }
66
62
 
67
63
  const LEGACY_MANAGED_PERMISSION_ENTRIES = [
@@ -80,14 +76,14 @@ function getCapabilityPermissionEntries(): Record<string, string[]> {
80
76
  "Skill(openspec-archive-change)",
81
77
  "Skill(mate-artifact-finish)",
82
78
  "Bash(openspec:*)",
83
- `Bash(${frameworkCommandName()} cap graphify:*)`,
79
+ `Bash(${FRAMEWORK_NAME} cap graphify:*)`,
84
80
  `Bash(${path.join(wrapperBinPath, "openspec")}:*)`,
85
81
  ],
86
82
  rtk: ["Bash(rtk:*)"],
87
83
  graphify: [
88
84
  "Skill(graphify)",
89
85
  "Bash(graphify:*)",
90
- `Bash(${frameworkCommandName()} cap graphify:*)`,
86
+ `Bash(${FRAMEWORK_NAME} cap graphify:*)`,
91
87
  `Bash(${path.join(wrapperBinPath, "graphify")}:*)`,
92
88
  ],
93
89
  "react-doctor": [
@@ -296,6 +292,22 @@ function buildManagedClaudeSettings(
296
292
  },
297
293
  ...(hooks.PostToolUse ?? []),
298
294
  ];
295
+ // Catch-all: an archive that the PostToolUse nudge never matched (a human
296
+ // running the command outside Claude, an unmatched command shape) still
297
+ // gets caught here before the session ends, by checking the archive dir
298
+ // against finish tags directly rather than guessing from command text.
299
+ hooks.Stop = [
300
+ {
301
+ hooks: [
302
+ {
303
+ type: "command",
304
+ command: `sh "${companionPath}/.claude/hooks/mate-artifact-finish.sh"`,
305
+ timeout: 10,
306
+ },
307
+ ],
308
+ },
309
+ ...(hooks.Stop ?? []),
310
+ ];
299
311
  }
300
312
  if (reactDoctorEnabled) {
301
313
  // Record edits cheaply, then scan once when the edited turn finishes.
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { frameworkCommandName } from "../../../framework";
5
+ import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import {
7
7
  OPENCODE_PLUGIN_PACKAGE_NAME,
8
8
  getOpenCodePluginPackageReference,
@@ -18,7 +18,6 @@ import { getSetupProvidersRoot, getSetupRootTemplates } from "./utils";
18
18
  // package-owned now; these exact filenames are removed during sync and
19
19
  // teardown while every other companion-local plugin is preserved.
20
20
  const LEGACY_PLUGIN_FILES = [
21
- "kizuna-companion.ts",
22
21
  "add-dir.ts",
23
22
  "companion.ts",
24
23
  "small-model.ts",
@@ -244,9 +243,9 @@ async function warmPluginCacheForSetup(pluginReference: string): Promise<void> {
244
243
 
245
244
  process.stderr.write(
246
245
  [
247
- `${frameworkCommandName()}: could not pre-fetch ${pluginReference} into OpenCode's plugin environment.`,
246
+ `${FRAMEWORK_NAME}: could not pre-fetch ${pluginReference} into OpenCode's plugin environment.`,
248
247
  "The first managed OpenCode launch will download it, which requires registry access.",
249
- `Re-run \`${frameworkCommandName()} companion setup\` with network access to warm the cache ahead of time.`,
248
+ `Re-run \`${FRAMEWORK_NAME} companion setup\` with network access to warm the cache ahead of time.`,
250
249
  ...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
251
250
  ].join("\n") + "\n",
252
251
  );
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
5
+ import { FRAMEWORK_NAME } from "../framework";
6
6
  import { ConfigStore, mergeWithDefaults } from "../lib/orchestrator/config-store";
7
7
  import { GlobalConfigStore } from "../lib/orchestrator/global-config-store";
8
8
  import {
@@ -91,9 +91,8 @@ export async function syncCompanionFiles(
91
91
  );
92
92
  }
93
93
 
94
- // Exported for the distribution-identity tests.
95
94
  export function mateFolderReadme(): string {
96
- const n = frameworkCommandName();
95
+ const n = FRAMEWORK_NAME;
97
96
  const packageName =
98
97
  getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
99
98
  return [
package/src/tui.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { SelectMenu, type SelectMenuItem } from "./lib/components/select-menu";
2
+ export { WizardFooter } from "./lib/components/wizard-footer";
3
+ export { WizardHeader } from "./lib/components/wizard-header";
4
+ export { TextInputDisplay } from "./lib/components/text-input-display";
5
+ export { ConfirmPromptApp, confirmPrompt } from "./lib/components/confirm-prompt";
@@ -1,30 +0,0 @@
1
- import { YamlFileStore } from "../../../lib/orchestrator/yaml-file-store";
2
- import { pluginPinFilePath } from "./paths";
3
-
4
- /** Committed record of one declared plugin's resolved install. */
5
- export interface PluginPin {
6
- package: string;
7
- declaredVersion: string;
8
- resolvedVersion: string;
9
- integrity?: string;
10
- }
11
-
12
- export interface PluginPinFile {
13
- plugins: PluginPin[];
14
- }
15
-
16
- export class PluginPinStore extends YamlFileStore<PluginPinFile> {
17
- constructor(companionPath: string) {
18
- super(pluginPinFilePath(companionPath));
19
- }
20
-
21
- override async load(): Promise<PluginPinFile> {
22
- const parsed = await super.load();
23
- return { plugins: Array.isArray(parsed?.plugins) ? parsed.plugins : [] };
24
- }
25
-
26
- protected onMissing(): Promise<PluginPinFile> {
27
- // The pin file appears only once install records a resolved plugin.
28
- return Promise.resolve({ plugins: [] });
29
- }
30
- }