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.
- package/README.md +97 -38
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/cli.js +121 -21
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +38 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +26 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +22 -1
- package/dist/commands/mcp.js +65 -2
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +34 -14
- package/dist/commands/mcpImport.d.ts +27 -0
- package/dist/commands/mcpImport.js +336 -0
- package/dist/commands/memory.d.ts +5 -3
- package/dist/commands/memory.js +12 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +90 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +458 -71
- package/dist/commands/rollback.js +4 -1
- package/dist/commands/skill.d.ts +17 -0
- package/dist/commands/skill.js +47 -2
- package/dist/commands/sync.js +2 -0
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +7 -0
- package/dist/lib/backup.d.ts +2 -0
- package/dist/lib/backup.js +4 -0
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +1 -0
- package/dist/lib/mcpMigrateRead.js +3 -0
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +168 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -49
- package/dist/lib/terminalPicker.js +53 -218
- package/dist/pi-bridge/bundle.js +475 -37
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +224 -26
- package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
- package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
- package/docs/assets/trellis-mark.svg +12 -0
- package/docs/getting-started.md +328 -35
- package/docs/overview.md +131 -0
- package/docs/overview.zh-CN.md +100 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +9 -5
- package/package.json +3 -2
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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. */
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}>;
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}>;
|