agent-trellis 0.7.0 → 0.8.0

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 (104) hide show
  1. package/README.md +97 -38
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/cli.js +121 -21
  15. package/dist/commands/doctor.d.ts +7 -2
  16. package/dist/commands/doctor.js +38 -2
  17. package/dist/commands/init.d.ts +4 -2
  18. package/dist/commands/init.js +26 -13
  19. package/dist/commands/kimi.d.ts +16 -0
  20. package/dist/commands/kimi.js +64 -0
  21. package/dist/commands/manage.d.ts +39 -0
  22. package/dist/commands/manage.js +126 -0
  23. package/dist/commands/mcp.d.ts +22 -1
  24. package/dist/commands/mcp.js +65 -2
  25. package/dist/commands/mcpGateway.d.ts +6 -0
  26. package/dist/commands/mcpGateway.js +34 -14
  27. package/dist/commands/mcpImport.d.ts +27 -0
  28. package/dist/commands/mcpImport.js +336 -0
  29. package/dist/commands/memory.d.ts +5 -3
  30. package/dist/commands/memory.js +12 -8
  31. package/dist/commands/migrate.d.ts +16 -5
  32. package/dist/commands/migrate.js +90 -17
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +458 -71
  35. package/dist/commands/rollback.js +4 -1
  36. package/dist/commands/skill.d.ts +17 -0
  37. package/dist/commands/skill.js +47 -2
  38. package/dist/commands/sync.js +2 -0
  39. package/dist/core/canonical.d.ts +15 -7
  40. package/dist/core/canonical.js +101 -13
  41. package/dist/core/capabilitySelection.d.ts +45 -0
  42. package/dist/core/capabilitySelection.js +124 -0
  43. package/dist/core/types.d.ts +25 -2
  44. package/dist/core/types.js +7 -0
  45. package/dist/lib/backup.d.ts +2 -0
  46. package/dist/lib/backup.js +4 -0
  47. package/dist/lib/builtinSkills.d.ts +2 -0
  48. package/dist/lib/builtinSkills.js +4 -0
  49. package/dist/lib/cliMetadata.d.ts +1 -0
  50. package/dist/lib/cliMetadata.js +14 -0
  51. package/dist/lib/gatewayBackend.d.ts +28 -0
  52. package/dist/lib/gatewayBackend.js +72 -1
  53. package/dist/lib/installAgent.d.ts +7 -5
  54. package/dist/lib/installAgent.js +4 -3
  55. package/dist/lib/mcpConnect.js +5 -1
  56. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  57. package/dist/lib/mcpMigrateRead.js +3 -0
  58. package/dist/lib/mcpRuntime.d.ts +46 -0
  59. package/dist/lib/mcpRuntime.js +185 -0
  60. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  61. package/dist/lib/mcpStatusProvider.js +25 -0
  62. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  63. package/dist/lib/mcpToolRegistry.js +107 -14
  64. package/dist/lib/memoryGraph.d.ts +1 -1
  65. package/dist/lib/memoryGraph.js +9 -3
  66. package/dist/lib/memoryProvider.d.ts +58 -0
  67. package/dist/lib/memoryProvider.js +175 -0
  68. package/dist/lib/memoryStatus.d.ts +17 -0
  69. package/dist/lib/memoryStatus.js +88 -0
  70. package/dist/lib/nativeMemory.d.ts +19 -0
  71. package/dist/lib/nativeMemory.js +129 -0
  72. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  73. package/dist/lib/runtimeControlProvider.js +168 -0
  74. package/dist/lib/secretEnv.d.ts +2 -1
  75. package/dist/lib/secretEnv.js +6 -2
  76. package/dist/lib/skillProvider.d.ts +13 -0
  77. package/dist/lib/skillProvider.js +210 -0
  78. package/dist/lib/taskProvider.d.ts +7 -0
  79. package/dist/lib/taskProvider.js +43 -0
  80. package/dist/lib/taskStore.d.ts +46 -0
  81. package/dist/lib/taskStore.js +156 -0
  82. package/dist/lib/terminalPicker.d.ts +23 -49
  83. package/dist/lib/terminalPicker.js +53 -218
  84. package/dist/pi-bridge/bundle.js +475 -37
  85. package/dist/pi-bridge/index.js +42 -21
  86. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  87. package/dist/pi-bridge/schemaTranslate.js +4 -8
  88. package/dist/probes/kimi-code.d.ts +12 -0
  89. package/dist/probes/kimi-code.js +55 -0
  90. package/dist/sdk.d.ts +1 -1
  91. package/docs/architecture.md +224 -26
  92. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  93. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  94. package/docs/assets/trellis-mark.svg +12 -0
  95. package/docs/getting-started.md +328 -35
  96. package/docs/overview.md +131 -0
  97. package/docs/overview.zh-CN.md +100 -0
  98. package/docs/research.md +59 -0
  99. package/docs/roadmap.md +9 -5
  100. package/package.json +3 -2
  101. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  102. package/schema/capability-selection.example.yaml +23 -0
  103. package/schema/scope.example.yaml +1 -1
  104. package/schema/servers.example.yaml +15 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `trellis doctor` — read-only cross-agent scan. No `.trellis/` canonical
3
- * source required (docs/roadmap.md): this compares the four agents' own
3
+ * source required (docs/roadmap.md): this compares the five agents' own
4
4
  * current state against each other, the same manual process used
5
5
  * throughout docs/research.md's investigation, now formalized.
6
6
  */
@@ -9,7 +9,9 @@ import * as claudeCodeProbe from "../probes/claude-code.js";
9
9
  import * as codexProbe from "../probes/codex.js";
10
10
  import * as kiroProbe from "../probes/kiro.js";
11
11
  import * as piProbe from "../probes/pi.js";
12
+ import * as kimiCodeProbe from "../probes/kimi-code.js";
12
13
  import { loadCanonicalSource } from "../core/canonical.js";
14
+ import { GATEWAY_ENTRY_NAME, RUNTIME_ENTRY_NAME, resolveMcpPlan } from "../adapters/mcpPlan.js";
13
15
  import { ALL_AGENTS } from "../core/types.js";
14
16
  /**
15
17
  * Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
@@ -32,6 +34,7 @@ const EXIT_NONZERO_KINDS = new Set([
32
34
  "drift",
33
35
  "case-mismatch",
34
36
  "parse-diagnostic",
37
+ "runtime-drift",
35
38
  ]);
36
39
  /** Exported so tests can assert on this resolution directly, without
37
40
  * routing through real per-agent probes `runDoctor` also runs. */
@@ -72,6 +75,7 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
72
75
  { agent: "codex", run: () => codexProbe.probe(homeDir, { probeMcp }) },
73
76
  { agent: "kiro", run: () => kiroProbe.probe(homeDir, { probeMcp }) },
74
77
  { agent: "pi", run: () => piProbe.probe(homeDir) },
78
+ { agent: "kimi-code", run: () => kimiCodeProbe.probe(homeDir, { probeMcp }) },
75
79
  ];
76
80
  // One agent erroring must not blank the other three's results.
77
81
  const settled = await Promise.allSettled(probeList.map((p) => p.run()));
@@ -89,7 +93,15 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
89
93
  });
90
94
  }
91
95
  });
92
- findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...detectMcpUnreachable(snapshots));
96
+ let canonical;
97
+ try {
98
+ canonical = loadCanonicalSource(homeDir);
99
+ }
100
+ catch {
101
+ // Doctor remains useful as a standalone cross-agent scanner when
102
+ // canonical has not been initialized yet.
103
+ }
104
+ findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots));
93
105
  return { snapshots, findings };
94
106
  }
95
107
  /**
@@ -233,6 +245,30 @@ export function detectMcpUnreachable(snapshots) {
233
245
  }
234
246
  return findings;
235
247
  }
248
+ /** Compares runtime entries against the same canonical MCP plan that sync
249
+ * uses. Only missing Trellis-owned runtime edges are reported; extra
250
+ * user-authored MCP entries remain outside doctor ownership. */
251
+ export function detectRuntimeDrift(snapshots, canonical) {
252
+ const findings = [];
253
+ for (const snap of snapshots) {
254
+ if (!snap.present || !canonical.managedAgents.includes(snap.agent) || snap.agent === "pi")
255
+ continue;
256
+ const expected = resolveMcpPlan(snap.agent, canonical.mcp, canonical.managedAgents, canonical.secretsPolicy).desired
257
+ .map((entry) => entry.name)
258
+ .filter((name) => name === RUNTIME_ENTRY_NAME || name === GATEWAY_ENTRY_NAME);
259
+ const actual = new Set(snap.mcpServers.map((server) => server.name));
260
+ for (const name of expected) {
261
+ if (!actual.has(name)) {
262
+ findings.push({
263
+ kind: "runtime-drift",
264
+ agent: snap.agent,
265
+ message: "canonical expects MCP runtime entry \"" + name + "\" for " + snap.agent + ", but it is missing from the agent static configuration",
266
+ });
267
+ }
268
+ }
269
+ }
270
+ return findings;
271
+ }
236
272
  /** Exported so `trellis onboard` can reuse it verbatim for its own final
237
273
  * stage (trellis-onboard-closed-loop) rather than a second copy of this
238
274
  * formatting. */
@@ -29,7 +29,7 @@ export interface InitReport {
29
29
  /** Exported so `migrate` can byte-compare canonical `agents.md` against
30
30
  * this exact string (design.md D3, trellis-cli-migrate) rather than
31
31
  * retyping an equivalent one that could silently drift from this. */
32
- export declare const AGENTS_MD_TEMPLATE = "# Shared instructions\n\nWrite what every agent should know here \u2014 communication preferences,\nproject conventions, anything you'd otherwise repeat per agent.\n\nAlready using Claude Code, Codex, Kiro, or pi? `trellis migrate --from\n<agent>` imports its real instructions and skills instead of starting\nfrom this placeholder.\n";
32
+ export declare const AGENTS_MD_TEMPLATE = "# Shared instructions\n\nWrite what every agent should know here \u2014 communication preferences,\nproject conventions, anything you'd otherwise repeat per agent.\n\nAlready using Claude Code, Codex, Kiro, pi, or Kimi Code? `trellis migrate --from\n<agent>` imports its real instructions and skills instead of starting\nfrom this placeholder.\n";
33
33
  /**
34
34
  * Trellis never spawns an installer itself (global package installs are
35
35
  * exactly the kind of irreversible, system-wide action that needs the
@@ -38,7 +38,9 @@ export declare const AGENTS_MD_TEMPLATE = "# Shared instructions\n\nWrite what e
38
38
  * own current install docs, same discipline as everything else here.
39
39
  */
40
40
  export declare const INSTALL_HINTS: Record<AgentId, string>;
41
- export declare function collectInitReport(homeDir?: string): Promise<InitReport>;
41
+ export declare function collectInitReport(homeDir?: string, options?: {
42
+ write?: boolean;
43
+ }): Promise<InitReport>;
42
44
  export declare function runInit(opts?: RunInitOptions): Promise<{
43
45
  exitCode: number;
44
46
  }>;
@@ -15,6 +15,7 @@ import * as claudeCodeProbe from "../probes/claude-code.js";
15
15
  import * as codexProbe from "../probes/codex.js";
16
16
  import * as kiroProbe from "../probes/kiro.js";
17
17
  import * as piProbe from "../probes/pi.js";
18
+ import * as kimiCodeProbe from "../probes/kimi-code.js";
18
19
  import { ALL_AGENTS } from "../core/types.js";
19
20
  /** Exported so `migrate` can byte-compare canonical `agents.md` against
20
21
  * this exact string (design.md D3, trellis-cli-migrate) rather than
@@ -24,7 +25,7 @@ export const AGENTS_MD_TEMPLATE = `# Shared instructions
24
25
  Write what every agent should know here — communication preferences,
25
26
  project conventions, anything you'd otherwise repeat per agent.
26
27
 
27
- Already using Claude Code, Codex, Kiro, or pi? \`trellis migrate --from
28
+ Already using Claude Code, Codex, Kiro, pi, or Kimi Code? \`trellis migrate --from
28
29
  <agent>\` imports its real instructions and skills instead of starting
29
30
  from this placeholder.
30
31
  `;
@@ -93,32 +94,44 @@ export const INSTALL_HINTS = {
93
94
  codex: "npm install -g @openai/codex",
94
95
  kiro: "https://kiro.dev/downloads/",
95
96
  pi: "npm install -g @earendil-works/pi-coding-agent",
97
+ "kimi-code": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
96
98
  };
97
- function ensureFile(path, template) {
99
+ function ensureFile(path, template, write = true) {
98
100
  if (existsSync(path)) {
99
101
  return { path, action: "already-present" };
100
102
  }
101
- writeFileSync(path, template);
103
+ if (write)
104
+ writeFileSync(path, template);
102
105
  return { path, action: "create" };
103
106
  }
104
- export async function collectInitReport(homeDir = homedir()) {
107
+ export async function collectInitReport(homeDir = homedir(), options = {}) {
108
+ const write = options.write !== false;
105
109
  const root = join(homeDir, ".trellis");
106
- mkdirSync(root, { recursive: true });
107
- mkdirSync(join(root, "mcp"), { recursive: true });
108
- mkdirSync(join(root, "skills"), { recursive: true });
109
- mkdirSync(join(root, "agents"), { recursive: true });
110
- mkdirSync(join(root, "memories"), { recursive: true });
110
+ if (write) {
111
+ mkdirSync(root, { recursive: true });
112
+ mkdirSync(join(root, "mcp"), { recursive: true });
113
+ mkdirSync(join(root, "skills"), { recursive: true });
114
+ mkdirSync(join(root, "agents"), { recursive: true });
115
+ mkdirSync(join(root, "memories"), { recursive: true });
116
+ }
111
117
  const files = [
112
- ensureFile(join(root, "agents.md"), AGENTS_MD_TEMPLATE),
113
- ensureFile(join(root, "mcp", "servers.yaml"), serversYamlTemplate()),
114
- ensureFile(join(root, "secrets.policy.yaml"), secretsPolicyYamlTemplate()),
115
- ensureFile(join(root, "managed.yaml"), managedYamlTemplate()),
118
+ ensureFile(join(root, "agents.md"), AGENTS_MD_TEMPLATE, write),
119
+ ensureFile(join(root, "mcp", "servers.yaml"), serversYamlTemplate(), write),
120
+ ensureFile(join(root, "secrets.policy.yaml"), secretsPolicyYamlTemplate(), write),
121
+ ensureFile(join(root, "managed.yaml"), managedYamlTemplate(), write),
116
122
  ];
123
+ const here = dirname(fileURLToPath(import.meta.url));
124
+ const builtinSkillPath = join(root, "skills", "trellis-runtime", "SKILL.md");
125
+ const builtinSkillTemplate = readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
126
+ if (write)
127
+ mkdirSync(dirname(builtinSkillPath), { recursive: true });
128
+ files.push(ensureFile(builtinSkillPath, builtinSkillTemplate, write));
117
129
  const probes = [
118
130
  { agent: "claude-code", run: () => claudeCodeProbe.probe(homeDir) },
119
131
  { agent: "codex", run: () => codexProbe.probe(homeDir) },
120
132
  { agent: "kiro", run: () => kiroProbe.probe(homeDir) },
121
133
  { agent: "pi", run: () => piProbe.probe(homeDir) },
134
+ { agent: "kimi-code", run: () => kimiCodeProbe.probe(homeDir) },
122
135
  ];
123
136
  const settled = await Promise.allSettled(probes.map((p) => p.run()));
124
137
  const agents = settled.map((result, index) => {
@@ -0,0 +1,16 @@
1
+ /** Runtime-first Kimi Code launcher.
2
+ *
3
+ * Kimi automatically discovers ~/.agents/skills. When canonical delivery is
4
+ * `mcp`, passing one empty --skills-dir is the only documented way to replace
5
+ * both user/project Skill roots for this launch and prevent duplicate native
6
+ * Skill delivery. Native/both modes invoke Kimi normally.
7
+ */
8
+ import { spawn as spawnProcess } from "node:child_process";
9
+ export interface RunKimiOptions {
10
+ homeDir?: string;
11
+ env?: NodeJS.ProcessEnv;
12
+ spawn?: typeof spawnProcess;
13
+ }
14
+ export declare function runKimi(args?: readonly string[], opts?: RunKimiOptions): Promise<{
15
+ exitCode: number;
16
+ }>;
@@ -0,0 +1,64 @@
1
+ /** Runtime-first Kimi Code launcher.
2
+ *
3
+ * Kimi automatically discovers ~/.agents/skills. When canonical delivery is
4
+ * `mcp`, passing one empty --skills-dir is the only documented way to replace
5
+ * both user/project Skill roots for this launch and prevent duplicate native
6
+ * Skill delivery. Native/both modes invoke Kimi normally.
7
+ */
8
+ import { execFileSync, spawn as spawnProcess } from "node:child_process";
9
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { capabilityDeliveryForAgent } from "../core/types.js";
13
+ import { loadCanonicalSource } from "../core/canonical.js";
14
+ function resolveKimiCommand(homeDir, env) {
15
+ const explicit = env.TRELLIS_KIMI_BIN;
16
+ if (explicit && existsSync(explicit))
17
+ return explicit;
18
+ const homeBinary = join(env.KIMI_CODE_HOME ?? join(homeDir, ".kimi-code"), "bin", "kimi");
19
+ if (existsSync(homeBinary))
20
+ return homeBinary;
21
+ try {
22
+ return execFileSync("which", ["kimi"], { encoding: "utf8", env }).trim() || "kimi";
23
+ }
24
+ catch {
25
+ return "kimi";
26
+ }
27
+ }
28
+ function hasSkillsDirArg(args) {
29
+ return args.some((arg) => arg === "--skills-dir" || arg.startsWith("--skills-dir="));
30
+ }
31
+ export async function runKimi(args = [], opts = {}) {
32
+ const homeDir = opts.homeDir ?? homedir();
33
+ const env = { ...process.env, ...(opts.env ?? {}) };
34
+ let canonical;
35
+ try {
36
+ canonical = loadCanonicalSource(homeDir);
37
+ }
38
+ catch (err) {
39
+ console.error(err instanceof Error ? err.message : String(err));
40
+ return { exitCode: 1 };
41
+ }
42
+ const delivery = capabilityDeliveryForAgent("kimi-code", canonical.mcp);
43
+ const runtimeOnly = delivery === "mcp";
44
+ if (runtimeOnly && hasSkillsDirArg(args)) {
45
+ console.error("trellis kimi: --skills-dir cannot be combined with Runtime-only delivery; change mcp.runtime.delivery.kimi-code or use native Kimi directly");
46
+ return { exitCode: 1 };
47
+ }
48
+ let emptySkillsDir;
49
+ const launchArgs = [...args];
50
+ if (runtimeOnly) {
51
+ emptySkillsDir = mkdtempSync(join(tmpdir(), "trellis-kimi-empty-skills-"));
52
+ launchArgs.unshift("--skills-dir", emptySkillsDir);
53
+ }
54
+ const command = resolveKimiCommand(homeDir, env);
55
+ const spawn = opts.spawn ?? spawnProcess;
56
+ const child = spawn(command, launchArgs, { stdio: "inherit", env });
57
+ const exitCode = await new Promise((resolve) => {
58
+ child.once("error", () => resolve(1));
59
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
60
+ });
61
+ if (emptySkillsDir)
62
+ rmSync(emptySkillsDir, { recursive: true, force: true });
63
+ return { exitCode };
64
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Explicit lifecycle management for `~/.trellis/managed.yaml`.
3
+ *
4
+ * Onboarding stays additive: omission must never silently revoke Trellis's
5
+ * write authorization. Exact replacement and subtraction live here so they
6
+ * are deliberate, visible in shell history, dry-runnable, and recoverable.
7
+ */
8
+ import type { AgentId } from "../core/types.js";
9
+ export type ManageOperation = "list" | "set" | "add" | "remove";
10
+ export interface ManagePlan {
11
+ operation: ManageOperation;
12
+ current: AgentId[];
13
+ desired: AgentId[];
14
+ added: AgentId[];
15
+ removed: AgentId[];
16
+ changed: boolean;
17
+ }
18
+ export interface ParsedManageArgs {
19
+ operation: ManageOperation;
20
+ ids?: string;
21
+ json: boolean;
22
+ dryRun: boolean;
23
+ }
24
+ export type ManagePlanResult = {
25
+ plan: ManagePlan;
26
+ } | {
27
+ error: string;
28
+ };
29
+ export declare function parseManageArgs(argv: readonly string[]): ParsedManageArgs | {
30
+ error: string;
31
+ };
32
+ export declare function collectManagePlan(operation: ManageOperation, rawIds: string | undefined, homeDir?: string): ManagePlanResult;
33
+ export declare function applyManagePlan(plan: ManagePlan, homeDir?: string): void;
34
+ export declare function runManage(parsed: ParsedManageArgs, opts?: {
35
+ homeDir?: string;
36
+ }): {
37
+ exitCode: number;
38
+ plan?: ManagePlan;
39
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Explicit lifecycle management for `~/.trellis/managed.yaml`.
3
+ *
4
+ * Onboarding stays additive: omission must never silently revoke Trellis's
5
+ * write authorization. Exact replacement and subtraction live here so they
6
+ * are deliberate, visible in shell history, dry-runnable, and recoverable.
7
+ */
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { loadCanonicalSource } from "../core/canonical.js";
11
+ import { ALL_AGENTS } from "../core/types.js";
12
+ import { openBackupSession } from "../lib/backup.js";
13
+ function stableAgents(ids) {
14
+ const selected = new Set(ids);
15
+ return ALL_AGENTS.filter((id) => selected.has(id));
16
+ }
17
+ function parseAgentList(raw, allowNone) {
18
+ if (raw === undefined || raw.trim() === "") {
19
+ return { error: `agent list is required — use a comma-separated subset of ${ALL_AGENTS.join(", ")}${allowNone ? ", or none" : ""}` };
20
+ }
21
+ if (raw.trim().toLowerCase() === "none") {
22
+ return allowNone ? { agents: [] } : { error: '"none" is valid only for `trellis manage set none`' };
23
+ }
24
+ const tokens = raw.split(",").map((token) => token.trim()).filter(Boolean);
25
+ const invalid = tokens.filter((token) => !ALL_AGENTS.includes(token));
26
+ if (invalid.length > 0) {
27
+ return { error: `unknown agent id${invalid.length === 1 ? "" : "s"}: ${invalid.join(", ")} — expected ${ALL_AGENTS.join(", ")}` };
28
+ }
29
+ return { agents: stableAgents(tokens) };
30
+ }
31
+ export function parseManageArgs(argv) {
32
+ const [rawOperation, ...rest] = argv;
33
+ if (rawOperation !== "list" && rawOperation !== "set" && rawOperation !== "add" && rawOperation !== "remove") {
34
+ return { error: `unknown manage operation "${rawOperation ?? "(none)"}" — expected list, set, add, or remove` };
35
+ }
36
+ const unknownFlags = rest.filter((arg) => arg.startsWith("--") && arg !== "--json" && arg !== "--dry-run");
37
+ if (unknownFlags.length > 0)
38
+ return { error: `unknown manage flag "${unknownFlags[0]}"` };
39
+ const positional = rest.filter((arg) => !arg.startsWith("--"));
40
+ if (rawOperation === "list") {
41
+ if (positional.length > 0)
42
+ return { error: "manage list accepts no agent list" };
43
+ }
44
+ else if (positional.length !== 1) {
45
+ return { error: `manage ${rawOperation} requires exactly one comma-separated agent list` };
46
+ }
47
+ return {
48
+ operation: rawOperation,
49
+ ids: positional[0],
50
+ json: rest.includes("--json"),
51
+ dryRun: rest.includes("--dry-run"),
52
+ };
53
+ }
54
+ export function collectManagePlan(operation, rawIds, homeDir = homedir()) {
55
+ const current = stableAgents(loadCanonicalSource(homeDir).managedAgents);
56
+ if (operation === "list") {
57
+ return { plan: { operation, current, desired: current, added: [], removed: [], changed: false } };
58
+ }
59
+ const parsed = parseAgentList(rawIds, operation === "set");
60
+ if ("error" in parsed)
61
+ return parsed;
62
+ const requested = new Set(parsed.agents);
63
+ const desired = operation === "set"
64
+ ? stableAgents(requested)
65
+ : operation === "add"
66
+ ? stableAgents([...current, ...requested])
67
+ : stableAgents(current.filter((id) => !requested.has(id)));
68
+ const currentSet = new Set(current);
69
+ const desiredSet = new Set(desired);
70
+ const added = desired.filter((id) => !currentSet.has(id));
71
+ const removed = current.filter((id) => !desiredSet.has(id));
72
+ return {
73
+ plan: {
74
+ operation,
75
+ current,
76
+ desired,
77
+ added,
78
+ removed,
79
+ changed: added.length > 0 || removed.length > 0,
80
+ },
81
+ };
82
+ }
83
+ export function applyManagePlan(plan, homeDir = homedir()) {
84
+ if (!plan.changed || plan.operation === "list")
85
+ return;
86
+ const session = openBackupSession(homeDir, `manage-${plan.operation}`);
87
+ session.writeFile(join(homeDir, ".trellis", "managed.yaml"), `agents: [${plan.desired.join(", ")}]\n`);
88
+ session.finalize();
89
+ }
90
+ function displayAgents(agents) {
91
+ return agents.length > 0 ? agents.join(", ") : "(none)";
92
+ }
93
+ export function runManage(parsed, opts = {}) {
94
+ const homeDir = opts.homeDir ?? homedir();
95
+ let result;
96
+ try {
97
+ result = collectManagePlan(parsed.operation, parsed.ids, homeDir);
98
+ }
99
+ catch (err) {
100
+ console.error(err instanceof Error ? err.message : String(err));
101
+ return { exitCode: 1 };
102
+ }
103
+ if ("error" in result) {
104
+ console.error(result.error);
105
+ return { exitCode: 1 };
106
+ }
107
+ const { plan } = result;
108
+ if (!parsed.dryRun)
109
+ applyManagePlan(plan, homeDir);
110
+ if (parsed.json) {
111
+ console.log(JSON.stringify(plan, null, 2));
112
+ }
113
+ else if (parsed.operation === "list") {
114
+ console.log(`managed agents: ${displayAgents(plan.current)}`);
115
+ }
116
+ else {
117
+ console.log(`${parsed.dryRun ? "[dry run] " : ""}managed agents: ${displayAgents(plan.current)} -> ${displayAgents(plan.desired)}`);
118
+ if (!plan.changed)
119
+ console.log(" no change");
120
+ if (plan.added.length > 0)
121
+ console.log(` added: ${plan.added.join(", ")}`);
122
+ if (plan.removed.length > 0)
123
+ console.log(` detached: ${plan.removed.join(", ")} (native state preserved)`);
124
+ }
125
+ return { exitCode: 0, plan };
126
+ }
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { type ServersYamlWriteResult } from "../core/canonical.js";
14
14
  import type { AdapterPlanItem } from "../core/adapter.js";
15
- import type { AgentId, McpServerDef, Transport } from "../core/types.js";
15
+ import type { AgentId, McpAuthMode, McpConfig, McpServerDef, Transport } from "../core/types.js";
16
16
  import { type BackupSession } from "../lib/backup.js";
17
17
  export interface RunMcpSyncOptions {
18
18
  json?: boolean;
@@ -24,6 +24,9 @@ export interface RunMcpSyncOptions {
24
24
  /** Onboard-only seam — see RunSyncOptions.managedAgents. Never a CLI
25
25
  * flag. */
26
26
  managedAgents?: readonly AgentId[];
27
+ /** Onboard-only canonical MCP override used to preview a route selection
28
+ * before writing it to servers.yaml. */
29
+ mcp?: McpConfig;
27
30
  /** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
28
31
  * flag. */
29
32
  backupSession?: BackupSession;
@@ -47,6 +50,7 @@ export declare function printReport(report: McpSyncReport, dryRun: boolean): voi
47
50
  export interface McpListEntry {
48
51
  name: string;
49
52
  transport: Transport;
53
+ auth?: McpAuthMode;
50
54
  enabled: boolean;
51
55
  agents: readonly AgentId[];
52
56
  command?: string;
@@ -75,6 +79,7 @@ export declare function runMcpList(opts?: {
75
79
  * command's own inline flag parsing in src/cli.ts. */
76
80
  export interface McpAddRawArgs {
77
81
  transport?: string;
82
+ auth?: string;
78
83
  command?: string;
79
84
  /** Comma-separated. */
80
85
  args?: string;
@@ -107,6 +112,22 @@ export interface McpAddPlan {
107
112
  * always refuses, ever (design.md D4).
108
113
  */
109
114
  export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
115
+ export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
116
+ export interface McpSetAuthPlan {
117
+ name: string;
118
+ auth: "oauth" | "none";
119
+ action: McpSetAuthAction;
120
+ detail: string;
121
+ def?: McpServerDef;
122
+ }
123
+ export declare function collectMcpSetAuthPlan(name: string | undefined, auth: string | undefined, homeDir?: string): McpSetAuthPlan;
124
+ export declare function runMcpSetAuth(name: string | undefined, auth: string | undefined, opts?: {
125
+ homeDir?: string;
126
+ json?: boolean;
127
+ dryRun?: boolean;
128
+ }): {
129
+ exitCode: number;
130
+ };
110
131
  export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string): ServersYamlWriteResult;
111
132
  export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
112
133
  homeDir?: string;
@@ -18,12 +18,14 @@ import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
18
18
  import { CodexAdapter } from "../adapters/codex.js";
19
19
  import { KiroAdapter } from "../adapters/kiro.js";
20
20
  import { PiAdapter } from "../adapters/pi.js";
21
+ import { KimiCodeAdapter } from "../adapters/kimi-code.js";
21
22
  import { openBackupSession } from "../lib/backup.js";
22
23
  const ADAPTER_FACTORY = {
23
24
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
24
25
  codex: (homeDir) => new CodexAdapter(homeDir),
25
26
  kiro: (homeDir) => new KiroAdapter(homeDir),
26
27
  pi: (homeDir) => new PiAdapter(homeDir),
28
+ "kimi-code": (homeDir) => new KimiCodeAdapter(homeDir),
27
29
  };
28
30
  /** Only agents in `canonical.managedAgents` — see src/commands/sync.ts's
29
31
  * own copy of this same restriction (trellis-managed-agents). */
@@ -33,7 +35,11 @@ function buildAdapters(homeDir, managedAgents) {
33
35
  export async function collectMcpSyncReport(opts = {}) {
34
36
  const homeDir = opts.homeDir ?? homedir();
35
37
  const loaded = loadCanonicalSource(homeDir);
36
- const canonical = opts.managedAgents ? { ...loaded, managedAgents: opts.managedAgents } : loaded;
38
+ const canonical = {
39
+ ...loaded,
40
+ ...(opts.managedAgents ? { managedAgents: opts.managedAgents } : {}),
41
+ ...(opts.mcp ? { mcp: opts.mcp } : {}),
42
+ };
37
43
  const reports = [];
38
44
  const ownSession = !opts.dryRun && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
39
45
  const backup = opts.backupSession ?? ownSession;
@@ -107,6 +113,7 @@ export function collectMcpListPlan(homeDir = homedir()) {
107
113
  return Object.entries(canonical.mcp.servers).map(([name, def]) => ({
108
114
  name,
109
115
  transport: def.transport,
116
+ auth: def.auth,
110
117
  enabled: def.enabled ?? true,
111
118
  agents: resolveScope(def.agents, canonical.managedAgents),
112
119
  command: def.command,
@@ -136,7 +143,7 @@ export function runMcpList(opts = {}) {
136
143
  else {
137
144
  for (const e of entries) {
138
145
  const scope = e.agents.length > 0 ? e.agents.join(", ") : "(no managed agent reaches it)";
139
- console.log(`${e.name} (${e.transport})${e.enabled ? "" : " [disabled]"} — ${scope}`);
146
+ console.log(`${e.name} (${e.transport}${e.auth === "oauth" ? ", oauth" : ""})${e.enabled ? "" : " [disabled]"} — ${scope}`);
140
147
  if (e.env && e.env.length > 0)
141
148
  console.log(` env: ${e.env.join(", ")} (values never read/printed)`);
142
149
  if (e.staticEnv)
@@ -152,6 +159,7 @@ export function parseMcpAddArgs(argv) {
152
159
  };
153
160
  return {
154
161
  transport: flag("--transport"),
162
+ auth: flag("--auth"),
155
163
  command: flag("--command"),
156
164
  args: flag("--args"),
157
165
  url: flag("--url"),
@@ -202,6 +210,12 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
202
210
  if ((transport === "http" || transport === "sse") && !raw.url) {
203
211
  return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
204
212
  }
213
+ if (raw.auth !== undefined && raw.auth !== "oauth") {
214
+ return { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` };
215
+ }
216
+ if (raw.auth === "oauth" && transport === "stdio") {
217
+ return { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" };
218
+ }
205
219
  const agentsList = parseCsv(raw.agents);
206
220
  if (agentsList) {
207
221
  for (const id of agentsList) {
@@ -215,6 +229,7 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
215
229
  }
216
230
  const def = {
217
231
  transport,
232
+ ...(raw.auth === "oauth" ? { auth: "oauth" } : {}),
218
233
  ...(raw.command ? { command: raw.command } : {}),
219
234
  ...(parseCsv(raw.args) ? { args: parseCsv(raw.args) } : {}),
220
235
  ...(raw.url ? { url: raw.url } : {}),
@@ -234,6 +249,54 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
234
249
  }
235
250
  return { name, action: "create", detail: "will add to servers.yaml", def };
236
251
  }
252
+ export function collectMcpSetAuthPlan(name, auth, homeDir = homedir()) {
253
+ const mode = auth === "oauth" || auth === "none" ? auth : undefined;
254
+ if (!name)
255
+ return { name: "(none)", auth: (mode ?? "none"), action: "invalid-input", detail: "usage: trellis mcp set <name> --auth oauth|none" };
256
+ if (!mode)
257
+ return { name, auth: "none", action: "invalid-input", detail: `--auth must be "oauth" or "none" (got ${auth ?? "(missing)"})` };
258
+ const canonical = loadCanonicalSource(homeDir);
259
+ const current = canonical.mcp.servers[name];
260
+ if (!current)
261
+ return { name, auth: mode, action: "not-found", detail: `no canonical MCP server named "${name}"` };
262
+ if (mode === "oauth" && !current.url) {
263
+ return { name, auth: mode, action: "invalid-input", detail: "auth: oauth requires an http or sse MCP server" };
264
+ }
265
+ const next = mode === "oauth" ? { ...current, auth: "oauth" } : (() => {
266
+ const { auth: _auth, ...withoutAuth } = current;
267
+ return withoutAuth;
268
+ })();
269
+ if (JSON.stringify(current) === JSON.stringify(next))
270
+ return { name, auth: mode, action: "already-set", detail: `auth is already ${mode}`, def: next };
271
+ return { name, auth: mode, action: "updated", detail: `set auth to ${mode}`, def: next };
272
+ }
273
+ export function runMcpSetAuth(name, auth, opts = {}) {
274
+ const homeDir = opts.homeDir ?? homedir();
275
+ let plan;
276
+ try {
277
+ plan = collectMcpSetAuthPlan(name, auth, homeDir);
278
+ }
279
+ catch (err) {
280
+ console.error(err instanceof Error ? err.message : String(err));
281
+ return { exitCode: 1 };
282
+ }
283
+ let writeError;
284
+ if (!opts.dryRun && plan.action === "updated" && plan.def) {
285
+ const result = upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def);
286
+ if (!result.ok)
287
+ writeError = result.error;
288
+ }
289
+ if (opts.json) {
290
+ console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
291
+ }
292
+ else {
293
+ console.log(`${opts.dryRun ? "[dry run] " : ""}mcp set ${plan.name}`);
294
+ console.log(` [${plan.action}] ${plan.detail}`);
295
+ if (writeError)
296
+ console.error(` write failed: ${writeError}`);
297
+ }
298
+ return { exitCode: ["invalid-input", "not-found"].includes(plan.action) || Boolean(writeError) ? 1 : 0 };
299
+ }
237
300
  export function applyMcpAddPlan(plan, homeDir = homedir()) {
238
301
  if (plan.action !== "create" || !plan.def)
239
302
  return { ok: true };
@@ -51,3 +51,9 @@ export declare function resolveGatewayUpstreams(agentId: AgentId, canonical: Pic
51
51
  export declare function runMcpGateway(opts: RunMcpGatewayOptions): Promise<{
52
52
  exitCode: number;
53
53
  }>;
54
+ /** The first-class runtime entrypoint. mcp-gateway remains a compatibility
55
+ * alias for existing native agent config, but both edges use the same
56
+ * provider registry and backend construction path. */
57
+ export declare function runMcpRuntime(opts: RunMcpGatewayOptions): Promise<{
58
+ exitCode: number;
59
+ }>;