@uniqbit/mate-core 0.15.1-canary.0 → 0.15.1-canary.2

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/companion/link.ts +2 -2
  3. package/src/cli/commands/config.ts +2 -2
  4. package/src/cli/commands/doctor.ts +3 -3
  5. package/src/cli/commands/install.ts +15 -6
  6. package/src/cli/commands/launch/shared.ts +6 -3
  7. package/src/cli/commands/setup.ts +3 -3
  8. package/src/cli/commands/update.ts +9 -9
  9. package/src/cli/main.ts +5 -2
  10. package/src/cli/plugin-commands.ts +3 -2
  11. package/src/cli/usage.ts +6 -3
  12. package/src/create-mate.ts +4 -3
  13. package/src/framework.ts +20 -0
  14. package/src/index.ts +1 -0
  15. package/src/lib/context-mode-package.ts +94 -0
  16. package/src/lib/install.ts +4 -9
  17. package/src/lib/opencode-plugin-package.ts +27 -30
  18. package/src/lib/orchestrator/adapters/base.ts +5 -4
  19. package/src/lib/orchestrator/adapters/claude.ts +20 -0
  20. package/src/lib/orchestrator/adapters/opencode.ts +4 -3
  21. package/src/lib/orchestrator/config-store.ts +2 -2
  22. package/src/lib/orchestrator/framework-context.ts +4 -4
  23. package/src/lib/orchestrator/global-config-store.ts +2 -3
  24. package/src/lib/orchestrator/launcher.ts +52 -0
  25. package/src/lib/orchestrator/repo-local-registry.ts +2 -3
  26. package/src/lib/orchestrator/setup-preflight.ts +5 -5
  27. package/src/lib/orchestrator/working-repo-store.ts +3 -3
  28. package/src/lib/update-checker.ts +8 -8
  29. package/src/playbooks/companion-guidance.ts +8 -9
  30. package/src/plugins.ts +1 -0
  31. package/src/runtime/env.ts +1 -0
  32. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +49 -8
  33. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/SKILL.md +8 -8
  34. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/references/openspec.md +3 -3
  35. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +25 -2
  36. package/src/tools/setup/capabilities/context-mode.ts +197 -0
  37. package/src/tools/setup/capabilities/graphify.ts +4 -4
  38. package/src/tools/setup/capabilities/openspec.ts +29 -1
  39. package/src/tools/setup/context-services.ts +6 -2
  40. package/src/tools/setup/engine.ts +2 -1
  41. package/src/tools/setup/plugin.ts +10 -2
  42. package/src/tools/setup/plugins/guidance.ts +3 -3
  43. package/src/tools/setup/providers/claude.ts +24 -7
  44. package/src/tools/setup/providers/opencode.ts +3 -2
  45. package/src/tools/setup.ts +13 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.1-canary.0",
3
+ "version": "0.15.1-canary.2",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
5
 
6
- import { frameworkConfig } from "../../../framework";
6
+ import { FRAMEWORK_NAME } from "../../../framework";
7
7
  import {
8
8
  selectCompanionLinkInputs,
9
9
  type CompanionLinkInputs,
@@ -48,7 +48,7 @@ interface CompanionLinkCommandDeps {
48
48
 
49
49
  /** All companions surfaced by the "existing companion" picker live under this directory. */
50
50
  function companionsHomeDir(): string {
51
- return path.join(os.homedir(), `.${frameworkConfig.name}`, "companions");
51
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "companions");
52
52
  }
53
53
 
54
54
  function isInsideDir(parentDir: string, candidatePath: string): boolean {
@@ -1,9 +1,9 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import { execSync } from "node:child_process";
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME } from "../../framework";
5
5
 
6
- const getGlobalConfigDir = () => path.join(os.homedir(), `.${frameworkConfig.name}`);
6
+ const getGlobalConfigDir = () => path.join(os.homedir(), `.${FRAMEWORK_NAME}`);
7
7
 
8
8
  /**
9
9
  * @command mate config
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
5
5
  import { getActiveDistribution } from "../../distribution";
6
6
  import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
7
7
  import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
@@ -34,7 +34,7 @@ interface ToolCheck {
34
34
  }
35
35
 
36
36
  function storesForCompanion(companionPath: string): CompanionStores {
37
- const configDir = path.join(companionPath, `.${frameworkConfig.name}`, "config");
37
+ const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
38
38
  return {
39
39
  configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
40
40
  workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
@@ -42,7 +42,7 @@ function storesForCompanion(companionPath: string): CompanionStores {
42
42
  }
43
43
 
44
44
  async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
45
- const localDir = path.join(cwd, `.${frameworkConfig.name}`);
45
+ const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
46
46
  const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
47
47
  await migrateConfigDir(localDir, localLegacyDirs);
48
48
 
@@ -1,4 +1,5 @@
1
1
  import { confirm } from "../confirm";
2
+ import { frameworkCommandName } from "../../framework";
2
3
  import {
3
4
  buildInstallPlan,
4
5
  inspectInstallPlan,
@@ -44,7 +45,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
44
45
  if (missing.length > 0 && !skipConfirm) {
45
46
  const ok = await confirm("Install the missing requirements? [y/N] ");
46
47
  if (!ok) {
47
- process.stderr.write("Installation declined. Run `mate install --yes` to continue.\n");
48
+ process.stderr.write(
49
+ `Installation declined. Run \`${frameworkCommandName()} install --yes\` to continue.\n`,
50
+ );
48
51
  process.exitCode = 1;
49
52
  return false;
50
53
  }
@@ -54,7 +57,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
54
57
  printPlanText(plan);
55
58
  if (!skipConfirm) {
56
59
  process.stderr.write(
57
- "mate: installation requires confirmation in a TTY. Re-run with `mate install --yes`.\n",
60
+ `${frameworkCommandName()}: installation requires confirmation in a TTY. Re-run with \`${frameworkCommandName()} install --yes\`.\n`,
58
61
  );
59
62
  process.exitCode = 1;
60
63
  return false;
@@ -73,9 +76,13 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
73
76
  }
74
77
  if (!execution.ok) {
75
78
  for (const result of execution.results.filter((item) => item.status === "failed")) {
76
- process.stderr.write(`mate: ${result.id} failed: ${result.error ?? "unknown error"}\n`);
79
+ process.stderr.write(
80
+ `${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
81
+ );
77
82
  }
78
- process.stderr.write("Installation is incomplete. Re-run `mate install` after remediation.\n");
83
+ process.stderr.write(
84
+ `Installation is incomplete. Re-run \`${frameworkCommandName()} install\` after remediation.\n`,
85
+ );
79
86
  process.exitCode = 1;
80
87
  return false;
81
88
  }
@@ -85,9 +92,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
85
92
  await saveCompleteInstallState(plan, execution.results);
86
93
  } catch (error) {
87
94
  process.stderr.write(
88
- `mate: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
95
+ `${frameworkCommandName()}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
96
+ );
97
+ process.stderr.write(
98
+ `Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
89
99
  );
90
- process.stderr.write("Installation is incomplete. Re-run `mate install`.\n");
91
100
  process.exitCode = 1;
92
101
  return false;
93
102
  }
@@ -1,3 +1,4 @@
1
+ import { frameworkCommandName } from "../../../framework";
1
2
  import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
2
3
  import {
3
4
  LaunchPreflightError,
@@ -42,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
42
43
  const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
43
44
 
44
45
  for (const arg of optionArgs) {
45
- process.stderr.write(`mate: unknown launch option: ${arg}\n`);
46
+ process.stderr.write(`${frameworkCommandName()}: unknown launch option: ${arg}\n`);
46
47
  process.exitCode = 1;
47
48
  return null;
48
49
  }
@@ -107,14 +108,16 @@ export async function runLaunchToolCommand(
107
108
  progress?.stop();
108
109
 
109
110
  if (error instanceof ToolNotAllowedError) {
110
- process.stderr.write(`mate: \`${tool}\` is disallowed by active repository policy.\n`);
111
+ process.stderr.write(
112
+ `${frameworkCommandName()}: \`${tool}\` is disallowed by active repository policy.\n`,
113
+ );
111
114
  process.exitCode = 1;
112
115
  return;
113
116
  }
114
117
 
115
118
  if (error instanceof RepositoryNotSelectedError) {
116
119
  process.stderr.write(`${error.message}\n`);
117
- process.stderr.write("Run `mate companion link` first.\n");
120
+ process.stderr.write(`Run \`${frameworkCommandName()} companion link\` first.\n`);
118
121
  process.exitCode = 1;
119
122
  return;
120
123
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { frameworkConfig } from "../../framework";
3
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../../framework";
4
4
  import { ConfigStore, defaultConfig } from "../../lib/orchestrator/config-store";
5
5
  import { executeSetup } from "../../tools/setup";
6
6
  import {
@@ -98,7 +98,7 @@ async function runSetupFlow(
98
98
  if (await checkWorkingRepo(cwd)) {
99
99
  process.stderr.write(
100
100
  `Warning: this directory looks like a working repository (project files detected).\n` +
101
- `Initializing it as a ${frameworkConfig.name} companion here may not be what you want.\n`,
101
+ `Initializing it as a ${frameworkCommandName()} companion here may not be what you want.\n`,
102
102
  );
103
103
  }
104
104
  const ok = await askConfirm("Initialize this directory as a Mate companion repository? [y/N] ");
@@ -156,7 +156,7 @@ async function runSetupFlow(
156
156
  ? getSetupSelectionsFromConfig(defaultConfig())
157
157
  : getSetupSelectionsFromConfig(
158
158
  await new ConfigStore(
159
- path.join(targetCwd, `.${frameworkConfig.name}`, "config", "framework.yaml"),
159
+ path.join(targetCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
160
160
  ).load(),
161
161
  );
162
162
 
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { spawnSync } from "node:child_process";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { frameworkCommandName } from "../../framework";
5
5
  import {
6
6
  OPENCODE_PLUGIN_PACKAGE_NAME,
7
7
  warmOpenCodePluginCache,
@@ -20,7 +20,7 @@ type InstallResult = ReturnType<typeof installPublicPackageSync>;
20
20
  function writeNpmOnlyRecoveryMessage(): void {
21
21
  const { packageName, registry } = getUpdateConfig();
22
22
  process.stderr.write(
23
- `${frameworkConfig.name}: self-update is only supported for npm-installed Mate.\n`,
23
+ `${frameworkCommandName()}: self-update is only supported for npm-installed Mate.\n`,
24
24
  );
25
25
  if (packageName.startsWith("@uniqbit/")) {
26
26
  process.stderr.write(
@@ -88,7 +88,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
88
88
  latest = await updateCommandDeps.fetchLatestVersion();
89
89
  } catch {
90
90
  process.stderr.write(
91
- `${frameworkConfig.name}: could not reach registry to check for updates\n`,
91
+ `${frameworkCommandName()}: could not reach registry to check for updates\n`,
92
92
  );
93
93
  process.exitCode = 1;
94
94
  return;
@@ -96,7 +96,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
96
96
 
97
97
  if (checkOnly) {
98
98
  if (updateCommandDeps.isNewer(latest, current)) {
99
- console.log(`${frameworkConfig.name}: update available (${current} → ${latest})`);
99
+ console.log(`${frameworkCommandName()}: update available (${current} → ${latest})`);
100
100
  process.exitCode = 1;
101
101
  } else {
102
102
  console.log("Up to date.");
@@ -127,11 +127,11 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
127
127
  } catch (error) {
128
128
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
129
129
  process.stderr.write(
130
- `${frameworkConfig.name}: npm is required for self-update but was not found on PATH\n`,
130
+ `${frameworkCommandName()}: npm is required for self-update but was not found on PATH\n`,
131
131
  );
132
132
  } else {
133
133
  process.stderr.write(
134
- `${frameworkConfig.name}: could not verify the npm installation used for self-update\n`,
134
+ `${frameworkCommandName()}: could not verify the npm installation used for self-update\n`,
135
135
  );
136
136
  }
137
137
  writeNpmOnlyRecoveryMessage();
@@ -148,7 +148,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
148
148
  const result = updateCommandDeps.installLatest(latest);
149
149
  if (result.status !== 0 || result.error) {
150
150
  process.stderr.write(
151
- `${frameworkConfig.name}: upgrade command exited with status ${result.status ?? 1}\n`,
151
+ `${frameworkCommandName()}: upgrade command exited with status ${result.status ?? 1}\n`,
152
152
  );
153
153
  process.exitCode = 1;
154
154
  return;
@@ -163,7 +163,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
163
163
  if (!warmed.ok) {
164
164
  process.stderr.write(
165
165
  [
166
- `${frameworkConfig.name}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
166
+ `${frameworkCommandName()}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
167
167
  "The next managed OpenCode launch will download it (requires registry access).",
168
168
  ...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
169
169
  ].join("\n") + "\n",
@@ -173,7 +173,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
173
173
  const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
174
174
  if (postInstall.status !== 0 || postInstall.error) {
175
175
  process.stderr.write(
176
- `\nUpgraded to ${latest}, but installation is incomplete. Run \`mate install\`.\n`,
176
+ `\nUpgraded to ${latest}, but installation is incomplete. Run \`${frameworkCommandName()} install\`.\n`,
177
177
  );
178
178
  process.exitCode = 1;
179
179
  return;
package/src/cli/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
+ import { frameworkCommandName } from "../framework";
2
3
  import {
3
4
  enforceUpdateIfRequired,
4
5
  scheduleBackgroundCheck,
@@ -81,8 +82,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
81
82
  if (!isInstallRecoveryCommand(command, subcommand)) {
82
83
  const preflight = await deps.inspectInstallPreflight();
83
84
  if (!preflight.ok) {
84
- console.error(`mate: ${preflight.reason ?? "installation is incomplete"}`);
85
- console.error("Run `mate install` before continuing.");
85
+ console.error(
86
+ `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
87
+ );
88
+ console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
86
89
  process.exitCode = 1;
87
90
  return;
88
91
  }
@@ -1,4 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
+ import { frameworkCommandName } from "../framework";
2
3
  import { resolveInstallContext } from "../lib/install";
3
4
  import type { FrameworkConfig } from "../lib/orchestrator/types";
4
5
  import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
@@ -85,7 +86,7 @@ export async function ensureCapabilityEnabled(
85
86
  const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
86
87
  if (plugin?.isEnabled(await loadConfig())) return true;
87
88
 
88
- const name = distribution.config.name;
89
+ const name = frameworkCommandName();
89
90
  console.error(
90
91
  `${name}: the "${pluginId}" capability is not enabled for this companion. ` +
91
92
  `Enable it via \`${name} companion setup\`.`,
@@ -95,7 +96,7 @@ export async function ensureCapabilityEnabled(
95
96
  }
96
97
 
97
98
  function usageFor(plugin: Plugin): string {
98
- const distributionName = getActiveDistribution().config.name;
99
+ const distributionName = frameworkCommandName();
99
100
  const namespace = namespaceOf(plugin);
100
101
  const lines = (plugin.cliCommands ?? []).map(
101
102
  (command) => ` ${distributionName} cap ${namespace} ${command.name} ${command.description}`,
package/src/cli/usage.ts CHANGED
@@ -1,9 +1,12 @@
1
- import { frameworkConfig } from "../framework";
1
+ import { getActiveDistribution } from "../distribution";
2
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
2
3
 
3
4
  export function usage(): string {
4
- const n = frameworkConfig.name;
5
+ const n = frameworkCommandName();
6
+ const packageName =
7
+ getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
5
8
  return [
6
- `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (@uniqbit/${n})`,
9
+ `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
7
10
  "",
8
11
  "Commands:",
9
12
  ` ${n} install [--yes]`,
@@ -1,5 +1,6 @@
1
1
  import { main } from "./cli/main";
2
2
  import { setActiveDistribution, type DistributionConfig } from "./distribution";
3
+ import { FRAMEWORK_NAME } from "./framework";
3
4
  import { createBunPlugin } from "./tools/setup/package-managers/bun";
4
5
  import { createUvPlugin } from "./tools/setup/package-managers/uv";
5
6
  import type { PluginRegistration } from "./tools/setup/plugin";
@@ -23,15 +24,15 @@ export interface MateCli {
23
24
  * Assemble a distribution from an identity config and plugin registration
24
25
  * entries. Distributions choose their plugin set; the framework always adds
25
26
  * bun and uv as required runtime substrate, plus gitignore management named
26
- * after `config.name` (built last since it reads the full plugin set).
27
- * Returns the runnable CLI.
27
+ * after the framework identity (built last since it reads the full plugin
28
+ * set). Returns the runnable CLI.
28
29
  */
29
30
  export function createMate({ config, plugins, main: runMain = main }: CreateMateOptions): MateCli {
30
31
  const registry = new PluginRegistry([
31
32
  ...plugins,
32
33
  { plugin: createBunPlugin(), policy: "required" },
33
34
  { plugin: createUvPlugin(), policy: "required" },
34
- createGitignorePlugin(config.name),
35
+ createGitignorePlugin(FRAMEWORK_NAME),
35
36
  ]);
36
37
  setActiveDistribution({ config, registry });
37
38
  return {
package/src/framework.ts CHANGED
@@ -1,5 +1,25 @@
1
1
  import { getActiveDistribution } from "./distribution";
2
2
 
3
+ /**
4
+ * Framework identity: names everything identity-shaped regardless of how the
5
+ * CLI is invoked — state directories (`~/.mate`, `.mate/`), managed-block
6
+ * markers, `MATE_*` env values, and the `companion-policy framework`
7
+ * attribute. A whitelabel distribution changes the invocation name only;
8
+ * identity stays `mate`. Use `frameworkCommandName()` for anything the user
9
+ * or an agent types.
10
+ */
11
+ export const FRAMEWORK_NAME = "mate";
12
+
13
+ /**
14
+ * Invocation name: how this distribution's CLI is invoked, derived from the
15
+ * package's `bin` key (`config.name`). Drives usage output, command hints,
16
+ * error prefixes, agent guidance, and permission entries — never paths,
17
+ * markers, or env values (use `FRAMEWORK_NAME` for those).
18
+ */
19
+ export function frameworkCommandName(): string {
20
+ return getActiveDistribution().config.name;
21
+ }
22
+
3
23
  /**
4
24
  * Live view of the active distribution's identity. Framework modules read
5
25
  * identity through this object; the values always come from the config the
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export { ensureCapabilityEnabled, type EnsureCapabilityEnabledDeps } from "./cli
24
24
  export type {
25
25
  CapabilityPlugin,
26
26
  InstructionsService,
27
+ LaunchPreflightContext,
27
28
  McpServerDescriptor,
28
29
  McpService,
29
30
  Plugin,
@@ -0,0 +1,94 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { PUBLIC_NPM_REGISTRY } from "./public-npm";
6
+
7
+ export const CONTEXT_MODE_PACKAGE_NAME = "context-mode";
8
+ export const CONTEXT_MODE_VERSION = "1.0.169";
9
+ export const CONTEXT_MODE_NODE_REQUIREMENT = ">=22.5.0";
10
+
11
+ export function getContextModePackageReference(): string {
12
+ return `${CONTEXT_MODE_PACKAGE_NAME}@${CONTEXT_MODE_VERSION}`;
13
+ }
14
+
15
+ export function isContextModePackageReference(value: unknown): value is string {
16
+ return (
17
+ typeof value === "string" &&
18
+ (value === CONTEXT_MODE_PACKAGE_NAME || value.startsWith(`${CONTEXT_MODE_PACKAGE_NAME}@`))
19
+ );
20
+ }
21
+
22
+ export function getContextModeInstallDir(companionPath: string): string {
23
+ return path.join(companionPath, ".mate", "dependencies", CONTEXT_MODE_PACKAGE_NAME);
24
+ }
25
+
26
+ export function getContextModePackageRoot(companionPath: string): string {
27
+ return path.join(
28
+ getContextModeInstallDir(companionPath),
29
+ "node_modules",
30
+ CONTEXT_MODE_PACKAGE_NAME,
31
+ );
32
+ }
33
+
34
+ export function isContextModeNodeVersionSupported(version: string): boolean {
35
+ const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number);
36
+ return major > 22 || (major === 22 && minor >= 5);
37
+ }
38
+
39
+ export function validateContextModeNodeRuntime(): void {
40
+ const result = spawnSync("node", ["--version"], { encoding: "utf8" });
41
+ const version = result.stdout?.trim() ?? "";
42
+ if (result.status !== 0 || !isContextModeNodeVersionSupported(version)) {
43
+ throw new Error(
44
+ `context-mode ${CONTEXT_MODE_VERSION} requires Node.js ${CONTEXT_MODE_NODE_REQUIREMENT}; found ${version || "no usable node executable"}.`,
45
+ );
46
+ }
47
+ }
48
+
49
+ export async function validateContextModePackage(companionPath: string): Promise<void> {
50
+ validateContextModeNodeRuntime();
51
+ const root = getContextModePackageRoot(companionPath);
52
+ const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8")) as {
53
+ version?: string;
54
+ };
55
+ if (manifest.version !== CONTEXT_MODE_VERSION) {
56
+ throw new Error(
57
+ `Expected ${getContextModePackageReference()} at ${root}; found ${manifest.version ?? "an unknown version"}. Re-run \`mate companion setup\`.`,
58
+ );
59
+ }
60
+ await Promise.all(
61
+ [
62
+ ".claude-plugin/plugin.json",
63
+ "hooks/hooks.json",
64
+ "skills/context-mode/SKILL.md",
65
+ "build/adapters/opencode/plugin.js",
66
+ ].map((asset) => fs.access(path.join(root, asset))),
67
+ );
68
+ }
69
+
70
+ export async function installContextModePackage(companionPath: string): Promise<void> {
71
+ validateContextModeNodeRuntime();
72
+ const installDir = getContextModeInstallDir(companionPath);
73
+ await fs.mkdir(installDir, { recursive: true });
74
+ await fs.writeFile(
75
+ path.join(installDir, "package.json"),
76
+ JSON.stringify(
77
+ { private: true, dependencies: { [CONTEXT_MODE_PACKAGE_NAME]: CONTEXT_MODE_VERSION } },
78
+ null,
79
+ 2,
80
+ ) + "\n",
81
+ "utf8",
82
+ );
83
+ const result = spawnSync(
84
+ "npm",
85
+ ["install", "--no-audit", "--no-fund", "--silent", "--registry", PUBLIC_NPM_REGISTRY],
86
+ { cwd: installDir, encoding: "utf8" },
87
+ );
88
+ if (result.error || result.status !== 0) {
89
+ throw new Error(
90
+ `Could not install ${getContextModePackageReference()}: ${result.error?.message ?? result.stderr?.trim() ?? `npm exited with ${result.status}`}`,
91
+ );
92
+ }
93
+ await validateContextModePackage(companionPath);
94
+ }
@@ -8,7 +8,7 @@ import { CompanionResolver } from "./orchestrator/companion-resolver";
8
8
  import { checkEngineRequirement } from "./orchestrator/engine-guard";
9
9
  import { GlobalConfigStore } from "./orchestrator/global-config-store";
10
10
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
11
- import { frameworkConfig } from "../framework";
11
+ import { FRAMEWORK_NAME } from "../framework";
12
12
  import type { FrameworkConfig } from "./orchestrator/types";
13
13
  import { getActiveDistribution } from "../distribution";
14
14
  import { createBunPlugin } from "../tools/setup/package-managers/bun";
@@ -55,7 +55,7 @@ export interface InstallState {
55
55
  }
56
56
 
57
57
  function defaultInstallStateRoot(): string {
58
- return path.join(os.homedir(), `.${frameworkConfig.name}`, "install-state");
58
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "install-state");
59
59
  }
60
60
 
61
61
  export function getInstallStatePath(
@@ -107,12 +107,7 @@ function coreContext(): InstallContext {
107
107
  }
108
108
 
109
109
  async function contextForCompanion(companionPath: string): Promise<InstallContext> {
110
- const configPath = path.join(
111
- companionPath,
112
- `.${frameworkConfig.name}`,
113
- "config",
114
- "framework.yaml",
115
- );
110
+ const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
116
111
  const config = mergeWithDefaults(await new ConfigStore(configPath).load());
117
112
  return {
118
113
  kind: "companion",
@@ -124,7 +119,7 @@ async function contextForCompanion(companionPath: string): Promise<InstallContex
124
119
 
125
120
  async function hasLocalConfig(cwd: string): Promise<boolean> {
126
121
  try {
127
- await fs.access(path.join(cwd, `.${frameworkConfig.name}`, "config", "framework.yaml"));
122
+ await fs.access(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
128
123
  return true;
129
124
  } catch {
130
125
  return false;
@@ -44,53 +44,50 @@ export const opencodePluginCacheDeps = {
44
44
  }),
45
45
  };
46
46
 
47
- /**
48
- * Pre-fetch the pinned plugin package into OpenCode's npm plugin environment
49
- * (`<cache>/packages/<spec>/`), mirroring OpenCode's own on-demand install
50
- * layout, so the first managed launch after a setup or update does not depend
51
- * on registry access. Best effort: failures are reported, never thrown.
52
- */
53
- export async function warmOpenCodePluginCache(
54
- version: string = getCurrentVersion(),
47
+ export async function warmOpenCodePackageCache(
48
+ packageName: string,
49
+ packageReference: string,
55
50
  env: NodeJS.ProcessEnv = process.env,
56
51
  registry = PUBLIC_NPM_REGISTRY,
57
52
  ): Promise<WarmPluginCacheResult> {
58
- // Escape hatch for tests and intentionally offline environments: skip the
59
- // pre-fetch entirely instead of attempting a registry install.
60
53
  if (env.MATE_DISABLE_OPENCODE_PLUGIN_PREFETCH === "1") {
61
54
  return { ok: true, detail: "pre-fetch disabled via MATE_DISABLE_OPENCODE_PLUGIN_PREFETCH" };
62
55
  }
63
-
64
- const reference = getOpenCodePluginPackageReference(version);
65
- const specDir = path.join(getOpenCodeCacheDir(env), "packages", reference);
66
-
56
+ const version = packageReference.slice(packageName.length + 1);
57
+ const specDir = path.join(getOpenCodeCacheDir(env), "packages", packageReference);
67
58
  try {
68
59
  await fs.mkdir(specDir, { recursive: true });
69
60
  await fs.writeFile(
70
61
  path.join(specDir, "package.json"),
71
- JSON.stringify({ dependencies: { [OPENCODE_PLUGIN_PACKAGE_NAME]: version } }, null, 2) + "\n",
62
+ JSON.stringify({ dependencies: { [packageName]: version } }, null, 2) + "\n",
72
63
  "utf8",
73
64
  );
74
-
75
65
  const result = opencodePluginCacheDeps.runInstall(specDir, registry);
76
- if (result.error) {
77
- return { ok: false, detail: result.error.message };
78
- }
66
+ if (result.error) return { ok: false, detail: result.error.message };
79
67
  if (result.status !== 0) {
80
- const output = `${result.stderr ?? ""}`.trim() || `npm install exited with ${result.status}`;
81
- return { ok: false, detail: output };
68
+ return {
69
+ ok: false,
70
+ detail: `${result.stderr ?? ""}`.trim() || `npm install exited with ${result.status}`,
71
+ };
82
72
  }
83
-
84
- await fs.access(
85
- path.join(
86
- specDir,
87
- "node_modules",
88
- ...OPENCODE_PLUGIN_PACKAGE_NAME.split("/"),
89
- "package.json",
90
- ),
91
- );
73
+ await fs.access(path.join(specDir, "node_modules", ...packageName.split("/"), "package.json"));
92
74
  return { ok: true };
93
75
  } catch (error) {
94
76
  return { ok: false, detail: (error as Error).message };
95
77
  }
96
78
  }
79
+
80
+ /**
81
+ * Pre-fetch the pinned plugin package into OpenCode's npm plugin environment
82
+ * (`<cache>/packages/<spec>/`), mirroring OpenCode's own on-demand install
83
+ * layout, so the first managed launch after a setup or update does not depend
84
+ * on registry access. Best effort: failures are reported, never thrown.
85
+ */
86
+ export async function warmOpenCodePluginCache(
87
+ version: string = getCurrentVersion(),
88
+ env: NodeJS.ProcessEnv = process.env,
89
+ registry = PUBLIC_NPM_REGISTRY,
90
+ ): Promise<WarmPluginCacheResult> {
91
+ const reference = getOpenCodePluginPackageReference(version);
92
+ return warmOpenCodePackageCache(OPENCODE_PLUGIN_PACKAGE_NAME, reference, env, registry);
93
+ }
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { version } from "../../../../package.json";
5
5
  import { isCommandOnPath } from "../../fs-utils";
6
- import { frameworkConfig } from "../../../framework";
6
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../../../framework";
7
7
  import { getReactDoctorBinPath, getWrapperBinPath } from "../../package-paths";
8
8
  import {
9
9
  GRAPHIFY_OUTPUT_SUBDIR,
@@ -77,7 +77,8 @@ export abstract class LaunchAdapter {
77
77
  const wrapperBinPath = getWrapperBinPath();
78
78
  const env: NodeJS.ProcessEnv = {
79
79
  ...process.env,
80
- MATE_NAME: frameworkConfig.name,
80
+ MATE_NAME: FRAMEWORK_NAME,
81
+ MATE_COMMAND: frameworkCommandName(),
81
82
  MATE_VERSION: version,
82
83
  MATE_ARTIFACT_PATH: context.companionPath,
83
84
  MATE_WRAPPER_BIN_PATH: wrapperBinPath,
@@ -148,7 +149,7 @@ export abstract class LaunchAdapter {
148
149
  command: this.toolName,
149
150
  args: builtArgs,
150
151
  env,
151
- warning: `${frameworkConfig.name}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
152
+ warning: `${frameworkCommandName()}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
152
153
  };
153
154
  }
154
155
 
@@ -182,7 +183,7 @@ export abstract class LaunchAdapter {
182
183
  command: this.toolName,
183
184
  args: builtArgs,
184
185
  env,
185
- warning: `${frameworkConfig.name}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
186
+ warning: `${frameworkCommandName()}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
186
187
  };
187
188
  }
188
189
  }