agent-trellis 0.6.3 → 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 (107) 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 +26 -2
  10. package/dist/adapters/mcpPlan.js +135 -15
  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 +129 -20
  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 +37 -7
  32. package/dist/commands/migrate.js +253 -27
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +463 -72
  35. package/dist/commands/onboardVerdict.js +20 -7
  36. package/dist/commands/rollback.js +4 -1
  37. package/dist/commands/secretsAudit.d.ts +21 -12
  38. package/dist/commands/secretsAudit.js +39 -10
  39. package/dist/commands/skill.d.ts +17 -0
  40. package/dist/commands/skill.js +47 -2
  41. package/dist/commands/sync.js +2 -0
  42. package/dist/core/canonical.d.ts +58 -4
  43. package/dist/core/canonical.js +183 -11
  44. package/dist/core/capabilitySelection.d.ts +45 -0
  45. package/dist/core/capabilitySelection.js +124 -0
  46. package/dist/core/types.d.ts +25 -2
  47. package/dist/core/types.js +7 -0
  48. package/dist/lib/backup.d.ts +2 -0
  49. package/dist/lib/backup.js +4 -0
  50. package/dist/lib/builtinSkills.d.ts +2 -0
  51. package/dist/lib/builtinSkills.js +4 -0
  52. package/dist/lib/cliMetadata.d.ts +1 -0
  53. package/dist/lib/cliMetadata.js +14 -0
  54. package/dist/lib/gatewayBackend.d.ts +28 -0
  55. package/dist/lib/gatewayBackend.js +72 -1
  56. package/dist/lib/installAgent.d.ts +7 -5
  57. package/dist/lib/installAgent.js +4 -3
  58. package/dist/lib/mcpConnect.js +5 -1
  59. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  60. package/dist/lib/mcpMigrateRead.js +3 -0
  61. package/dist/lib/mcpRuntime.d.ts +46 -0
  62. package/dist/lib/mcpRuntime.js +185 -0
  63. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  64. package/dist/lib/mcpStatusProvider.js +25 -0
  65. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  66. package/dist/lib/mcpToolRegistry.js +107 -14
  67. package/dist/lib/memoryGraph.d.ts +1 -1
  68. package/dist/lib/memoryGraph.js +9 -3
  69. package/dist/lib/memoryProvider.d.ts +58 -0
  70. package/dist/lib/memoryProvider.js +175 -0
  71. package/dist/lib/memoryStatus.d.ts +17 -0
  72. package/dist/lib/memoryStatus.js +88 -0
  73. package/dist/lib/nativeMemory.d.ts +19 -0
  74. package/dist/lib/nativeMemory.js +129 -0
  75. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  76. package/dist/lib/runtimeControlProvider.js +168 -0
  77. package/dist/lib/secretEnv.d.ts +17 -0
  78. package/dist/lib/secretEnv.js +31 -1
  79. package/dist/lib/skillProvider.d.ts +13 -0
  80. package/dist/lib/skillProvider.js +210 -0
  81. package/dist/lib/taskProvider.d.ts +7 -0
  82. package/dist/lib/taskProvider.js +43 -0
  83. package/dist/lib/taskStore.d.ts +46 -0
  84. package/dist/lib/taskStore.js +156 -0
  85. package/dist/lib/terminalPicker.d.ts +23 -28
  86. package/dist/lib/terminalPicker.js +54 -164
  87. package/dist/pi-bridge/bundle.js +493 -51
  88. package/dist/pi-bridge/index.js +42 -21
  89. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  90. package/dist/pi-bridge/schemaTranslate.js +4 -8
  91. package/dist/probes/kimi-code.d.ts +12 -0
  92. package/dist/probes/kimi-code.js +55 -0
  93. package/dist/sdk.d.ts +1 -1
  94. package/docs/architecture.md +224 -26
  95. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  96. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  97. package/docs/assets/trellis-mark.svg +12 -0
  98. package/docs/getting-started.md +403 -35
  99. package/docs/overview.md +131 -0
  100. package/docs/overview.zh-CN.md +100 -0
  101. package/docs/research.md +59 -0
  102. package/docs/roadmap.md +10 -5
  103. package/package.json +5 -3
  104. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  105. package/schema/capability-selection.example.yaml +23 -0
  106. package/schema/scope.example.yaml +1 -1
  107. package/schema/servers.example.yaml +15 -1
@@ -0,0 +1,45 @@
1
+ import type { AgentId, CapabilityDelivery, CanonicalSource, McpRouteMode } from "./types.js";
2
+ export type NameSelection = "all" | "none" | string[];
3
+ export interface CapabilitySelection {
4
+ skills: NameSelection;
5
+ mcpServers: NameSelection;
6
+ memories: NameSelection;
7
+ memoryMigration: "all" | "none";
8
+ mcpRoutes: Partial<Record<AgentId, {
9
+ mode: McpRouteMode;
10
+ servers?: string[];
11
+ }>>;
12
+ runtimeDelivery: Partial<Record<AgentId, CapabilityDelivery>>;
13
+ }
14
+ export interface CapabilityItem {
15
+ name: string;
16
+ displayName: string;
17
+ fingerprint?: string;
18
+ status: "new" | "unchanged" | "changed" | "unsupported";
19
+ detail?: string;
20
+ }
21
+ export interface UnsupportedCapabilitySource {
22
+ name: string;
23
+ detail: string;
24
+ }
25
+ export interface CapabilityInventory {
26
+ skills: CapabilityItem[];
27
+ mcpServers: CapabilityItem[];
28
+ canonicalMemories: CapabilityItem[];
29
+ nativeMemory: UnsupportedCapabilitySource[];
30
+ }
31
+ export type SelectionFileResult = {
32
+ ok: true;
33
+ selection: CapabilitySelection;
34
+ } | {
35
+ ok: false;
36
+ error: string;
37
+ };
38
+ export declare function parseCapabilitySelectionFile(path: string): SelectionFileResult;
39
+ export declare function defaultCapabilitySelection(): CapabilitySelection;
40
+ export declare function resolveSelectedNames(selection: NameSelection, available: readonly string[]): string[];
41
+ export declare function selectionContains(selection: NameSelection, name: string): boolean;
42
+ export declare function buildCapabilityInventory(source: {
43
+ skillNames: readonly string[];
44
+ mcpServerNames: readonly string[];
45
+ }, canonical: Pick<CanonicalSource, "skills" | "mcp" | "memories">, nativeMemory?: UnsupportedCapabilitySource[]): CapabilityInventory;
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { ALL_AGENTS } from "./types.js";
4
+ const ALL_SELECTION = {
5
+ skills: "all",
6
+ mcpServers: "all",
7
+ memories: "all",
8
+ memoryMigration: "all",
9
+ mcpRoutes: {},
10
+ runtimeDelivery: {},
11
+ };
12
+ function parseNameSelection(value, field) {
13
+ if (value === undefined)
14
+ return "all";
15
+ if (value === "all" || value === "none")
16
+ return value;
17
+ if (Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0)) {
18
+ return [...new Set(value)];
19
+ }
20
+ throw new Error(`${field} must be "all", "none", or a list of names`);
21
+ }
22
+ function parseRoutes(value) {
23
+ if (value === undefined)
24
+ return {};
25
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
26
+ throw new Error("mcp_routes must be a map of agent ids to route definitions");
27
+ }
28
+ const routes = {};
29
+ for (const [rawAgent, rawRoute] of Object.entries(value)) {
30
+ if (!ALL_AGENTS.includes(rawAgent)) {
31
+ throw new Error(`mcp_routes contains unknown agent "${rawAgent}"`);
32
+ }
33
+ if (typeof rawRoute !== "object" || rawRoute === null || Array.isArray(rawRoute)) {
34
+ throw new Error(`mcp_routes.${rawAgent} must be a route object`);
35
+ }
36
+ const route = rawRoute;
37
+ if (route.mode !== "direct" && route.mode !== "gateway" && route.mode !== "hub") {
38
+ throw new Error(`mcp_routes.${rawAgent}.mode must be direct, gateway, or hub`);
39
+ }
40
+ if (route.servers !== undefined && (!Array.isArray(route.servers) || !route.servers.every((item) => typeof item === "string" && item.length > 0))) {
41
+ throw new Error(`mcp_routes.${rawAgent}.servers must be a list of names`);
42
+ }
43
+ routes[rawAgent] = {
44
+ mode: route.mode,
45
+ ...(route.servers ? { servers: [...new Set(route.servers)] } : {}),
46
+ };
47
+ }
48
+ return routes;
49
+ }
50
+ function parseRuntimeDelivery(value) {
51
+ if (value === undefined)
52
+ return {};
53
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
54
+ throw new Error("runtime_delivery must be a map of agent ids to native, mcp, or both");
55
+ }
56
+ const delivery = {};
57
+ for (const [rawAgent, rawMode] of Object.entries(value)) {
58
+ if (!ALL_AGENTS.includes(rawAgent)) {
59
+ throw new Error(`runtime_delivery contains unknown agent "${rawAgent}"`);
60
+ }
61
+ if (rawMode !== "native" && rawMode !== "mcp" && rawMode !== "both") {
62
+ throw new Error(`runtime_delivery.${rawAgent} must be native, mcp, or both`);
63
+ }
64
+ delivery[rawAgent] = rawMode;
65
+ }
66
+ return delivery;
67
+ }
68
+ export function parseCapabilitySelectionFile(path) {
69
+ if (!existsSync(path))
70
+ return { ok: false, error: `selection file does not exist: ${path}` };
71
+ try {
72
+ const parsed = (parseYaml(readFileSync(path, "utf8")) ?? {});
73
+ return {
74
+ ok: true,
75
+ selection: {
76
+ skills: parseNameSelection(parsed.skills, "skills"),
77
+ mcpServers: parseNameSelection(parsed.mcp_servers, "mcp_servers"),
78
+ memories: parseNameSelection(parsed.memories, "memories"),
79
+ memoryMigration: parsed.memory_migration === undefined || parsed.memory_migration === "all" ? "all" : parsed.memory_migration === "none" ? "none" : (() => { throw new Error('memory_migration must be "all" or "none"'); })(),
80
+ mcpRoutes: parseRoutes(parsed.mcp_routes),
81
+ runtimeDelivery: parseRuntimeDelivery(parsed.runtime_delivery),
82
+ },
83
+ };
84
+ }
85
+ catch (err) {
86
+ return { ok: false, error: `could not parse selection file ${path}: ${err instanceof Error ? err.message : String(err)}` };
87
+ }
88
+ }
89
+ export function defaultCapabilitySelection() {
90
+ return { ...ALL_SELECTION, mcpRoutes: {}, runtimeDelivery: {} };
91
+ }
92
+ export function resolveSelectedNames(selection, available) {
93
+ if (selection === "all")
94
+ return [...available];
95
+ if (selection === "none")
96
+ return [];
97
+ const availableSet = new Set(available);
98
+ return [...new Set(selection.filter((name) => availableSet.has(name)))];
99
+ }
100
+ export function selectionContains(selection, name) {
101
+ return selection === "all" || (selection !== "none" && selection.includes(name));
102
+ }
103
+ export function buildCapabilityInventory(source, canonical, nativeMemory = []) {
104
+ const canonicalSkills = new Set(canonical.skills.map((skill) => skill.name));
105
+ const canonicalMcp = new Set(Object.keys(canonical.mcp.servers));
106
+ return {
107
+ skills: source.skillNames.map((name) => ({
108
+ name,
109
+ displayName: name,
110
+ status: canonicalSkills.has(name) ? "unchanged" : "new",
111
+ })),
112
+ mcpServers: source.mcpServerNames.map((name) => ({
113
+ name,
114
+ displayName: name,
115
+ status: canonicalMcp.has(name) ? "unchanged" : "new",
116
+ })),
117
+ canonicalMemories: canonical.memories.map((memory) => ({
118
+ name: memory.name,
119
+ displayName: memory.name,
120
+ status: "unchanged",
121
+ })),
122
+ nativeMemory,
123
+ };
124
+ }
@@ -5,11 +5,12 @@
5
5
  * files on disk are its serialization, not the other way around.
6
6
  */
7
7
  export type Transport = "stdio" | "http" | "sse";
8
- export type AgentId = "claude-code" | "codex" | "kiro" | "pi";
8
+ export type McpAuthMode = "oauth";
9
+ export type AgentId = "claude-code" | "codex" | "kiro" | "pi" | "kimi-code";
9
10
  export declare const ALL_AGENTS: readonly AgentId[];
10
11
  /**
11
12
  * Every scopable item (skill, subagent, memory entry, MCP server) defaults
12
- * to "all four agents" when `scope` is omitted — sharing everywhere is the
13
+ * to all managed agents when `scope` is omitted — sharing everywhere is the
13
14
  * common case Trellis exists for; restricting to specific agents is the
14
15
  * exception and must be declared explicitly. See docs/architecture.md
15
16
  * "Private / agent-specific capabilities".
@@ -26,6 +27,9 @@ export type Scope = AgentId[] | undefined;
26
27
  export declare function resolveScope(scope: Scope, managedAgents: readonly AgentId[]): readonly AgentId[];
27
28
  export interface McpServerDef {
28
29
  transport: Transport;
30
+ /** Explicit authorization classification. Absence means ordinary MCP;
31
+ * Trellis never infers OAuth from a URL or a transient 401. */
32
+ auth?: McpAuthMode;
29
33
  /** stdio only */
30
34
  command?: string;
31
35
  args?: string[];
@@ -109,6 +113,18 @@ export interface GatewayConfig {
109
113
  */
110
114
  agents?: AgentId[];
111
115
  }
116
+ export type McpRouteMode = "direct" | "gateway" | "hub";
117
+ export interface McpRoute {
118
+ mode: McpRouteMode;
119
+ /** Omit to expose every eligible canonical server for this agent. */
120
+ servers?: string[];
121
+ }
122
+ export type CapabilityDelivery = "native" | "mcp" | "both";
123
+ export interface McpRuntimeConfig {
124
+ /** Per-agent delivery for Trellis-owned capabilities and upstream MCP.
125
+ * Omitted means native, preserving the existing adapter behavior. */
126
+ delivery?: Partial<Record<AgentId, CapabilityDelivery>>;
127
+ }
112
128
  export interface McpConfig {
113
129
  servers: Record<string, McpServerDef>;
114
130
  /**
@@ -127,7 +143,14 @@ export interface McpConfig {
127
143
  * two mean different things and may both be set, in which case gateway
128
144
  * wins per agent (trellis-mcp-gateway-hosting design.md D5/D6). */
129
145
  gateway?: GatewayConfig;
146
+ /** Optional per-agent route overrides. Explicit routes take precedence
147
+ * over the legacy hub/gateway shorthand for the named agent. */
148
+ routes?: Partial<Record<AgentId, McpRoute>>;
149
+ /** Optional runtime delivery preferences. */
150
+ runtime?: McpRuntimeConfig;
130
151
  }
152
+ export declare function capabilityDeliveryForAgent(agentId: AgentId, mcp: McpConfig): CapabilityDelivery;
153
+ export declare function usesNativeCapabilityDelivery(agentId: AgentId, mcp: McpConfig): boolean;
131
154
  export interface SkillRef {
132
155
  name: string;
133
156
  /** Absolute path to the skill's directory. Must contain `SKILL.md` — the
@@ -9,6 +9,7 @@ export const ALL_AGENTS = [
9
9
  "codex",
10
10
  "kiro",
11
11
  "pi",
12
+ "kimi-code",
12
13
  ];
13
14
  /**
14
15
  * `managedAgents` is the hard outer boundary (trellis-managed-agents
@@ -22,3 +23,9 @@ export function resolveScope(scope, managedAgents) {
22
23
  const candidates = scope ?? managedAgents;
23
24
  return candidates.filter((id) => managedAgents.includes(id));
24
25
  }
26
+ export function capabilityDeliveryForAgent(agentId, mcp) {
27
+ return mcp.runtime?.delivery?.[agentId] ?? "native";
28
+ }
29
+ export function usesNativeCapabilityDelivery(agentId, mcp) {
30
+ return capabilityDeliveryForAgent(agentId, mcp) !== "mcp";
31
+ }
@@ -39,10 +39,12 @@ export interface BackupManifest {
39
39
  operations: BackupOperation[];
40
40
  }
41
41
  export interface BackupSession {
42
+ readonly runId: string;
42
43
  writeFile(path: string, content: string): void;
43
44
  createSymlink(path: string, linkTarget: string): Promise<void>;
44
45
  repairSymlink(path: string, oldLinkTarget: string, newLinkTarget: string): Promise<void>;
45
46
  removeSymlink(path: string, oldLinkTarget: string): Promise<void>;
47
+ hasOperations(): boolean;
46
48
  /** Writes manifest.json. No-op (creates nothing) if zero operations
47
49
  * were ever recorded. */
48
50
  finalize(): void;
@@ -67,6 +67,7 @@ export function openBackupSession(homeDir, command) {
67
67
  dirCreated = true;
68
68
  }
69
69
  return {
70
+ runId,
70
71
  writeFile(path, content) {
71
72
  ensureDir();
72
73
  const afterHash = sha256(content);
@@ -105,6 +106,9 @@ export function openBackupSession(homeDir, command) {
105
106
  await rm(path, { force: true });
106
107
  operations.push({ kind: "symlink-remove", path, beforeLinkTarget: oldLinkTarget });
107
108
  },
109
+ hasOperations() {
110
+ return operations.length > 0;
111
+ },
108
112
  finalize() {
109
113
  if (!dirCreated)
110
114
  return;
@@ -0,0 +1,2 @@
1
+ export declare const TRELLIS_RUNTIME_SKILL_NAME = "trellis-runtime";
2
+ export declare function isBuiltinSkillName(name: string): boolean;
@@ -0,0 +1,4 @@
1
+ export const TRELLIS_RUNTIME_SKILL_NAME = "trellis-runtime";
2
+ export function isBuiltinSkillName(name) {
3
+ return name === TRELLIS_RUNTIME_SKILL_NAME;
4
+ }
@@ -0,0 +1 @@
1
+ export declare const TRELLIS_VERSION: string;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ /**
3
+ * Read the package version relative to the compiled/source CLI entrypoint.
4
+ * `src/lib` and `dist/lib` are both exactly two levels below package.json,
5
+ * so the same URL works under tsx and from an installed package.
6
+ */
7
+ function loadPackageVersion() {
8
+ const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
9
+ if (typeof manifest.version !== "string" || manifest.version.length === 0) {
10
+ throw new Error("agent-trellis package.json does not declare a version");
11
+ }
12
+ return manifest.version;
13
+ }
14
+ export const TRELLIS_VERSION = loadPackageVersion();
@@ -23,6 +23,8 @@ export interface GatewayBackend {
23
23
  listTools(): Promise<AggregatedTool[]>;
24
24
  /** Invoke one aggregated tool; implementations route it to its owner. */
25
25
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
26
+ /** Sanitized connection state for user/Agent remediation. */
27
+ listStatus?(): readonly UpstreamStatus[];
26
28
  /** Release everything this backend holds. Must be safe to call twice. */
27
29
  close(): Promise<void>;
28
30
  }
@@ -32,6 +34,17 @@ export interface UpstreamSpec {
32
34
  name: string;
33
35
  def: McpServerDef;
34
36
  }
37
+ export type UpstreamStatusKind = "ready" | "auth-required" | "unavailable" | "timeout" | "degraded";
38
+ export interface UpstreamStatus {
39
+ name: string;
40
+ transport: "stdio" | "http" | "sse";
41
+ status: UpstreamStatusKind;
42
+ detail?: string;
43
+ remediation?: string;
44
+ remediationKind?: "oauth" | "environment" | "install" | "endpoint" | "router";
45
+ requiresHuman?: boolean;
46
+ requiresSessionRestart?: boolean;
47
+ }
35
48
  export interface LocalBackendOptions {
36
49
  secretsPolicy: SecretsPolicy;
37
50
  clientInfo: McpClientInfo;
@@ -64,12 +77,27 @@ export declare class LocalBackend implements GatewayBackend {
64
77
  private readonly warn;
65
78
  private readonly registry;
66
79
  private readonly connected;
80
+ private readonly statuses;
67
81
  private closed;
68
82
  private constructor();
69
83
  static connect(upstreams: readonly UpstreamSpec[], options: LocalBackendOptions): Promise<LocalBackend>;
70
84
  private addUpstream;
71
85
  listTools(): Promise<AggregatedTool[]>;
72
86
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
87
+ listStatus(): readonly UpstreamStatus[];
73
88
  close(): Promise<void>;
74
89
  private closeUpstream;
75
90
  }
91
+ /**
92
+ * Attaches a stored OAuth token, refreshing it first if it has expired.
93
+ *
94
+ * Returns the definition unchanged when nothing is stored for this
95
+ * server: the overwhelmingly common case is a remote server that uses no
96
+ * OAuth at all, or one whose credential lives in `headers` already, and
97
+ * neither should be disturbed.
98
+ *
99
+ * An explicit `Authorization` header in canonical wins. Someone who wrote
100
+ * one meant it, and silently overriding it with a stored token would be
101
+ * the kind of invisible precedence that is impossible to debug.
102
+ */
103
+ export declare function withOAuthHeader(name: string, def: McpServerDef, homeDir: string): Promise<McpServerDef>;
@@ -32,6 +32,7 @@ export class LocalBackend {
32
32
  warn;
33
33
  registry = new McpToolRegistry();
34
34
  connected = new Set();
35
+ statuses = new Map();
35
36
  closed = false;
36
37
  constructor(warn) {
37
38
  this.warn = warn;
@@ -43,6 +44,8 @@ export class LocalBackend {
43
44
  return backend;
44
45
  }
45
46
  async addUpstream({ name, def }, options) {
47
+ const transport = def.transport === "http" || def.transport === "sse" ? def.transport : "stdio";
48
+ this.statuses.set(name, { name, transport, status: "unavailable", detail: "connecting" });
46
49
  let effectiveDef = def;
47
50
  if (options.homeDir && def.url) {
48
51
  try {
@@ -53,6 +56,7 @@ export class LocalBackend {
53
56
  // dropped — the same isolation a failed connection gets, never a
54
57
  // browser and never a blocked startup.
55
58
  this.warn(`trellis-mcp-gateway: skipping MCP server "${name}": ${errorText(err)}`);
59
+ this.statuses.set(name, failureStatus(name, def, err));
56
60
  return;
57
61
  }
58
62
  }
@@ -62,6 +66,7 @@ export class LocalBackend {
62
66
  }
63
67
  catch (err) {
64
68
  this.warn(`trellis-mcp-gateway: failed to connect to MCP server "${name}": ${errorText(err)}`);
69
+ this.statuses.set(name, failureStatus(name, def, err));
65
70
  return;
66
71
  }
67
72
  this.connected.add(client);
@@ -73,12 +78,28 @@ export class LocalBackend {
73
78
  catch (err) {
74
79
  this.warn(`trellis-mcp-gateway: failed to list tools for MCP server "${name}": ${errorText(err)}`);
75
80
  await this.closeUpstream(client);
81
+ this.statuses.set(name, failureStatus(name, def, err));
76
82
  return;
77
83
  }
78
84
  const { skipped } = this.registry.add(name, client, tools);
79
85
  for (const collided of skipped) {
80
86
  this.warn(`trellis-mcp-gateway: tool "${collided}" from server "${name}" collides with an already-registered tool and was skipped`);
81
87
  }
88
+ if (tools.length === 0 && name === "mcp-router") {
89
+ this.statuses.set(name, {
90
+ name,
91
+ transport,
92
+ status: "degraded",
93
+ detail: "router connected but exposed no downstream tools",
94
+ remediationKind: "router",
95
+ requiresHuman: true,
96
+ remediation: "check the MCP router's provider login/configuration and reconnect it, then restart the Agent session",
97
+ requiresSessionRestart: true,
98
+ });
99
+ }
100
+ else {
101
+ this.statuses.set(name, { name, transport, status: "ready", detail: `connected; ${tools.length} tool(s) available` });
102
+ }
82
103
  }
83
104
  async listTools() {
84
105
  return this.registry.listTools();
@@ -86,6 +107,9 @@ export class LocalBackend {
86
107
  async callTool(name, args) {
87
108
  return this.registry.callTool(name, args);
88
109
  }
110
+ listStatus() {
111
+ return [...this.statuses.values()];
112
+ }
89
113
  async close() {
90
114
  if (this.closed)
91
115
  return;
@@ -105,6 +129,53 @@ export class LocalBackend {
105
129
  }
106
130
  }
107
131
  }
132
+ function failureStatus(name, def, error) {
133
+ const detail = errorText(error).slice(0, 300);
134
+ const auth = def.url !== undefined && /401|403|unauthorized|forbidden|authentication|required|login/i.test(detail);
135
+ const timeout = /timed out|timeout/i.test(detail);
136
+ if (auth) {
137
+ return {
138
+ name,
139
+ transport: def.transport === "sse" ? "sse" : "http",
140
+ status: "auth-required",
141
+ detail,
142
+ remediationKind: "oauth",
143
+ requiresHuman: true,
144
+ requiresSessionRestart: true,
145
+ remediation: `run \`trellis mcp auth ${name}\` in a human terminal, then restart the Agent session`,
146
+ };
147
+ }
148
+ if (timeout) {
149
+ return {
150
+ name,
151
+ transport: def.transport === "sse" ? "sse" : def.url ? "http" : "stdio",
152
+ status: "timeout",
153
+ detail,
154
+ remediationKind: def.url ? "endpoint" : "environment",
155
+ requiresHuman: true,
156
+ requiresSessionRestart: true,
157
+ remediation: def.url
158
+ ? `check the endpoint/network; if it requires OAuth, run \`trellis mcp auth ${name}\``
159
+ : `check that \`${def.command ?? "the MCP command"}\` is installed and responsive`,
160
+ };
161
+ }
162
+ const envNames = [...(def.env ?? []), ...Object.keys(def.envAliases ?? {})];
163
+ const missingExecutable = /ENOENT|not found|no such file/i.test(detail);
164
+ return {
165
+ name,
166
+ transport: def.transport === "sse" ? "sse" : def.url ? "http" : "stdio",
167
+ status: "unavailable",
168
+ detail,
169
+ remediationKind: def.url ? "endpoint" : (missingExecutable ? "install" : "environment"),
170
+ requiresHuman: true,
171
+ requiresSessionRestart: true,
172
+ remediation: def.url
173
+ ? `check the MCP endpoint and credentials; run \`trellis mcp auth ${name}\` if it uses OAuth`
174
+ : envNames.length > 0
175
+ ? `check the configured environment variables (${envNames.join(", ")}) and rerun \`trellis mcp sync\``
176
+ : `check that \`${def.command ?? "the MCP command"}\` is installed and responsive`,
177
+ };
178
+ }
108
179
  /**
109
180
  * Attaches a stored OAuth token, refreshing it first if it has expired.
110
181
  *
@@ -117,7 +188,7 @@ export class LocalBackend {
117
188
  * one meant it, and silently overriding it with a stored token would be
118
189
  * the kind of invisible precedence that is impossible to debug.
119
190
  */
120
- async function withOAuthHeader(name, def, homeDir) {
191
+ export async function withOAuthHeader(name, def, homeDir) {
121
192
  const hasExplicitAuth = Object.keys(def.headers ?? {}).some((key) => key.toLowerCase() === "authorization");
122
193
  if (hasExplicitAuth)
123
194
  return def;
@@ -2,14 +2,15 @@
2
2
  * Install-then-manage (trellis-managed-agents design.md D5): selecting a
3
3
  * not-yet-present agent into the managed set is itself the authorization
4
4
  * to install it, but never silently — one confirmation, then a real
5
- * `npm install -g <package>` child process. Kiro has no npm package (a
6
- * desktop download) and is refused before this module is ever reached.
5
+ * `npm install -g <package>` child process. Kiro and Kimi Code use official
6
+ * install scripts rather than an npm package and are refused before this
7
+ * module is ever reached.
7
8
  */
8
9
  import type { AgentId } from "../core/types.js";
9
10
  /** Only agents with a real `npm install -g <pkg>` command — Kiro's own
10
11
  * `INSTALL_HINTS` entry is a download URL, not a package, and is never
11
12
  * looked up here. */
12
- export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro">, string>;
13
+ export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro" | "kimi-code">, string>;
13
14
  export interface ConfirmAndInstallOptions {
14
15
  /** Test/real seam, same pattern as onboard's `promptForAgent` — a real
15
16
  * terminal confirmation by default. */
@@ -19,8 +20,9 @@ export interface ConfirmAndInstallOptions {
19
20
  }
20
21
  export interface ConfirmAndInstallResult {
21
22
  installed: boolean;
22
- /** False only when the agent has no npm package at all (Kiro) — the
23
- * caller refuses this agent with its download URL instead of prompting. */
23
+ /** False when the agent has no supported npm installation path — the
24
+ * caller refuses it with its official installation hint instead of
25
+ * prompting. */
24
26
  installable: boolean;
25
27
  }
26
28
  export declare function confirmAndInstall(agent: AgentId, opts?: ConfirmAndInstallOptions): Promise<ConfirmAndInstallResult>;
@@ -2,8 +2,9 @@
2
2
  * Install-then-manage (trellis-managed-agents design.md D5): selecting a
3
3
  * not-yet-present agent into the managed set is itself the authorization
4
4
  * to install it, but never silently — one confirmation, then a real
5
- * `npm install -g <package>` child process. Kiro has no npm package (a
6
- * desktop download) and is refused before this module is ever reached.
5
+ * `npm install -g <package>` child process. Kiro and Kimi Code use official
6
+ * install scripts rather than an npm package and are refused before this
7
+ * module is ever reached.
7
8
  */
8
9
  import { execFileSync } from "node:child_process";
9
10
  import { createInterface } from "node:readline/promises";
@@ -31,7 +32,7 @@ function runInstallReal(pkg) {
31
32
  execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
32
33
  }
33
34
  export async function confirmAndInstall(agent, opts = {}) {
34
- if (agent === "kiro") {
35
+ if (agent === "kiro" || agent === "kimi-code") {
35
36
  return { installed: false, installable: false };
36
37
  }
37
38
  const pkg = NPM_INSTALLABLE[agent];
@@ -19,6 +19,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
19
19
  import { resolveSecretEnv } from "./secretEnv.js";
20
20
  import { extractTemplateVarNames } from "./envVarNames.js";
21
21
  export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
22
+ const FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS = 1_000;
22
23
  /**
23
24
  * A hanging server (process alive, protocol response never sent) leaves
24
25
  * `promise` permanently unsettled — indistinguishable from "still
@@ -55,7 +56,10 @@ export async function connectWithCleanup(client, transport, timeoutMs, message)
55
56
  }
56
57
  catch (err) {
57
58
  try {
58
- await transport.close();
59
+ // A failed HTTP transport can itself hang while closing. Cleanup is
60
+ // best-effort and must not turn one bad upstream into a Gateway-wide
61
+ // startup timeout; preserve the original connect error below.
62
+ await withTimeout(Promise.resolve().then(() => transport.close()), FAILED_TRANSPORT_CLEANUP_TIMEOUT_MS, "transport cleanup timed out");
59
63
  }
60
64
  catch {
61
65
  // best-effort; the original connect failure is what matters
@@ -37,6 +37,7 @@ export interface McpMigrateReadResult {
37
37
  export declare function resolvedEnvTextMap(def: Pick<McpServerDef, "env" | "envAliases" | "staticEnv">): Record<string, string>;
38
38
  export declare function readClaudeCodeMcpDefs(homeDir: string): McpMigrateReadResult;
39
39
  export declare function readKiroMcpDefs(homeDir: string): McpMigrateReadResult;
40
+ export declare function readKimiCodeMcpDefs(homeDir: string): McpMigrateReadResult;
40
41
  export interface CodexMcpEntryRich {
41
42
  name: string;
42
43
  enabled: boolean;
@@ -108,6 +108,9 @@ export function readClaudeCodeMcpDefs(homeDir) {
108
108
  export function readKiroMcpDefs(homeDir) {
109
109
  return readRichJsonMcpDefs(join(homeDir, ".kiro", "settings", "mcp.json"));
110
110
  }
111
+ export function readKimiCodeMcpDefs(homeDir) {
112
+ return readRichJsonMcpDefs(join(homeDir, ".kimi-code", "mcp.json"));
113
+ }
111
114
  /**
112
115
  * Pure conversion half — separated from the subprocess/file-read effects
113
116
  * below so the stdio/non-stdio/malformed decision logic is unit-testable
@@ -0,0 +1,46 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { AgentId } from "../core/types.js";
4
+ import type { GatewayBackend } from "./gatewayBackend.js";
5
+ export interface RuntimeContext {
6
+ agentId: AgentId;
7
+ homeDir: string;
8
+ }
9
+ export interface TrellisProvider {
10
+ readonly id: string;
11
+ listTools?(context: RuntimeContext): Promise<Tool[]> | Tool[];
12
+ callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
13
+ listResources?(context: RuntimeContext): Promise<Resource[]> | Resource[];
14
+ readResource?(uri: URL, context: RuntimeContext): Promise<ReadResourceResult> | ReadResourceResult;
15
+ listPrompts?(context: RuntimeContext): Promise<Prompt[]> | Prompt[];
16
+ getPrompt?(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult> | GetPromptResult;
17
+ close?(): Promise<void>;
18
+ }
19
+ export declare class BuiltinRegistry {
20
+ private readonly providers;
21
+ private readonly warn;
22
+ private readonly toolOwners;
23
+ private readonly originalToolOwners;
24
+ private readonly exposedToOriginal;
25
+ private readonly resourceOwners;
26
+ private readonly promptOwners;
27
+ constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void);
28
+ listTools(context: RuntimeContext): Promise<Tool[]>;
29
+ callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
30
+ listResources(context: RuntimeContext): Promise<Resource[]>;
31
+ readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
32
+ listPrompts(context: RuntimeContext): Promise<Prompt[]>;
33
+ getPrompt(name: string, args: Record<string, string> | undefined, context: RuntimeContext): Promise<GetPromptResult>;
34
+ close(): Promise<void>;
35
+ }
36
+ /** Adapts the existing gateway tool backend without duplicating its routing
37
+ * or upstream connection logic. */
38
+ export declare class UpstreamProvider implements TrellisProvider {
39
+ private readonly backend;
40
+ readonly id = "upstream";
41
+ constructor(backend: GatewayBackend);
42
+ listTools(): Promise<Tool[]>;
43
+ callTool(name: string, args: unknown): Promise<CallToolResult>;
44
+ close(): Promise<void>;
45
+ }
46
+ export declare function createRuntimeServer(serverInfo: Implementation, context: RuntimeContext, registry: BuiltinRegistry): Server;