@uniqbit/mate-core 0.17.1 → 0.17.2-canary.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.17.1",
3
+ "version": "0.17.2-canary.1",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -11,7 +11,7 @@ export async function runPluginCommand(
11
11
  await runPluginInstallCommand(argv);
12
12
  return;
13
13
  case "verify":
14
- await runPluginVerifyCommand();
14
+ await runPluginVerifyCommand(argv);
15
15
  return;
16
16
  default:
17
17
  console.error(`Unknown plugin command: ${subcommand ?? ""}`);
@@ -1,19 +1,22 @@
1
1
  import { FRAMEWORK_NAME } from "../../../framework";
2
2
  import { resolveInstallContext } from "../../../lib/install";
3
- import { verifyDeclaredPlugins } from "../../../tools/setup/dynamic-plugins/verify";
3
+ import { inspectDeclaredPlugins } from "../../../tools/setup/dynamic-plugins/verify";
4
4
 
5
5
  /**
6
6
  * @command mate plugin verify
7
- * @description Strict, installation-free check that every plugin declared by the companion is allowed by `MATE_ALLOWED_PLUGINS`, installed, and loadable with the current environment. Exits non-zero naming each failing package.
7
+ * @description Strict, installation-free check that every plugin declared by the companion is allowed by `MATE_ALLOWED_PLUGINS`, installed, and loadable with the current environment. Exits non-zero naming each failing package. With `--json`, prints `{"capabilities": [...]}` listing the capability IDs the verified plugins provide.
8
8
  */
9
- export async function runPluginVerifyCommand(cwd = process.cwd()): Promise<boolean> {
9
+ export async function runPluginVerifyCommand(
10
+ argv: string[] = [],
11
+ cwd = process.cwd(),
12
+ ): Promise<boolean> {
10
13
  const context = await resolveInstallContext(cwd);
11
14
  if ((context.kind !== "companion" && context.kind !== "hub") || !context.companionPath) {
12
15
  process.stderr.write(`${FRAMEWORK_NAME}: \`plugin verify\` requires a companion context.\n`);
13
16
  process.exitCode = 1;
14
17
  return false;
15
18
  }
16
- const failures = await verifyDeclaredPlugins(context.companionPath);
19
+ const { failures, capabilities } = await inspectDeclaredPlugins(context.companionPath);
17
20
  for (const failure of failures) {
18
21
  process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
19
22
  }
@@ -21,5 +24,6 @@ export async function runPluginVerifyCommand(cwd = process.cwd()): Promise<boole
21
24
  process.exitCode = 1;
22
25
  return false;
23
26
  }
27
+ if (argv.includes("--json")) process.stdout.write(`${JSON.stringify({ capabilities })}\n`);
24
28
  return true;
25
29
  }
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
 
3
4
  import { FRAMEWORK_NAME } from "../../../framework";
@@ -26,11 +27,56 @@ export async function launchableAgents(
26
27
  return TERMINAL_AGENTS.filter((agent) => allowed.includes(agent) && installed(agent));
27
28
  }
28
29
 
30
+ /** Lowercase letters, digits and hyphens, so a name can never carry shell syntax or a flag. */
31
+ export const AGENT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
32
+
33
+ const AGENT_DEFINITION_DIRS: Record<TerminalAgent, string> = {
34
+ claude: path.join(".claude", "agents"),
35
+ opencode: path.join(".opencode", "agents"),
36
+ };
37
+
38
+ export interface DefaultAgentOutcome {
39
+ agentArgs?: string[];
40
+ notice?: string;
41
+ }
42
+
43
+ /**
44
+ * The companion's `studio.terminal.agent` for one provider. Never throws and
45
+ * never echoes the configured value into the notice.
46
+ */
47
+ export async function defaultAgentFor(
48
+ companionPath: string,
49
+ agent: TerminalAgent,
50
+ ): Promise<DefaultAgentOutcome> {
51
+ const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
52
+ if (!fs.existsSync(configPath)) return {};
53
+ let name: unknown;
54
+ try {
55
+ name = (await new ConfigStore(configPath).load()).studio?.terminal?.agent;
56
+ } catch {
57
+ return {};
58
+ }
59
+ if (name === undefined || name === null) return {};
60
+ if (typeof name !== "string" || !AGENT_NAME_PATTERN.test(name)) {
61
+ return {
62
+ notice: "Default agent not applied: studio.terminal.agent is not a valid agent name.",
63
+ };
64
+ }
65
+ const definition = path.join(companionPath, AGENT_DEFINITION_DIRS[agent], `${name}.md`);
66
+ if (!fs.existsSync(definition)) {
67
+ return {
68
+ notice: `Default agent "${name}" not applied: this companion has no ${agent} definition for it.`,
69
+ };
70
+ }
71
+ return { agentArgs: ["--agent", name] };
72
+ }
73
+
29
74
  export interface LaunchResolverOptions {
30
75
  collectInventory: () => Promise<StudioInventory>;
31
76
  /** Pinned by `serve --companion`; overrides the page selection. */
32
77
  launchCompanion?: string | null;
33
78
  launchableAgents?: (companionPath: string) => Promise<TerminalAgent[]>;
79
+ defaultAgent?: (companionPath: string, agent: TerminalAgent) => Promise<DefaultAgentOutcome>;
34
80
  }
35
81
 
36
82
  /** The companion a launch targets: the pinned one, else the page's, both read from the current inventory. */
@@ -50,6 +96,7 @@ export function createLaunchResolver(
50
96
  options: LaunchResolverOptions,
51
97
  ): (agent: string, digest: string | null) => Promise<TerminalLaunchResolution> {
52
98
  const agents = options.launchableAgents ?? launchableAgents;
99
+ const defaultAgent = options.defaultAgent ?? defaultAgentFor;
53
100
  return async (agent, digest) => {
54
101
  const companion = await effectiveLaunchCompanion(
55
102
  await options.collectInventory(),
@@ -66,6 +113,9 @@ export function createLaunchResolver(
66
113
  if (!(await agents(companion.path)).includes(agent as TerminalAgent)) {
67
114
  return { reason: `${agent} is not allowed by this companion or is not installed` };
68
115
  }
69
- return { companionPath: companion.path };
116
+ return {
117
+ companionPath: companion.path,
118
+ ...(await defaultAgent(companion.path, agent as TerminalAgent)),
119
+ };
70
120
  };
71
121
  }
@@ -177,7 +177,15 @@ export interface TerminalSessionInfo {
177
177
  startedAt: number;
178
178
  }
179
179
 
180
- export type TerminalLaunchResolution = { companionPath: string } | { reason: string };
180
+ export type TerminalLaunchResolution =
181
+ | {
182
+ companionPath: string;
183
+ /** Extra argv entries for the managed launch; set only by companion configuration. */
184
+ agentArgs?: string[];
185
+ /** Written to the terminal before the agent starts. */
186
+ notice?: string;
187
+ }
188
+ | { reason: string };
181
189
 
182
190
  export interface TerminalRegistryOptions {
183
191
  detachMs: number;
@@ -388,7 +396,7 @@ export class TerminalRegistry {
388
396
  void this.finish(detached, { type: "ended", reason: "evicted" });
389
397
  }
390
398
 
391
- const session = this.launch(agent, resolved.companionPath, size);
399
+ const session = this.launch(agent, resolved.companionPath, size, resolved);
392
400
  this.attachTo(connection, session, size);
393
401
  }
394
402
 
@@ -396,6 +404,7 @@ export class TerminalRegistry {
396
404
  agent: TerminalAgent,
397
405
  companionPath: string,
398
406
  size: { cols: number; rows: number },
407
+ extras: { agentArgs?: string[]; notice?: string } = {},
399
408
  ): Session {
400
409
  const env: Record<string, string> = {};
401
410
  for (const [key, value] of Object.entries(this.options.env ?? process.env)) {
@@ -413,6 +422,7 @@ export class TerminalRegistry {
413
422
  "--companion",
414
423
  "--yes",
415
424
  ...(this.options.noGit ? ["--no-git"] : []),
425
+ ...(extras.agentArgs ?? []),
416
426
  ];
417
427
 
418
428
  const session = {
@@ -427,6 +437,7 @@ export class TerminalRegistry {
427
437
  ending: null,
428
438
  exited: false,
429
439
  } as Omit<Session, "process"> as Session;
440
+ if (extras.notice) session.ring.push(new TextEncoder().encode(`${extras.notice}\r\n`));
430
441
  session.process = this.spawn({
431
442
  argv,
432
443
  cwd: companionPath,
@@ -120,6 +120,11 @@ export interface PluginDeclaration {
120
120
  config?: unknown;
121
121
  }
122
122
 
123
+ export interface StudioTerminalConfig {
124
+ /** Persona selected with `--agent` when Studio's terminal starts a provider. */
125
+ agent?: string;
126
+ }
127
+
123
128
  export interface FrameworkConfig {
124
129
  type?: FrameworkType;
125
130
  git?: GitModeProfile;
@@ -131,6 +136,7 @@ export interface FrameworkConfig {
131
136
  cliTools?: CliToolConfig[];
132
137
  packageManagers?: string[];
133
138
  engines?: EngineConstraints;
139
+ studio?: { terminal?: StudioTerminalConfig };
134
140
  }
135
141
 
136
142
  export interface CompanionRegistryConfig {
@@ -17,6 +17,12 @@ export interface PluginVerificationFailure {
17
17
  reason: string;
18
18
  }
19
19
 
20
+ export interface PluginInspection {
21
+ failures: PluginVerificationFailure[];
22
+ /** IDs of the capabilities the loaded, allowlisted plugins provide. */
23
+ capabilities: string[];
24
+ }
25
+
20
26
  /**
21
27
  * Strict, installation-free check of every declared plugin: allowed, installed
22
28
  * and loadable with the effective environment. Unlike hydration it fails
@@ -27,14 +33,22 @@ export async function verifyDeclaredPlugins(
27
33
  companionPath: string,
28
34
  deps: DynamicPluginLoadDeps = {},
29
35
  ): Promise<PluginVerificationFailure[]> {
36
+ return (await inspectDeclaredPlugins(companionPath, deps)).failures;
37
+ }
38
+
39
+ export async function inspectDeclaredPlugins(
40
+ companionPath: string,
41
+ deps: DynamicPluginLoadDeps = {},
42
+ ): Promise<PluginInspection> {
30
43
  const env = deps.env ?? process.env;
31
44
  const failures: PluginVerificationFailure[] = [];
45
+ const capabilities: string[] = [];
32
46
  let policy: ReturnType<typeof readPluginPolicy>;
33
47
  try {
34
48
  policy = readPluginPolicy(env);
35
49
  } catch (error) {
36
50
  if (!(error instanceof PluginPolicyError)) throw error;
37
- return [{ package: "(allowlist)", reason: error.message }];
51
+ return { failures: [{ package: "(allowlist)", reason: error.message }], capabilities };
38
52
  }
39
53
 
40
54
  const declarations: PluginDeclaration[] = [];
@@ -65,6 +79,7 @@ export async function verifyDeclaredPlugins(
65
79
  // oxlint-disable-next-line no-await-in-loop -- declared order is part of the contract
66
80
  const result = await loadDynamicPlugin(companionPath, declaration, { ...deps, env });
67
81
  if (!result.ok) failures.push({ package: name, reason: result.warning });
82
+ else if (result.plugin.kind === "capability") capabilities.push(result.plugin.id);
68
83
  }
69
- return failures;
84
+ return { failures, capabilities };
70
85
  }