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.
- 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 +26 -2
- package/dist/adapters/mcpPlan.js +135 -15
- 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 +129 -20
- 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 +37 -7
- package/dist/commands/migrate.js +253 -27
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +463 -72
- package/dist/commands/onboardVerdict.js +20 -7
- package/dist/commands/rollback.js +4 -1
- package/dist/commands/secretsAudit.d.ts +21 -12
- package/dist/commands/secretsAudit.js +39 -10
- 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 +58 -4
- package/dist/core/canonical.js +183 -11
- 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 +17 -0
- package/dist/lib/secretEnv.js +31 -1
- 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 -28
- package/dist/lib/terminalPicker.js +54 -164
- package/dist/pi-bridge/bundle.js +493 -51
- 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 +403 -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 +10 -5
- package/package.json +5 -3
- 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/migrate.js
CHANGED
|
@@ -4,25 +4,53 @@
|
|
|
4
4
|
* Pure `collectMigratePlan` / effectful `applyMigratePlan`, same
|
|
5
5
|
* plan-then-apply split every adapter already uses.
|
|
6
6
|
*/
|
|
7
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
10
|
import * as claudeCodeProbe from "../probes/claude-code.js";
|
|
11
11
|
import * as codexProbe from "../probes/codex.js";
|
|
12
12
|
import * as kiroProbe from "../probes/kiro.js";
|
|
13
13
|
import * as piProbe from "../probes/pi.js";
|
|
14
|
+
import * as kimiCodeProbe from "../probes/kimi-code.js";
|
|
14
15
|
import { AGENTS_MD_TEMPLATE } from "./init.js";
|
|
15
16
|
import { decideDirImport } from "../lib/dirEquals.js";
|
|
16
17
|
import { deepEqual } from "../lib/deepEqual.js";
|
|
17
|
-
import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
|
|
18
|
+
import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, readKimiCodeMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
|
|
19
|
+
import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
|
|
18
20
|
import { findLiteralSecret } from "../adapters/mcpPlan.js";
|
|
19
|
-
import { loadCanonicalSource, upsertServerYaml } from "../core/canonical.js";
|
|
21
|
+
import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
|
|
20
22
|
import { ALL_AGENTS } from "../core/types.js";
|
|
23
|
+
import { discoverNativeMemory, MAX_NATIVE_MEMORY_BYTES, renderNativeMemory } from "../lib/nativeMemory.js";
|
|
24
|
+
/** Where a `staticEnv` literal secret's real value goes when
|
|
25
|
+
* `secrets.policy.yaml` doesn't already have its own `env_file`
|
|
26
|
+
* (trellis-migrate-extract-static-env-secrets design.md D3) — a sibling
|
|
27
|
+
* of `servers.yaml`, so "the mcp stuff" stays one directory rather than
|
|
28
|
+
* a second, disconnected location. `~`-form for what gets written into
|
|
29
|
+
* `secrets.policy.yaml` itself (portable across machines); resolved form
|
|
30
|
+
* for real file I/O. */
|
|
31
|
+
const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
|
|
32
|
+
function defaultLocalSecretsEnvFilePath(homeDir) {
|
|
33
|
+
return join(homeDir, ".trellis", "mcp", "servers.local.env");
|
|
34
|
+
}
|
|
35
|
+
/** Picks the rc file `ensureShellEnvSource` writes its one pointer block
|
|
36
|
+
* into, from `$SHELL` — zsh and bash are the only shells Trellis's own
|
|
37
|
+
* dev/CI machines and this feature's real-machine dogfooding have ever
|
|
38
|
+
* exercised; anything else falls back to `.profile`, the POSIX-sh default
|
|
39
|
+
* every shell still sources one way or another. */
|
|
40
|
+
function defaultShellRcPath(homeDir) {
|
|
41
|
+
const shell = process.env.SHELL ?? "";
|
|
42
|
+
if (shell.includes("zsh"))
|
|
43
|
+
return join(homeDir, ".zshrc");
|
|
44
|
+
if (shell.includes("bash"))
|
|
45
|
+
return join(homeDir, ".bash_profile");
|
|
46
|
+
return join(homeDir, ".profile");
|
|
47
|
+
}
|
|
21
48
|
const PROBES = {
|
|
22
49
|
"claude-code": (homeDir) => claudeCodeProbe.probe(homeDir),
|
|
23
50
|
codex: (homeDir) => codexProbe.probe(homeDir),
|
|
24
51
|
kiro: (homeDir) => kiroProbe.probe(homeDir),
|
|
25
52
|
pi: (homeDir) => piProbe.probe(homeDir),
|
|
53
|
+
"kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
|
|
26
54
|
};
|
|
27
55
|
/** pi has no static MCP config to read at all (roadmap.md P14/
|
|
28
56
|
* trellis-migrate-mcp-servers) — deliberately absent, not an oversight;
|
|
@@ -31,6 +59,7 @@ const MCP_READERS = {
|
|
|
31
59
|
"claude-code": readClaudeCodeMcpDefs,
|
|
32
60
|
kiro: readKiroMcpDefs,
|
|
33
61
|
codex: readCodexMcpDefs,
|
|
62
|
+
"kimi-code": readKimiCodeMcpDefs,
|
|
34
63
|
};
|
|
35
64
|
function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
|
|
36
65
|
if (isSymlink) {
|
|
@@ -91,6 +120,32 @@ function planInstructions(snapshot, canonicalAgentsMd) {
|
|
|
91
120
|
remediation: `compare \`~/.trellis/agents.md\` against ${snapshot.instructionsFile.path} and either merge by hand or delete whichever copy you don't want to keep`,
|
|
92
121
|
};
|
|
93
122
|
}
|
|
123
|
+
function planNativeMemoryCandidate(candidate, homeDir) {
|
|
124
|
+
const targetPath = join(homeDir, ".trellis", "memories", `${candidate.targetName}.md`);
|
|
125
|
+
const rendered = renderNativeMemory(candidate);
|
|
126
|
+
if (!existsSync(targetPath)) {
|
|
127
|
+
return {
|
|
128
|
+
kind: "memory",
|
|
129
|
+
name: candidate.targetName,
|
|
130
|
+
action: "create",
|
|
131
|
+
detail: `will import ${candidate.displayName} from ${candidate.sourceAgent} native memory`,
|
|
132
|
+
memorySourcePath: candidate.sourcePath,
|
|
133
|
+
memorySourceId: candidate.sourceId,
|
|
134
|
+
memoryTargetPath: targetPath,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const current = readFileSync(targetPath, "utf8");
|
|
138
|
+
if (current === rendered) {
|
|
139
|
+
return { kind: "memory", name: candidate.targetName, action: "already-migrated", detail: "canonical memory is byte-identical" };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
kind: "memory",
|
|
143
|
+
name: candidate.targetName,
|
|
144
|
+
action: "conflict",
|
|
145
|
+
detail: `canonical memories/${candidate.targetName}.md already has different content — resolve by hand`,
|
|
146
|
+
remediation: `compare the canonical file with ${candidate.sourcePath} and decide which memory to keep before re-running migration`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
94
149
|
/**
|
|
95
150
|
* True when `existing` and `def` differ ONLY in which of `env`/
|
|
96
151
|
* `envAliases`/`staticEnv` an env value is filed under, while every
|
|
@@ -110,24 +165,119 @@ export function isSafeReclassification(existing, def) {
|
|
|
110
165
|
return false;
|
|
111
166
|
return deepEqual(resolvedEnvTextMap(existing), resolvedEnvTextMap(def));
|
|
112
167
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
168
|
+
/** Moves `sourceKey` out of `staticEnv` into a name-only `env` reference
|
|
169
|
+
* under `varName` — the def this produces never holds the literal that
|
|
170
|
+
* triggered extraction (trellis-migrate-extract-static-env-secrets
|
|
171
|
+
* design.md D7), safe to upsert into canonical or appear in any output.
|
|
172
|
+
* `sourceKey` and `varName` differ under D11's `TRELLIS_<SERVER>_<KEY>`
|
|
173
|
+
* naming scheme — the source dict key is not, in general, the name the
|
|
174
|
+
* value ends up referenced by. */
|
|
175
|
+
function extractedMcpDef(def, sourceKey, varName) {
|
|
176
|
+
const remainingStaticEnv = { ...(def.staticEnv ?? {}) };
|
|
177
|
+
delete remainingStaticEnv[sourceKey];
|
|
178
|
+
const result = { ...def, env: [...(def.env ?? []), varName] };
|
|
179
|
+
if (Object.keys(remainingStaticEnv).length > 0) {
|
|
180
|
+
result.staticEnv = remainingStaticEnv;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
delete result.staticEnv;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/** Uppercase, non-alphanumeric runs collapsed to a single `_`, no leading
|
|
188
|
+
* or trailing `_` — the one sanitization every synthesized name piece
|
|
189
|
+
* shares (design.md D11), so `mcp-router` becomes `MCP_ROUTER` and a
|
|
190
|
+
* header key like `X-Api-Key` becomes `X_API_KEY`. */
|
|
191
|
+
function sanitizeEnvNamePart(s) {
|
|
192
|
+
return s.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "X";
|
|
193
|
+
}
|
|
194
|
+
/** `TRELLIS_<SERVER>_<KEY>` — the project-wide prefix rules out a
|
|
195
|
+
* collision with anything already in the user's own environment or
|
|
196
|
+
* another tool's convention; the server-name segment rules out two
|
|
197
|
+
* Trellis-managed servers colliding with each other over the same key
|
|
198
|
+
* (design.md D11). Deliberately never the bare source key alone (what
|
|
199
|
+
* the code shipped with initially, and what an already-extracted server
|
|
200
|
+
* predating this scheme still uses — `planStaticEnvExtraction`'s own
|
|
201
|
+
* value-based lookup is what keeps recognizing those, not this
|
|
202
|
+
* function). */
|
|
203
|
+
function synthesizeVarName(serverName, key) {
|
|
204
|
+
return `TRELLIS_${sanitizeEnvNamePart(serverName)}_${sanitizeEnvNamePart(key)}`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Only reached when `findLiteralSecret` matched inside `staticEnv` —
|
|
208
|
+
* `key` is that field's own dict key (design.md D1). Reads the target
|
|
209
|
+
* local-secrets file read-only to decide the outcome; never writes
|
|
210
|
+
* anything itself (planning stays dry-run-safe) and never puts the real
|
|
211
|
+
* value on the returned item (design.md D9) — only used here, in memory,
|
|
212
|
+
* to compare against what's already on disk.
|
|
213
|
+
*
|
|
214
|
+
* The variable name a *new* extraction is filed under is synthesized
|
|
215
|
+
* (design.md D11's `TRELLIS_<SERVER>_<KEY>`, not the bare `key`) — but an
|
|
216
|
+
* already-extracted server may have been extracted before this scheme
|
|
217
|
+
* existed, under the bare key or any other name. Rather than hardcoding
|
|
218
|
+
* "the old scheme was the bare key", this looks up `existing.env` by
|
|
219
|
+
* VALUE: any name already in canonical's `env` list whose local-secrets
|
|
220
|
+
* value already matches `realValue` means this server was already
|
|
221
|
+
* extracted, under whatever name that was, and must keep using it — not
|
|
222
|
+
* get a second, newly-synthesized reference alongside the first.
|
|
223
|
+
*/
|
|
224
|
+
function planStaticEnvExtraction(name, def, existing, homeDir, policy, key) {
|
|
225
|
+
const realValue = def.staticEnv[key];
|
|
226
|
+
const targetPath = policy.envFile ?? defaultLocalSecretsEnvFilePath(homeDir);
|
|
227
|
+
const localSecrets = existsSync(targetPath) ? parseDotenv(readFileSync(targetPath, "utf-8")) : {};
|
|
228
|
+
const priorName = existing?.env?.find((n) => Object.hasOwn(localSecrets, n) && localSecrets[n] === realValue);
|
|
229
|
+
const varName = priorName ?? synthesizeVarName(name, key);
|
|
230
|
+
const safeDef = extractedMcpDef(def, key, varName);
|
|
231
|
+
const alreadyInSecretsFile = Object.hasOwn(localSecrets, varName);
|
|
232
|
+
if (priorName === undefined && alreadyInSecretsFile && localSecrets[varName] !== realValue) {
|
|
233
|
+
return {
|
|
234
|
+
kind: "mcp",
|
|
235
|
+
name,
|
|
236
|
+
action: "conflict",
|
|
237
|
+
detail: `refusing to extract MCP server "${name}"'s "${varName}": ${targetPath} already has a different value for this name — resolve by hand`,
|
|
238
|
+
remediation: `compare the source agent's current value for "${varName}" against the value already in ${targetPath} and update whichever one is stale, then re-run migrate`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (alreadyInSecretsFile && existing !== undefined && deepEqual(existing, safeDef)) {
|
|
242
|
+
return { kind: "mcp", name, action: "already-migrated", detail: `already extracted — "${varName}" is referenced from servers.yaml and its real value is already in ${targetPath}` };
|
|
243
|
+
}
|
|
244
|
+
if (existing !== undefined && !deepEqual(existing, safeDef) && !isSafeReclassification(existing, safeDef)) {
|
|
123
245
|
return {
|
|
124
246
|
kind: "mcp",
|
|
125
247
|
name,
|
|
126
248
|
action: "conflict",
|
|
127
|
-
detail: `
|
|
128
|
-
remediation: `
|
|
249
|
+
detail: `canonical mcp/servers.yaml already has a different definition for "${name}" — resolve by hand`,
|
|
250
|
+
remediation: `compare the source agent's own definition for "${name}" against \`~/.trellis/mcp/servers.yaml\` and update canonical by hand if the source's is the one to keep`,
|
|
129
251
|
};
|
|
130
252
|
}
|
|
253
|
+
return {
|
|
254
|
+
kind: "mcp",
|
|
255
|
+
name,
|
|
256
|
+
action: "extract-secret",
|
|
257
|
+
detail: `will extract "${key}" to ${targetPath} as "${varName}", referencing it from servers.yaml instead of holding the literal value`,
|
|
258
|
+
mcpDef: safeDef,
|
|
259
|
+
extractVarName: varName,
|
|
260
|
+
extractTargetPath: targetPath,
|
|
261
|
+
extractSourceKey: key,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function planMcpServer(name, def, existing, homeDir, policy) {
|
|
265
|
+
// A `staticEnv` match is extracted (trellis-migrate-extract-static-env-secrets):
|
|
266
|
+
// its own dict key is a natural variable name, and `env`/`env_vars` is
|
|
267
|
+
// the one shape proven to resolve back to a real value for every
|
|
268
|
+
// consumer (pi-bridge, claude-code, codex, kiro). A literal anywhere
|
|
269
|
+
// else (`command`/`url`/`args`/`headers`) has either no natural name to
|
|
270
|
+
// extract to, or no `${VAR}` resolution mechanism proven across every
|
|
271
|
+
// consumer — a fake reference there would be strictly worse than the
|
|
272
|
+
// literal it replaced (an agent silently failing to connect instead of
|
|
273
|
+
// the source agent's own working config). Accepted as ordinary literal
|
|
274
|
+
// config instead (design.md D11), same as if no match were found at
|
|
275
|
+
// all; `secrets audit` keeps flagging canonical itself so this isn't
|
|
276
|
+
// silent.
|
|
277
|
+
const secretMatch = findLiteralSecret(def);
|
|
278
|
+
if (secretMatch?.field === "staticEnv") {
|
|
279
|
+
return planStaticEnvExtraction(name, def, existing, homeDir, policy, secretMatch.key);
|
|
280
|
+
}
|
|
131
281
|
if (existing === undefined) {
|
|
132
282
|
return { kind: "mcp", name, action: "create", detail: "will add to servers.yaml", mcpDef: def };
|
|
133
283
|
}
|
|
@@ -153,13 +303,14 @@ function planMcpServer(name, def, existing) {
|
|
|
153
303
|
* conflict. Omitting `only` (or passing both kinds) is exactly today's
|
|
154
304
|
* behavior.
|
|
155
305
|
*/
|
|
156
|
-
export async function collectMigratePlan(agent, homeDir = homedir(), only) {
|
|
306
|
+
export async function collectMigratePlan(agent, homeDir = homedir(), only, workspaceDir = process.cwd()) {
|
|
157
307
|
const snapshot = await PROBES[agent](homeDir);
|
|
158
308
|
if (!snapshot.present) {
|
|
159
309
|
return { agent, present: false, items: [] };
|
|
160
310
|
}
|
|
161
311
|
const canonicalRoot = join(homeDir, ".trellis");
|
|
162
312
|
const items = [];
|
|
313
|
+
let memoryDiscovery;
|
|
163
314
|
const wants = (kind) => !only || only.includes(kind);
|
|
164
315
|
if (wants("skill")) {
|
|
165
316
|
for (const root of snapshot.skillRoots) {
|
|
@@ -176,38 +327,108 @@ export async function collectMigratePlan(agent, homeDir = homedir(), only) {
|
|
|
176
327
|
if (wants("mcp")) {
|
|
177
328
|
const reader = MCP_READERS[agent];
|
|
178
329
|
if (reader) {
|
|
179
|
-
const
|
|
330
|
+
const canonical = loadCanonicalSource(homeDir);
|
|
180
331
|
const { entries, unsupported } = reader(homeDir);
|
|
181
332
|
for (const { name, def } of entries) {
|
|
182
|
-
items.push(planMcpServer(name, def,
|
|
333
|
+
items.push(planMcpServer(name, def, canonical.mcp.servers[name], homeDir, canonical.secretsPolicy));
|
|
183
334
|
}
|
|
184
335
|
for (const { name, reason } of unsupported) {
|
|
185
336
|
items.push({ kind: "mcp", name, action: "skip-unsupported", detail: reason });
|
|
186
337
|
}
|
|
187
338
|
}
|
|
188
339
|
}
|
|
189
|
-
|
|
340
|
+
if (wants("memory")) {
|
|
341
|
+
const discovery = discoverNativeMemory(agent, homeDir, workspaceDir);
|
|
342
|
+
memoryDiscovery = { ...discovery, candidates: discovery.candidates.map(({ content: _content, ...safe }) => safe) };
|
|
343
|
+
for (const candidate of discovery.candidates) {
|
|
344
|
+
items.push(planNativeMemoryCandidate(candidate, homeDir));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { agent, present: true, items, ...(memoryDiscovery ? { memoryDiscovery } : {}) };
|
|
190
348
|
}
|
|
191
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Re-reads the source agent's real MCP entries at apply time to recover
|
|
351
|
+
* `sourceKey`'s current value — deliberately not carried on the plan item
|
|
352
|
+
* itself (design.md D9), so it never rides through a `--json`/`--dry-run`
|
|
353
|
+
* plan object a caller might log or serialize. A cheap, synchronous
|
|
354
|
+
* re-read (the same `MCP_READERS` function `collectMigratePlan` already
|
|
355
|
+
* called), not a second network/probe round-trip. `sourceKey` (the
|
|
356
|
+
* source's own dict key) and `varName` (the synthesized name it's
|
|
357
|
+
* written under, design.md D11) are looked up and written under
|
|
358
|
+
* separately — they are not, in general, the same string.
|
|
359
|
+
*/
|
|
360
|
+
function applyStaticEnvExtraction(agent, serverName, sourceKey, varName, targetPath, homeDir, backup) {
|
|
361
|
+
const reader = MCP_READERS[agent];
|
|
362
|
+
const realValue = reader?.(homeDir).entries.find((e) => e.name === serverName)?.def.staticEnv?.[sourceKey];
|
|
363
|
+
if (realValue === undefined)
|
|
364
|
+
return; // source changed between plan and apply; nothing left to extract
|
|
365
|
+
if (targetPath === defaultLocalSecretsEnvFilePath(homeDir)) {
|
|
366
|
+
ensureGitignoreEntry(join(homeDir, ".trellis", ".gitignore"), "mcp/servers.local.env", backup);
|
|
367
|
+
}
|
|
368
|
+
writeLocalSecretValue(targetPath, varName, realValue, backup);
|
|
369
|
+
writeSecretsPolicyExtraction(join(homeDir, ".trellis", "secrets.policy.yaml"), { varName, envFilePath: DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE }, backup);
|
|
370
|
+
}
|
|
371
|
+
function copyDirectoryThroughBackup(sourceDir, destDir, backup) {
|
|
372
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
373
|
+
const source = join(sourceDir, entry);
|
|
374
|
+
const dest = join(destDir, entry);
|
|
375
|
+
if (statSync(source).isDirectory()) {
|
|
376
|
+
mkdirSync(dest, { recursive: true });
|
|
377
|
+
copyDirectoryThroughBackup(source, dest, backup);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
backup.writeFile(dest, readFileSync(source, "utf-8"));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export function applyMigratePlan(plan, homeDir = homedir(), backup) {
|
|
192
385
|
const canonicalRoot = join(homeDir, ".trellis");
|
|
193
386
|
for (const item of plan.items) {
|
|
194
|
-
if (item.action !== "create" && item.action !== "reclassify")
|
|
387
|
+
if (item.action !== "create" && item.action !== "reclassify" && item.action !== "extract-secret")
|
|
195
388
|
continue;
|
|
196
389
|
if (item.kind === "skill" && item.sourceDir) {
|
|
197
390
|
const dest = join(canonicalRoot, "skills", item.name);
|
|
198
391
|
mkdirSync(dest, { recursive: true });
|
|
199
|
-
|
|
392
|
+
if (backup)
|
|
393
|
+
copyDirectoryThroughBackup(item.sourceDir, dest, backup);
|
|
394
|
+
else
|
|
395
|
+
cpSync(item.sourceDir, dest, { recursive: true });
|
|
200
396
|
}
|
|
201
397
|
else if (item.kind === "instructions" && item.sourceContent !== undefined) {
|
|
202
398
|
mkdirSync(canonicalRoot, { recursive: true });
|
|
203
|
-
|
|
399
|
+
if (backup)
|
|
400
|
+
backup.writeFile(join(canonicalRoot, "agents.md"), item.sourceContent);
|
|
401
|
+
else
|
|
402
|
+
writeFileSync(join(canonicalRoot, "agents.md"), item.sourceContent);
|
|
204
403
|
}
|
|
205
404
|
else if (item.kind === "mcp" && item.mcpDef) {
|
|
206
|
-
upsertServerYaml(join(canonicalRoot, "mcp", "servers.yaml"), item.name, item.mcpDef);
|
|
405
|
+
upsertServerYaml(join(canonicalRoot, "mcp", "servers.yaml"), item.name, item.mcpDef, backup);
|
|
406
|
+
if (item.action === "extract-secret" && item.extractVarName && item.extractTargetPath && item.extractSourceKey) {
|
|
407
|
+
applyStaticEnvExtraction(plan.agent, item.name, item.extractSourceKey, item.extractVarName, item.extractTargetPath, homeDir, backup);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (item.kind === "memory" && item.memorySourcePath && item.memorySourceId && item.memoryTargetPath) {
|
|
411
|
+
if (statSync(item.memorySourcePath).size > MAX_NATIVE_MEMORY_BYTES)
|
|
412
|
+
throw new Error(`native memory source exceeds the ${MAX_NATIVE_MEMORY_BYTES}-byte limit: ${item.memorySourcePath}`);
|
|
413
|
+
const content = readFileSync(item.memorySourcePath, "utf8");
|
|
414
|
+
const imported = renderNativeMemory({ sourceAgent: plan.agent, sourceId: item.memorySourceId, content });
|
|
415
|
+
mkdirSync(dirname(item.memoryTargetPath), { recursive: true });
|
|
416
|
+
if (backup)
|
|
417
|
+
backup.writeFile(item.memoryTargetPath, imported);
|
|
418
|
+
else
|
|
419
|
+
writeFileSync(item.memoryTargetPath, imported);
|
|
207
420
|
}
|
|
208
421
|
}
|
|
422
|
+
// Runs every real (non-dry-run) migrate invocation, not just the one that
|
|
423
|
+
// performed a fresh extraction — a machine that already extracted a
|
|
424
|
+
// secret before this shell-wiring existed gets fixed the next time
|
|
425
|
+
// migrate runs at all, not only on its next brand-new extraction.
|
|
426
|
+
const envFile = loadCanonicalSource(homeDir).secretsPolicy.envFile;
|
|
427
|
+
if (envFile) {
|
|
428
|
+
ensureShellEnvSource(defaultShellRcPath(homeDir), envFile, backup);
|
|
429
|
+
}
|
|
209
430
|
}
|
|
210
|
-
const ONLY_VALUES = ["skills", "instructions", "mcp"];
|
|
431
|
+
const ONLY_VALUES = ["skills", "instructions", "mcp", "memory"];
|
|
211
432
|
function isMigrateOnlyValue(value) {
|
|
212
433
|
return ONLY_VALUES.includes(value);
|
|
213
434
|
}
|
|
@@ -216,6 +437,8 @@ function toMigrateKinds(only) {
|
|
|
216
437
|
return ["skill"];
|
|
217
438
|
if (only === "instructions")
|
|
218
439
|
return ["instructions"];
|
|
440
|
+
if (only === "memory")
|
|
441
|
+
return ["memory"];
|
|
219
442
|
return ["mcp"];
|
|
220
443
|
}
|
|
221
444
|
export async function runMigrate(opts = {}) {
|
|
@@ -252,12 +475,15 @@ export async function runMigrate(opts = {}) {
|
|
|
252
475
|
* this formatting (including the empty-plan "nothing to migrate" case). */
|
|
253
476
|
export function printPlan(plan, dryRun) {
|
|
254
477
|
console.log(`${dryRun ? "[dry run] " : ""}migrate --from ${plan.agent}`);
|
|
478
|
+
if (plan.memoryDiscovery && plan.memoryDiscovery.status !== "supported") {
|
|
479
|
+
console.log(` memory source — [${plan.memoryDiscovery.status}] ${plan.memoryDiscovery.detail}`);
|
|
480
|
+
}
|
|
255
481
|
if (plan.items.length === 0) {
|
|
256
482
|
console.log(" nothing to migrate");
|
|
257
483
|
return;
|
|
258
484
|
}
|
|
259
485
|
for (const item of plan.items) {
|
|
260
|
-
const label = item.kind === "skill" ? `skill "${item.name}"` : item.kind === "mcp" ? `mcp server "${item.name}"` : "instructions";
|
|
486
|
+
const label = item.kind === "skill" ? `skill "${item.name}"` : item.kind === "mcp" ? `mcp server "${item.name}"` : item.kind === "memory" ? `memory "${item.name}"` : "instructions";
|
|
261
487
|
console.log(` [${item.action}] ${label} — ${item.detail}`);
|
|
262
488
|
}
|
|
263
489
|
}
|
|
@@ -12,15 +12,39 @@
|
|
|
12
12
|
* new skill-copy, symlink, conflict-detection, or secrets-scanning
|
|
13
13
|
* judgment is made here.
|
|
14
14
|
*/
|
|
15
|
-
import type { AgentId } from "../core/types.js";
|
|
15
|
+
import type { AgentId, McpConfig, McpRouteMode } from "../core/types.js";
|
|
16
|
+
import { type CapabilityInventory } from "../core/capabilitySelection.js";
|
|
17
|
+
import { type NativeMemoryStatus } from "../lib/nativeMemory.js";
|
|
16
18
|
import type { MigrateKind, MigratePlan } from "./migrate.js";
|
|
17
19
|
import type { SyncReport } from "./sync.js";
|
|
18
20
|
import type { McpSyncReport } from "./mcp.js";
|
|
19
21
|
import type { SecretsAuditReport } from "./secretsAudit.js";
|
|
20
22
|
import type { MemorySyncResult } from "./memory.js";
|
|
21
23
|
import type { DoctorReport } from "./doctor.js";
|
|
24
|
+
import type { RollbackReport } from "./rollback.js";
|
|
22
25
|
import type { ConfirmAndInstallOptions } from "../lib/installAgent.js";
|
|
23
26
|
import type { VerdictItem } from "./onboardVerdict.js";
|
|
27
|
+
/**
|
|
28
|
+
* Scores only real source-side content. The recommendation is a hint for a
|
|
29
|
+
* first-time user, never a filter: every candidate remains selectable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function migrationSourceScore(summary: Pick<OnboardAgentSummary, "skillCount" | "mcpServerCount" | "hasRealInstructions">): number;
|
|
32
|
+
export declare function recommendedMigrationSource(summaries: readonly (Pick<OnboardAgentSummary, "agent" | "skillCount" | "mcpServerCount" | "hasRealInstructions">)[]): AgentId | undefined;
|
|
33
|
+
export declare const MCP_ROUTE_OPTIONS: ({
|
|
34
|
+
mode: "gateway";
|
|
35
|
+
label: string;
|
|
36
|
+
} | {
|
|
37
|
+
mode: "direct";
|
|
38
|
+
label: string;
|
|
39
|
+
} | {
|
|
40
|
+
mode: "hub";
|
|
41
|
+
label: string;
|
|
42
|
+
})[];
|
|
43
|
+
export declare function interactiveMcpRouteOptions(hubConfigured: boolean): Array<{
|
|
44
|
+
mode: McpRouteMode;
|
|
45
|
+
label: string;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
}>;
|
|
24
48
|
export interface OnboardAgentSummary {
|
|
25
49
|
agent: AgentId;
|
|
26
50
|
present: boolean;
|
|
@@ -37,8 +61,13 @@ export interface OnboardAgentSummary {
|
|
|
37
61
|
* memory). 0 for pi, which has no MCP reader at all — not a gap here,
|
|
38
62
|
* the same fact `migrate.ts` already establishes. */
|
|
39
63
|
mcpServerCount: number;
|
|
64
|
+
mcpServerNames: string[];
|
|
65
|
+
nativeMemoryStatus: NativeMemoryStatus;
|
|
66
|
+
nativeMemoryCount: number;
|
|
67
|
+
nativeMemoryNames: string[];
|
|
68
|
+
nativeMemoryDetail: string;
|
|
40
69
|
}
|
|
41
|
-
export declare function collectOnboardSummary(homeDir?: string): Promise<OnboardAgentSummary[]>;
|
|
70
|
+
export declare function collectOnboardSummary(homeDir?: string, workspaceDir?: string): Promise<OnboardAgentSummary[]>;
|
|
42
71
|
export interface RunOnboardOptions {
|
|
43
72
|
/** Non-interactive migration-source choice. */
|
|
44
73
|
agent?: string;
|
|
@@ -60,10 +89,19 @@ export interface RunOnboardOptions {
|
|
|
60
89
|
* Omitted leaves whatever's already configured untouched — same
|
|
61
90
|
* no-prompt posture as `mcpMode` (design.md D12). */
|
|
62
91
|
memory?: string;
|
|
92
|
+
/** Non-interactive native-source memory migration toggle: `"on"` or
|
|
93
|
+
* `"off"`. Omitted preserves the current state and prompts only when
|
|
94
|
+
* supported source candidates are available in a real terminal. */
|
|
95
|
+
memoryMigrate?: string;
|
|
96
|
+
/** Optional YAML/JSON item-level capability selection for automation. */
|
|
97
|
+
selectionFile?: string;
|
|
63
98
|
dryRun?: boolean;
|
|
64
99
|
json?: boolean;
|
|
65
100
|
/** Defaults to the real `~`; overridable for tests only. */
|
|
66
101
|
homeDir?: string;
|
|
102
|
+
/** Defaults to the current working directory; controls project-scoped
|
|
103
|
+
* native-memory discovery and is overridable for tests. */
|
|
104
|
+
workspaceDir?: string;
|
|
67
105
|
/** Test-only: overrides the real `process.stdin.isTTY` check so the
|
|
68
106
|
* interactive-vs-refuse branch is exercisable without a real terminal. */
|
|
69
107
|
isTTY?: boolean;
|
|
@@ -84,6 +122,13 @@ export interface RunOnboardOptions {
|
|
|
84
122
|
* (design.md D9). `true` means "apply for real", matching what
|
|
85
123
|
* accepting the real picker's "Yes, apply now" option means. */
|
|
86
124
|
promptToApply?: () => Promise<boolean>;
|
|
125
|
+
/** Test-only: replaces the real mode-transition confirmation. */
|
|
126
|
+
promptForModeChange?: (previous: "direct" | "hub" | "gateway", next: "direct" | "hub" | "gateway", agents: readonly AgentId[]) => Promise<boolean>;
|
|
127
|
+
/** Test-only: replaces the interactive shared-Memory choice. `keep` leaves
|
|
128
|
+
* the existing state untouched; the real picker puts that choice first. */
|
|
129
|
+
promptForMemory?: (current: "on" | "off") => Promise<"keep" | "on" | "off">;
|
|
130
|
+
/** Test-only: replaces the interactive native-memory migration choice. */
|
|
131
|
+
promptForMemoryMigration?: (source: AgentId, count: number) => Promise<"keep" | "on" | "off">;
|
|
87
132
|
/** Test-only: injected into every `confirmAndInstall` call for a
|
|
88
133
|
* selected, not-yet-present agent. Never a real terminal prompt or a
|
|
89
134
|
* real `npm install` in a unit test. */
|
|
@@ -96,6 +141,23 @@ export interface OnboardInstallResult {
|
|
|
96
141
|
}
|
|
97
142
|
export interface OnboardResult {
|
|
98
143
|
summary: OnboardAgentSummary[];
|
|
144
|
+
inventory?: CapabilityInventory;
|
|
145
|
+
capabilitySummary?: {
|
|
146
|
+
awarenessSkill: boolean;
|
|
147
|
+
globalInstructions: {
|
|
148
|
+
available: boolean;
|
|
149
|
+
native: boolean;
|
|
150
|
+
runtimeReadable: boolean;
|
|
151
|
+
};
|
|
152
|
+
skills: number;
|
|
153
|
+
memories: number;
|
|
154
|
+
mcpServers: number;
|
|
155
|
+
};
|
|
156
|
+
managedAgentRuntime?: Array<{
|
|
157
|
+
agent: AgentId;
|
|
158
|
+
delivery: "native" | "mcp" | "both";
|
|
159
|
+
route: "direct" | "gateway" | "hub";
|
|
160
|
+
}>;
|
|
99
161
|
source?: AgentId;
|
|
100
162
|
sourceReason?: "auto-selected" | "flag" | "prompt";
|
|
101
163
|
/** The full managed set this run acted against — the union of whatever
|
|
@@ -131,6 +193,8 @@ export interface OnboardResult {
|
|
|
131
193
|
* inconsistent across agents independent of this run.
|
|
132
194
|
*/
|
|
133
195
|
doctorReport?: DoctorReport;
|
|
196
|
+
/** Present when a blocking real run was automatically restored. */
|
|
197
|
+
rollback?: RollbackReport;
|
|
134
198
|
/**
|
|
135
199
|
* The resolved MCP mode after this run (trellis-onboard-mcp-mode
|
|
136
200
|
* design.md D7) — `current` reflects canonical's state whether or not
|
|
@@ -165,6 +229,14 @@ export interface OnboardResult {
|
|
|
165
229
|
*/
|
|
166
230
|
verdict: VerdictItem[];
|
|
167
231
|
}
|
|
232
|
+
/** Converges explicit per-Agent route lists with the shared Memory toggle.
|
|
233
|
+
* Legacy/top-level routes without a `servers` list already expose every
|
|
234
|
+
* eligible server and need no rewrite. Hub routes are intentionally opaque:
|
|
235
|
+
* the external hub owns its upstream set. */
|
|
236
|
+
export declare function mergeMemoryIntoExplicitRoutes(mcp: McpConfig, managedAgents: readonly AgentId[], memoryEnabled: boolean): {
|
|
237
|
+
mcp: McpConfig;
|
|
238
|
+
changed: boolean;
|
|
239
|
+
};
|
|
168
240
|
export declare function collectOnboardPlan(opts?: RunOnboardOptions): Promise<OnboardResult>;
|
|
169
241
|
export declare function runOnboard(opts?: RunOnboardOptions): Promise<{
|
|
170
242
|
exitCode: number;
|