agent-trellis 0.15.0 → 0.16.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/dist/cli.js +16 -34
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +55 -1
- package/dist/commands/mcp.d.ts +25 -2
- package/dist/commands/mcp.js +111 -24
- package/dist/commands/mcpGateway.js +3 -3
- package/dist/lib/cliArgs.d.ts +17 -0
- package/dist/lib/cliArgs.js +36 -0
- package/dist/lib/scopeSelection.d.ts +1 -1
- package/dist/lib/scopeSelection.js +4 -3
- package/dist/lib/workspaceConfig.d.ts +7 -1
- package/dist/lib/workspaceConfig.js +20 -1
- package/dist/lib/workspaceDoctor.d.ts +15 -0
- package/dist/lib/workspaceDoctor.js +37 -10
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { collectMemoryList, runMemoryExtraction, runMemorySync } from "./command
|
|
|
24
24
|
import { parseManageArgs, runManage } from "./commands/manage.js";
|
|
25
25
|
import { runKimi } from "./commands/kimi.js";
|
|
26
26
|
import { TRELLIS_VERSION } from "./lib/cliMetadata.js";
|
|
27
|
+
import { flagValue } from "./lib/cliArgs.js";
|
|
27
28
|
import { parseSyncArgs } from "./lib/syncArgs.js";
|
|
28
29
|
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "mcp-runtime", "skill", "add", "update", "memory", "manage", "kimi", "secrets", "rollback"];
|
|
29
30
|
function printUsage() {
|
|
@@ -193,6 +194,9 @@ Commands:
|
|
|
193
194
|
Remove a canonical MCP server, then run the same sync as
|
|
194
195
|
mcp sync so every managed agent's Trellis-owned entry for
|
|
195
196
|
it is removed too
|
|
197
|
+
--workspace remove from .agents/config.json instead of
|
|
198
|
+
servers.yaml (a shadowed global entry takes
|
|
199
|
+
effect again, it is not deleted)
|
|
196
200
|
--dry-run preview the plan, write nothing
|
|
197
201
|
--json machine-readable output, no report text
|
|
198
202
|
skill list
|
|
@@ -325,31 +329,15 @@ async function main(argv) {
|
|
|
325
329
|
return;
|
|
326
330
|
}
|
|
327
331
|
if (command === "onboard") {
|
|
328
|
-
const agentIndex = rest.indexOf("--agent");
|
|
329
|
-
const agent = agentIndex >= 0 ? rest[agentIndex + 1] : undefined;
|
|
330
|
-
const manageIndex = rest.indexOf("--manage");
|
|
331
|
-
const manage = manageIndex >= 0 ? rest[manageIndex + 1] : undefined;
|
|
332
|
-
const mcpModeIndex = rest.indexOf("--mcp-mode");
|
|
333
|
-
const mcpMode = mcpModeIndex >= 0 ? rest[mcpModeIndex + 1] : undefined;
|
|
334
|
-
const hubUrlIndex = rest.indexOf("--hub-url");
|
|
335
|
-
const hubUrl = hubUrlIndex >= 0 ? rest[hubUrlIndex + 1] : undefined;
|
|
336
|
-
const gatewayAgentsIndex = rest.indexOf("--gateway-agents");
|
|
337
|
-
const gatewayAgents = gatewayAgentsIndex >= 0 ? rest[gatewayAgentsIndex + 1] : undefined;
|
|
338
|
-
const memoryIndex = rest.indexOf("--memory");
|
|
339
|
-
const memory = memoryIndex >= 0 ? rest[memoryIndex + 1] : undefined;
|
|
340
|
-
const memoryMigrateIndex = rest.indexOf("--memory-migrate");
|
|
341
|
-
const memoryMigrate = memoryMigrateIndex >= 0 ? rest[memoryMigrateIndex + 1] : undefined;
|
|
342
|
-
const selectionIndex = rest.indexOf("--selection");
|
|
343
|
-
const selectionFile = selectionIndex >= 0 ? rest[selectionIndex + 1] : undefined;
|
|
344
332
|
const { exitCode } = await runOnboard({
|
|
345
|
-
agent,
|
|
346
|
-
manage,
|
|
347
|
-
mcpMode,
|
|
348
|
-
hubUrl,
|
|
349
|
-
gatewayAgents,
|
|
350
|
-
memory,
|
|
351
|
-
memoryMigrate,
|
|
352
|
-
selectionFile,
|
|
333
|
+
agent: flagValue(rest, "--agent"),
|
|
334
|
+
manage: flagValue(rest, "--manage"),
|
|
335
|
+
mcpMode: flagValue(rest, "--mcp-mode"),
|
|
336
|
+
hubUrl: flagValue(rest, "--hub-url"),
|
|
337
|
+
gatewayAgents: flagValue(rest, "--gateway-agents"),
|
|
338
|
+
memory: flagValue(rest, "--memory"),
|
|
339
|
+
memoryMigrate: flagValue(rest, "--memory-migrate"),
|
|
340
|
+
selectionFile: flagValue(rest, "--selection"),
|
|
353
341
|
dryRun: rest.includes("--dry-run"),
|
|
354
342
|
json: rest.includes("--json"),
|
|
355
343
|
});
|
|
@@ -369,21 +357,16 @@ async function main(argv) {
|
|
|
369
357
|
if (rest[i] === "--choose" && rest[i + 1] !== undefined)
|
|
370
358
|
choose.push(rest[i + 1]);
|
|
371
359
|
}
|
|
372
|
-
const dedupIndex = rest.indexOf("--dedup");
|
|
373
360
|
const { exitCode } = await runMigrateWorkspace({
|
|
374
361
|
choose,
|
|
375
|
-
dedup:
|
|
362
|
+
dedup: flagValue(rest, "--dedup"),
|
|
376
363
|
dryRun: rest.includes("--dry-run"),
|
|
377
364
|
json: rest.includes("--json"),
|
|
378
365
|
});
|
|
379
366
|
process.exitCode = exitCode;
|
|
380
367
|
return;
|
|
381
368
|
}
|
|
382
|
-
const
|
|
383
|
-
const from = fromIndex >= 0 ? rest[fromIndex + 1] : undefined;
|
|
384
|
-
const onlyIndex = rest.indexOf("--only");
|
|
385
|
-
const only = onlyIndex >= 0 ? rest[onlyIndex + 1] : undefined;
|
|
386
|
-
const { exitCode } = await runMigrate({ from, only, dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
|
|
369
|
+
const { exitCode } = await runMigrate({ from: flagValue(rest, "--from"), only: flagValue(rest, "--only"), dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
|
|
387
370
|
process.exitCode = exitCode;
|
|
388
371
|
return;
|
|
389
372
|
}
|
|
@@ -511,7 +494,7 @@ async function main(argv) {
|
|
|
511
494
|
process.exitCode = 1;
|
|
512
495
|
return;
|
|
513
496
|
}
|
|
514
|
-
process.exitCode = (await runMcpRemove(name, { json, dryRun })).exitCode;
|
|
497
|
+
process.exitCode = (await runMcpRemove(name, { json, dryRun, workspace: mcpRest.includes("--workspace") })).exitCode;
|
|
515
498
|
return;
|
|
516
499
|
}
|
|
517
500
|
if (subcommand === "renew") {
|
|
@@ -544,8 +527,7 @@ async function main(argv) {
|
|
|
544
527
|
}
|
|
545
528
|
if (subcommand === "add") {
|
|
546
529
|
const [name] = skillRest;
|
|
547
|
-
const
|
|
548
|
-
const from = fromIndex >= 0 ? skillRest[fromIndex + 1] : undefined;
|
|
530
|
+
const from = flagValue(skillRest, "--from");
|
|
549
531
|
if (!name || !from) {
|
|
550
532
|
console.error("Usage: trellis skill add <name> --from <path>");
|
|
551
533
|
process.exitCode = 1;
|
|
@@ -15,7 +15,7 @@ import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
|
15
15
|
* machine.
|
|
16
16
|
*/
|
|
17
17
|
export declare const DEFAULT_KNOWN_HOST_INJECTED: readonly string[];
|
|
18
|
-
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable";
|
|
18
|
+
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable" | "workspace-toml-invalid";
|
|
19
19
|
export interface Finding {
|
|
20
20
|
kind: FindingKind;
|
|
21
21
|
/** Omitted for a finding that names more than one agent (drift). */
|
package/dist/commands/doctor.js
CHANGED
|
@@ -44,6 +44,10 @@ const EXIT_NONZERO_KINDS = new Set([
|
|
|
44
44
|
// exists to catch *before* the session does (trellis-mcp-oauth-renew
|
|
45
45
|
// design.md D4).
|
|
46
46
|
"oauth-expired",
|
|
47
|
+
// A project-level Codex config that fails TOML parsing will fail the
|
|
48
|
+
// *agent*'s own startup regardless of ownership — catch it here before
|
|
49
|
+
// the next session does (trellis-doctor-workspace-toml-validation).
|
|
50
|
+
"workspace-toml-invalid",
|
|
47
51
|
]);
|
|
48
52
|
/** Exported so tests can assert on this resolution directly, without
|
|
49
53
|
* routing through real per-agent probes `runDoctor` also runs. */
|
|
@@ -120,7 +124,53 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
120
124
|
// canonical has not been initialized yet.
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
|
|
127
|
+
// Workspace attribution runs whenever a workspace root is in scope —
|
|
128
|
+
// even when `~/.trellis/` itself failed to load or has never been
|
|
129
|
+
// initialized. The spec's third scenario: a user managing only project
|
|
130
|
+
// skills in `.agents/skills/` without a global Trellis install still
|
|
131
|
+
// gets project-level config validation
|
|
132
|
+
// (trellis-doctor-workspace-toml-validation design.md D5). When the
|
|
133
|
+
// canonical source is absent we synthesize the minimal workspace layer
|
|
134
|
+
// with every map empty — provenance is moot without a global layer, but
|
|
135
|
+
// `collectWorkspaceDoctorSection`'s per-file inspection is what we want.
|
|
136
|
+
let workspace;
|
|
137
|
+
if (canonical?.workspace) {
|
|
138
|
+
workspace = collectWorkspaceDoctorSection(canonical);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const workspaceRoot = workspaceDir != null ? findWorkspaceRoot(workspaceDir) : null;
|
|
142
|
+
if (workspaceRoot) {
|
|
143
|
+
// `managedAgents` is driven by `~/.trellis/managed.yaml`, which is
|
|
144
|
+
// absent in this branch — sweep every known agent so codex's TOML
|
|
145
|
+
// file is still inspected (otherwise an empty list would skip the
|
|
146
|
+
// file entirely and the spec's scenario-3 finding would never fire).
|
|
147
|
+
const minimal = {
|
|
148
|
+
instructionsFile: "",
|
|
149
|
+
managedAgents: ALL_AGENTS,
|
|
150
|
+
skills: [],
|
|
151
|
+
agents: [],
|
|
152
|
+
memories: [],
|
|
153
|
+
mcp: { servers: {}, knownHostInjected: [] },
|
|
154
|
+
secretsPolicy: { allowedVars: [], rejectPatterns: [] },
|
|
155
|
+
diagnostics: [],
|
|
156
|
+
workspace: { root: workspaceRoot, mcpProvenance: {}, mcpGlobalServers: {}, skillProvenance: {}, globalSkills: [], shadowed: [] },
|
|
157
|
+
};
|
|
158
|
+
workspace = collectWorkspaceDoctorSection(minimal);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Workspace TOML findings become top-level findings so the exit code
|
|
162
|
+
// and `--json` consumers see them alongside cross-agent findings, not
|
|
163
|
+
// only inside the workspace section. They keep `agent: "codex"` — the
|
|
164
|
+
// file layout tells us which agent the file breaks.
|
|
165
|
+
if (workspace) {
|
|
166
|
+
for (const finding of workspace.tomlFindings) {
|
|
167
|
+
findings.push({
|
|
168
|
+
kind: "workspace-toml-invalid",
|
|
169
|
+
agent: finding.agent,
|
|
170
|
+
message: `${finding.path}${finding.line !== undefined ? `:${finding.line}${finding.column !== undefined ? `:${finding.column}` : ""}` : ""} — ${finding.reason} (not rendered by Trellis; fix by hand)`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
124
174
|
const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
|
|
125
175
|
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
126
176
|
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}), ...(workspace ? { workspace } : {}) };
|
|
@@ -395,6 +445,10 @@ export function printReport(report) {
|
|
|
395
445
|
for (const hint of ws.unmigrated) {
|
|
396
446
|
console.log(` ⚠ ${hint.path} holds ${hint.entries.length} entr${hint.entries.length === 1 ? "y" : "ies"} Trellis didn't render (${hint.entries.join(", ")}) — run \`trellis migrate --workspace\``);
|
|
397
447
|
}
|
|
448
|
+
for (const finding of ws.tomlFindings) {
|
|
449
|
+
const location = finding.line !== undefined ? `:${finding.line}${finding.column !== undefined ? `:${finding.column}` : ""}` : "";
|
|
450
|
+
console.log(` ⚠ ${finding.path}${location} — ${finding.reason} (not rendered by Trellis — fix by hand and re-run \`trellis doctor\`)`);
|
|
451
|
+
}
|
|
398
452
|
for (const warning of ws.precedenceWarnings) {
|
|
399
453
|
console.log(` ⚠ ${warning}`);
|
|
400
454
|
}
|
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -120,8 +120,9 @@ export declare function runMcpList(opts?: {
|
|
|
120
120
|
};
|
|
121
121
|
/** Raw, unvalidated CLI flag values — `collectMcpAddPlan` does the actual
|
|
122
122
|
* parsing/validation. Kept as plain strings here so `parseMcpAddArgs`
|
|
123
|
-
* stays a dumb `--flag value` reader
|
|
124
|
-
*
|
|
123
|
+
* stays a dumb `--flag value` reader. Both `--flag value` and
|
|
124
|
+
* `--flag=value` are accepted (see src/lib/cliArgs.ts — the `=` form used
|
|
125
|
+
* to be silently dropped). */
|
|
125
126
|
export interface McpAddRawArgs {
|
|
126
127
|
transport?: string;
|
|
127
128
|
auth?: string;
|
|
@@ -139,6 +140,9 @@ export interface McpAddRawArgs {
|
|
|
139
140
|
agents?: string;
|
|
140
141
|
/** `"true"` or `"false"`. */
|
|
141
142
|
enabled?: string;
|
|
143
|
+
/** `--*` tokens no flag claimed (typos, wrong command); `runMcpAdd`
|
|
144
|
+
* warns about them instead of letting them vanish silently. */
|
|
145
|
+
unknown?: string[];
|
|
142
146
|
}
|
|
143
147
|
export declare function parseMcpAddArgs(argv: readonly string[]): McpAddRawArgs;
|
|
144
148
|
export declare function parseMcpSetArgs(argv: readonly string[]): McpSetAuthRawArgs;
|
|
@@ -199,6 +203,8 @@ export interface McpSetAuthRawArgs {
|
|
|
199
203
|
clientId?: string;
|
|
200
204
|
/** A variable NAME, never a secret value. */
|
|
201
205
|
clientSecretEnv?: string;
|
|
206
|
+
/** See `McpAddRawArgs.unknown`. */
|
|
207
|
+
unknown?: string[];
|
|
202
208
|
}
|
|
203
209
|
/**
|
|
204
210
|
* Sets or clears the OAuth classification, and optionally the
|
|
@@ -342,10 +348,27 @@ export declare function applyMcpRemoveWithSync(plan: McpRemovePlan, opts?: {
|
|
|
342
348
|
dryRun?: boolean;
|
|
343
349
|
backupSession?: BackupSession;
|
|
344
350
|
}): Promise<McpRemoveOutcome>;
|
|
351
|
+
/** Workspace twin of `collectMcpRemovePlan`: presence is judged against
|
|
352
|
+
* `.agents/config.json` only — a global entry of the same name is this
|
|
353
|
+
* entry's shadow target, not a reason to touch the global file. */
|
|
354
|
+
export declare function collectMcpRemoveWorkspacePlan(name: string, workspaceRoot: string): McpRemovePlan;
|
|
355
|
+
/** Workspace twin of `applyMcpRemoveWithSync`: deletes the entry from
|
|
356
|
+
* `.agents/config.json` (backup-session wrapped), then cascades the same
|
|
357
|
+
* mcp sync — run inside the workspace, so `planWorkspaceMcp` sees the
|
|
358
|
+
* entry is gone and removes the rendered project-level copies (the
|
|
359
|
+
* ledger-gated propagation already exists; this command was the missing
|
|
360
|
+
* corner). A shadowed global entry simply takes effect again. */
|
|
361
|
+
export declare function applyMcpRemoveWorkspaceWithSync(plan: McpRemovePlan, workspaceRoot: string, opts?: {
|
|
362
|
+
homeDir?: string;
|
|
363
|
+
dryRun?: boolean;
|
|
364
|
+
backupSession?: BackupSession;
|
|
365
|
+
}): Promise<McpRemoveOutcome>;
|
|
345
366
|
export declare function runMcpRemove(name: string, opts?: {
|
|
346
367
|
homeDir?: string;
|
|
347
368
|
json?: boolean;
|
|
348
369
|
dryRun?: boolean;
|
|
370
|
+
workspace?: boolean;
|
|
371
|
+
workspaceDir?: string;
|
|
349
372
|
}): Promise<{
|
|
350
373
|
exitCode: number;
|
|
351
374
|
}>;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -29,8 +29,9 @@ import { ZcodeAdapter } from "../adapters/zcode.js";
|
|
|
29
29
|
import { openBackupSession } from "../lib/backup.js";
|
|
30
30
|
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
31
31
|
import { applyWorkspaceMcp, planWorkspaceMcp, workspacePrecedenceWarnings } from "../lib/workspaceMcp.js";
|
|
32
|
-
import { readWorkspaceConfigJson, upsertWorkspaceServer } from "../lib/workspaceConfig.js";
|
|
32
|
+
import { readWorkspaceConfigJson, removeWorkspaceServer, upsertWorkspaceServer } from "../lib/workspaceConfig.js";
|
|
33
33
|
import { toServerDefYaml } from "../core/mcpServerDef.js";
|
|
34
|
+
import { flagValue, unknownFlags } from "../lib/cliArgs.js";
|
|
34
35
|
const ADAPTER_FACTORY = {
|
|
35
36
|
"claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
|
|
36
37
|
codex: (homeDir) => new CodexAdapter(homeDir),
|
|
@@ -232,34 +233,39 @@ export function runMcpList(opts = {}) {
|
|
|
232
233
|
}
|
|
233
234
|
return { exitCode: 0 };
|
|
234
235
|
}
|
|
236
|
+
const MCP_ADD_KNOWN_FLAGS = [
|
|
237
|
+
"--transport", "--auth", "--command", "--args", "--url", "--headers",
|
|
238
|
+
"--env", "--static-env", "--agents", "--enabled",
|
|
239
|
+
"--workspace", "--dry-run", "--json",
|
|
240
|
+
];
|
|
235
241
|
export function parseMcpAddArgs(argv) {
|
|
236
|
-
const
|
|
237
|
-
const i = argv.indexOf(name);
|
|
238
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
239
|
-
};
|
|
242
|
+
const unknown = unknownFlags(argv, MCP_ADD_KNOWN_FLAGS);
|
|
240
243
|
return {
|
|
241
|
-
transport:
|
|
242
|
-
auth:
|
|
243
|
-
command:
|
|
244
|
-
args:
|
|
245
|
-
url:
|
|
246
|
-
headers:
|
|
247
|
-
env:
|
|
248
|
-
staticEnv:
|
|
249
|
-
agents:
|
|
250
|
-
enabled:
|
|
244
|
+
transport: flagValue(argv, "--transport"),
|
|
245
|
+
auth: flagValue(argv, "--auth"),
|
|
246
|
+
command: flagValue(argv, "--command"),
|
|
247
|
+
args: flagValue(argv, "--args"),
|
|
248
|
+
url: flagValue(argv, "--url"),
|
|
249
|
+
headers: flagValue(argv, "--headers"),
|
|
250
|
+
env: flagValue(argv, "--env"),
|
|
251
|
+
staticEnv: flagValue(argv, "--static-env"),
|
|
252
|
+
agents: flagValue(argv, "--agents"),
|
|
253
|
+
enabled: flagValue(argv, "--enabled"),
|
|
254
|
+
...(unknown.length > 0 ? { unknown } : {}),
|
|
251
255
|
};
|
|
252
256
|
}
|
|
257
|
+
const MCP_SET_KNOWN_FLAGS = [
|
|
258
|
+
"--auth", "--client-id", "--client-secret-env", "--auth-owner",
|
|
259
|
+
"--dry-run", "--json",
|
|
260
|
+
];
|
|
253
261
|
export function parseMcpSetArgs(argv) {
|
|
254
|
-
const
|
|
255
|
-
const i = argv.indexOf(name);
|
|
256
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
257
|
-
};
|
|
262
|
+
const unknown = unknownFlags(argv, MCP_SET_KNOWN_FLAGS);
|
|
258
263
|
return {
|
|
259
|
-
auth:
|
|
260
|
-
clientId:
|
|
261
|
-
clientSecretEnv:
|
|
262
|
-
authOwner:
|
|
264
|
+
auth: flagValue(argv, "--auth"),
|
|
265
|
+
clientId: flagValue(argv, "--client-id"),
|
|
266
|
+
clientSecretEnv: flagValue(argv, "--client-secret-env"),
|
|
267
|
+
authOwner: flagValue(argv, "--auth-owner"),
|
|
268
|
+
...(unknown.length > 0 ? { unknown } : {}),
|
|
263
269
|
};
|
|
264
270
|
}
|
|
265
271
|
function parseCsv(raw) {
|
|
@@ -491,6 +497,7 @@ export function collectMcpSetAuthPlan(name, raw, homeDir = homedir()) {
|
|
|
491
497
|
}
|
|
492
498
|
export function runMcpSetAuth(name, raw, opts = {}) {
|
|
493
499
|
const homeDir = opts.homeDir ?? homedir();
|
|
500
|
+
warnUnknownFlags("mcp set", raw.unknown);
|
|
494
501
|
let plan;
|
|
495
502
|
try {
|
|
496
503
|
plan = collectMcpSetAuthPlan(name, raw, homeDir);
|
|
@@ -671,8 +678,16 @@ export async function applyMcpAddWorkspaceWithSync(plan, workspaceRoot, opts = {
|
|
|
671
678
|
}
|
|
672
679
|
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
673
680
|
}
|
|
681
|
+
/** Warn about `--*` tokens the parser didn't claim — a typo like
|
|
682
|
+
* `--tranport http` must not exit 0 in silence. */
|
|
683
|
+
function warnUnknownFlags(commandLabel, unknown) {
|
|
684
|
+
if (unknown && unknown.length > 0) {
|
|
685
|
+
console.error(`warning: ${commandLabel}: unrecognized flag(s) ${unknown.map((f) => `"${f}"`).join(", ")} — ignored`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
674
688
|
export async function runMcpAdd(name, raw, opts = {}) {
|
|
675
689
|
const homeDir = opts.homeDir ?? homedir();
|
|
690
|
+
warnUnknownFlags("mcp add", raw.unknown);
|
|
676
691
|
if (opts.workspace) {
|
|
677
692
|
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
678
693
|
if (!workspaceRoot) {
|
|
@@ -767,8 +782,65 @@ export async function applyMcpRemoveWithSync(plan, opts = {}) {
|
|
|
767
782
|
}
|
|
768
783
|
return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
|
|
769
784
|
}
|
|
785
|
+
/** Workspace twin of `collectMcpRemovePlan`: presence is judged against
|
|
786
|
+
* `.agents/config.json` only — a global entry of the same name is this
|
|
787
|
+
* entry's shadow target, not a reason to touch the global file. */
|
|
788
|
+
export function collectMcpRemoveWorkspacePlan(name, workspaceRoot) {
|
|
789
|
+
const config = readWorkspaceConfigJson(workspaceRoot);
|
|
790
|
+
return { name, action: config.mcp?.servers?.[name] ? "removed" : "not-found" };
|
|
791
|
+
}
|
|
792
|
+
/** Workspace twin of `applyMcpRemoveWithSync`: deletes the entry from
|
|
793
|
+
* `.agents/config.json` (backup-session wrapped), then cascades the same
|
|
794
|
+
* mcp sync — run inside the workspace, so `planWorkspaceMcp` sees the
|
|
795
|
+
* entry is gone and removes the rendered project-level copies (the
|
|
796
|
+
* ledger-gated propagation already exists; this command was the missing
|
|
797
|
+
* corner). A shadowed global entry simply takes effect again. */
|
|
798
|
+
export async function applyMcpRemoveWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
799
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
800
|
+
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "mcp-remove") : undefined;
|
|
801
|
+
const session = opts.backupSession ?? ownSession;
|
|
802
|
+
if (!opts.dryRun && plan.action === "removed") {
|
|
803
|
+
removeWorkspaceServer(workspaceRoot, plan.name, session);
|
|
804
|
+
}
|
|
805
|
+
ownSession?.finalize();
|
|
806
|
+
const planFailed = plan.action === "not-found";
|
|
807
|
+
let syncReport;
|
|
808
|
+
if (!planFailed && existsSync(join(homeDir, ".trellis"))) {
|
|
809
|
+
syncReport = await collectMcpSyncReport({ homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
810
|
+
}
|
|
811
|
+
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
812
|
+
}
|
|
770
813
|
export async function runMcpRemove(name, opts = {}) {
|
|
771
814
|
const homeDir = opts.homeDir ?? homedir();
|
|
815
|
+
if (opts.workspace) {
|
|
816
|
+
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
817
|
+
if (!workspaceRoot) {
|
|
818
|
+
console.error("no .agents/ found here or above — run `trellis init --workspace` first (or pass --workspace from inside the repo)");
|
|
819
|
+
return { exitCode: 1 };
|
|
820
|
+
}
|
|
821
|
+
let plan;
|
|
822
|
+
try {
|
|
823
|
+
plan = collectMcpRemoveWorkspacePlan(name, workspaceRoot);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
827
|
+
return { exitCode: 1 };
|
|
828
|
+
}
|
|
829
|
+
const { sync: syncReport } = await applyMcpRemoveWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
830
|
+
if (opts.json) {
|
|
831
|
+
console.log(JSON.stringify(syncReport ? { ...plan, workspace: workspaceRoot, sync: syncReport } : { ...plan, workspace: workspaceRoot }, null, 2));
|
|
832
|
+
}
|
|
833
|
+
else if (plan.action === "not-found") {
|
|
834
|
+
console.error(`"${name}" is not in the workspace layer (${workspaceRoot}/.agents/config.json) — nothing to remove.`);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}removed MCP server "${name}" from ${workspaceRoot}/.agents/config.json.`);
|
|
838
|
+
if (syncReport)
|
|
839
|
+
printReport(syncReport, opts.dryRun ?? false);
|
|
840
|
+
}
|
|
841
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
842
|
+
return { exitCode: plan.action === "not-found" || syncConflict ? 1 : 0 };
|
|
843
|
+
}
|
|
772
844
|
let plan;
|
|
773
845
|
try {
|
|
774
846
|
plan = collectMcpRemovePlan(name, homeDir);
|
|
@@ -792,7 +864,22 @@ export async function runMcpRemove(name, opts = {}) {
|
|
|
792
864
|
console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
|
|
793
865
|
}
|
|
794
866
|
else if (plan.action === "not-found") {
|
|
795
|
-
|
|
867
|
+
// A name that lives only in the workspace layer used to get this same
|
|
868
|
+
// blunt message (found by the v0.15.0 real-machine smoke test) —
|
|
869
|
+
// point at the flag instead of implying the entry doesn't exist.
|
|
870
|
+
let workspaceHint = "";
|
|
871
|
+
const workspaceRoot = opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd());
|
|
872
|
+
if (workspaceRoot) {
|
|
873
|
+
try {
|
|
874
|
+
if (readWorkspaceConfigJson(workspaceRoot).mcp?.servers?.[name]) {
|
|
875
|
+
workspaceHint = ` here, but it exists in the workspace layer — re-run with --workspace to remove it from ${workspaceRoot}/.agents/config.json`;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// An unreadable workspace config must not mask the primary answer.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
console.error(`"${name}" is not a canonical MCP server${workspaceHint} — nothing to remove.`);
|
|
796
883
|
}
|
|
797
884
|
else if (writeError) {
|
|
798
885
|
console.error(` write failed: ${writeError}`);
|
|
@@ -28,16 +28,16 @@ import { AgentBridgeProvider } from "../lib/agentBridgeProvider.js";
|
|
|
28
28
|
import { RuntimeMemoryProvider } from "../lib/memoryProvider.js";
|
|
29
29
|
import { SkillProvider } from "../lib/skillProvider.js";
|
|
30
30
|
import { ALL_AGENTS, isOAuthAuth } from "../core/types.js";
|
|
31
|
+
import { flagValue } from "../lib/cliArgs.js";
|
|
31
32
|
const CLIENT_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
32
33
|
const SERVER_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
33
34
|
const RUNTIME_CLIENT_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
34
35
|
const RUNTIME_SERVER_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
35
36
|
export function parseMcpGatewayArgs(argv) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
37
|
+
const value = flagValue(argv, "--agent");
|
|
38
|
+
if (value === undefined) {
|
|
38
39
|
return { error: `mcp-gateway requires --agent <${ALL_AGENTS.join("|")}>` };
|
|
39
40
|
}
|
|
40
|
-
const value = argv[index + 1];
|
|
41
41
|
if (!ALL_AGENTS.includes(value)) {
|
|
42
42
|
return { error: `unknown agent "${value}" — expected one of ${ALL_AGENTS.join(", ")}` };
|
|
43
43
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI flag readers. Every command historically rolled its own
|
|
3
|
+
* `argv.indexOf("--flag")` lookup — which silently ignores the equally
|
|
4
|
+
* common `--flag=value` form (real incident, found by the v0.15.0
|
|
5
|
+
* real-machine smoke test: `mcp add --args=-y,…` exited 0 with the args
|
|
6
|
+
* silently dropped from canonical). These helpers accept both forms, and
|
|
7
|
+
* `unknownFlags` catches typos instead of letting them vanish.
|
|
8
|
+
*/
|
|
9
|
+
/** Value of `--name <value>` or `--name=<value>`; undefined when absent.
|
|
10
|
+
* The space form returns the next token verbatim (even one starting with
|
|
11
|
+
* `-`), matching the historical `indexOf` behavior. First occurrence wins,
|
|
12
|
+
* same as `indexOf`. */
|
|
13
|
+
export declare function flagValue(argv: readonly string[], name: string): string | undefined;
|
|
14
|
+
/** `--*` tokens none of `known` claims. Non-flag tokens (positionals,
|
|
15
|
+
* values) are never flags; a `--name=value` token is classified by its
|
|
16
|
+
* name part, so a known flag in `=` form is not "unknown". */
|
|
17
|
+
export declare function unknownFlags(argv: readonly string[], known: readonly string[]): string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI flag readers. Every command historically rolled its own
|
|
3
|
+
* `argv.indexOf("--flag")` lookup — which silently ignores the equally
|
|
4
|
+
* common `--flag=value` form (real incident, found by the v0.15.0
|
|
5
|
+
* real-machine smoke test: `mcp add --args=-y,…` exited 0 with the args
|
|
6
|
+
* silently dropped from canonical). These helpers accept both forms, and
|
|
7
|
+
* `unknownFlags` catches typos instead of letting them vanish.
|
|
8
|
+
*/
|
|
9
|
+
/** Value of `--name <value>` or `--name=<value>`; undefined when absent.
|
|
10
|
+
* The space form returns the next token verbatim (even one starting with
|
|
11
|
+
* `-`), matching the historical `indexOf` behavior. First occurrence wins,
|
|
12
|
+
* same as `indexOf`. */
|
|
13
|
+
export function flagValue(argv, name) {
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const token = argv[i];
|
|
16
|
+
if (token === name)
|
|
17
|
+
return argv[i + 1];
|
|
18
|
+
if (token.startsWith(`${name}=`))
|
|
19
|
+
return token.slice(name.length + 1);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** `--*` tokens none of `known` claims. Non-flag tokens (positionals,
|
|
24
|
+
* values) are never flags; a `--name=value` token is classified by its
|
|
25
|
+
* name part, so a known flag in `=` form is not "unknown". */
|
|
26
|
+
export function unknownFlags(argv, known) {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const token of argv) {
|
|
29
|
+
if (!token.startsWith("--"))
|
|
30
|
+
continue;
|
|
31
|
+
const key = token.split("=", 1)[0];
|
|
32
|
+
if (!known.includes(key))
|
|
33
|
+
result.push(token);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
@@ -27,7 +27,7 @@ export type ScopeSelection = {
|
|
|
27
27
|
detail: string;
|
|
28
28
|
};
|
|
29
29
|
export declare const SCOPE_SELECTOR_USAGE = "exactly one of --agents <ids>, --all, --none is required";
|
|
30
|
-
/**
|
|
30
|
+
/** Shared reader — accepts `--agents <ids>` and `--agents=<ids>`. */
|
|
31
31
|
export declare function parseScopeArgs(argv: readonly string[]): ScopeSelectorRaw;
|
|
32
32
|
export declare function resolveScopeSelection(raw: ScopeSelectorRaw, managed: readonly AgentId[]): ScopeSelection;
|
|
33
33
|
/** Order-insensitive equality between the scope on disk and the one about to
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* disagree about what a given selection means.
|
|
7
7
|
*/
|
|
8
8
|
import { ALL_AGENTS } from "../core/types.js";
|
|
9
|
+
import { flagValue } from "./cliArgs.js";
|
|
9
10
|
export const SCOPE_SELECTOR_USAGE = "exactly one of --agents <ids>, --all, --none is required";
|
|
10
|
-
/**
|
|
11
|
+
/** Shared reader — accepts `--agents <ids>` and `--agents=<ids>`. */
|
|
11
12
|
export function parseScopeArgs(argv) {
|
|
12
|
-
const
|
|
13
|
+
const agents = flagValue(argv, "--agents");
|
|
13
14
|
return {
|
|
14
|
-
...(
|
|
15
|
+
...(agents !== undefined ? { agents } : {}),
|
|
15
16
|
all: argv.includes("--all"),
|
|
16
17
|
none: argv.includes("--none"),
|
|
17
18
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Write-side helpers for the workspace layer (trellis-workspace-scope
|
|
3
3
|
* tasks 3.3/3.4): scaffolding `.agents/` (`init --workspace`) and
|
|
4
|
-
* upserting a server
|
|
4
|
+
* upserting/removing a server in `.agents/config.json`
|
|
5
|
+
* (`mcp add --workspace` / `mcp remove --workspace`).
|
|
5
6
|
*
|
|
6
7
|
* The read side lives in src/lib/workspace.ts (`loadWorkspaceLayer`) —
|
|
7
8
|
* same file, same schema, one validator (via `toServerDefYaml`, so a
|
|
@@ -31,6 +32,11 @@ export declare function writeWorkspaceConfigJson(workspaceRoot: string, config:
|
|
|
31
32
|
* the shared YAML/JSON translator. `version` is filled in when absent —
|
|
32
33
|
* an existing file's version (and any unknown keys) is preserved. */
|
|
33
34
|
export declare function upsertWorkspaceServer(workspaceRoot: string, name: string, def: McpServerDef, backup?: BackupSession): void;
|
|
35
|
+
/** Delete `mcp.servers.<name>`, preserving every other key (unknown keys
|
|
36
|
+
* included, same posture as the upsert). Returns false — and writes
|
|
37
|
+
* nothing — when the entry isn't there. The follow-up `mcp sync` is what
|
|
38
|
+
* propagates the deletion to the rendered project-level files. */
|
|
39
|
+
export declare function removeWorkspaceServer(workspaceRoot: string, name: string, backup?: BackupSession): boolean;
|
|
34
40
|
export interface WorkspaceScaffoldFile {
|
|
35
41
|
path: string;
|
|
36
42
|
action: "create" | "already-present";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Write-side helpers for the workspace layer (trellis-workspace-scope
|
|
3
3
|
* tasks 3.3/3.4): scaffolding `.agents/` (`init --workspace`) and
|
|
4
|
-
* upserting a server
|
|
4
|
+
* upserting/removing a server in `.agents/config.json`
|
|
5
|
+
* (`mcp add --workspace` / `mcp remove --workspace`).
|
|
5
6
|
*
|
|
6
7
|
* The read side lives in src/lib/workspace.ts (`loadWorkspaceLayer`) —
|
|
7
8
|
* same file, same schema, one validator (via `toServerDefYaml`, so a
|
|
@@ -59,6 +60,24 @@ export function upsertWorkspaceServer(workspaceRoot, name, def, backup) {
|
|
|
59
60
|
};
|
|
60
61
|
writeWorkspaceConfigJson(workspaceRoot, next, backup);
|
|
61
62
|
}
|
|
63
|
+
/** Delete `mcp.servers.<name>`, preserving every other key (unknown keys
|
|
64
|
+
* included, same posture as the upsert). Returns false — and writes
|
|
65
|
+
* nothing — when the entry isn't there. The follow-up `mcp sync` is what
|
|
66
|
+
* propagates the deletion to the rendered project-level files. */
|
|
67
|
+
export function removeWorkspaceServer(workspaceRoot, name, backup) {
|
|
68
|
+
const config = readWorkspaceConfigJson(workspaceRoot);
|
|
69
|
+
const servers = config.mcp?.servers;
|
|
70
|
+
if (!servers || !(name in servers))
|
|
71
|
+
return false;
|
|
72
|
+
const nextServers = { ...servers };
|
|
73
|
+
delete nextServers[name];
|
|
74
|
+
const next = {
|
|
75
|
+
...config,
|
|
76
|
+
mcp: { ...(config.mcp ?? {}), servers: nextServers },
|
|
77
|
+
};
|
|
78
|
+
writeWorkspaceConfigJson(workspaceRoot, next, backup);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
62
81
|
/** Exported for `migrate --workspace`: a `.agents/AGENTS.md` holding
|
|
63
82
|
* exactly this template is treated as still-empty scaffolding that an
|
|
64
83
|
* instructions import may replace, not as user content to conflict on. */
|
|
@@ -20,6 +20,19 @@ export interface WorkspaceUnmigratedHint {
|
|
|
20
20
|
/** Entry names present in the file with no Trellis manifest record. */
|
|
21
21
|
entries: string[];
|
|
22
22
|
}
|
|
23
|
+
/** One parse-level failure when inspecting an agent's project MCP file.
|
|
24
|
+
* Distinct from `parse-diagnostic` (agent-global probes). Even when Trellis
|
|
25
|
+
* has never rendered into this file, a project file that breaks the agent's
|
|
26
|
+
* own startup is exactly what doctor exists to surface
|
|
27
|
+
* (trellis-doctor-workspace-toml-validation). */
|
|
28
|
+
export interface WorkspaceTomlFinding {
|
|
29
|
+
agent: AgentId;
|
|
30
|
+
path: string;
|
|
31
|
+
/** Parser's 1-based line and column of the failure, when known. */
|
|
32
|
+
line?: number;
|
|
33
|
+
column?: number;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
23
36
|
export interface WorkspaceDoctorSection {
|
|
24
37
|
root: string;
|
|
25
38
|
mcp: WorkspaceLayerEntry[];
|
|
@@ -38,6 +51,8 @@ export interface WorkspaceDoctorSection {
|
|
|
38
51
|
name: string;
|
|
39
52
|
}[];
|
|
40
53
|
unmigrated: WorkspaceUnmigratedHint[];
|
|
54
|
+
/** Parse-level failures on workspace agent MCP files (usually empty). */
|
|
55
|
+
tomlFindings: WorkspaceTomlFinding[];
|
|
41
56
|
/** zcode's user-level-first deviation notices (empty otherwise). */
|
|
42
57
|
precedenceWarnings: string[];
|
|
43
58
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* from the already-loaded canonical source and file reads only.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { parse as parseToml } from "smol-toml";
|
|
13
14
|
import { resolveSecretEnv } from "./secretEnv.js";
|
|
14
15
|
import { loadWorkspaceRendered, workspaceMcpConfigPath, workspacePrecedenceWarnings } from "./workspaceMcp.js";
|
|
15
16
|
const VAR_REF_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
@@ -36,15 +37,36 @@ function collectVarRefs(canonical) {
|
|
|
36
37
|
return refs;
|
|
37
38
|
}
|
|
38
39
|
/** Entry names a JSON `mcpServers` file / Codex TOML / zcode nested
|
|
39
|
-
* config holds beyond what the in-repo manifest records Trellis
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* config holds beyond what the in-repo manifest records Trellis rendering.
|
|
41
|
+
*
|
|
42
|
+
* The Codex branch TOML-parses the file first — syntactically invalid TOML
|
|
43
|
+
* is indistinguishable from valid TOML to the section-header regex, but is
|
|
44
|
+
* fatal to Codex's own startup, so it is surfaced as `invalid` and never
|
|
45
|
+
* turned into an unmigrated hint. The parsed AST is discarded on success:
|
|
46
|
+
* header extraction keeps using the existing regex to avoid changing edge
|
|
47
|
+
* semantics (dotted quoted keys, single-quoted keys) that the regex was
|
|
48
|
+
* already validated against — smol-toml is used parse-only, never
|
|
49
|
+
* round-tripped, respecting lib/tomlSection.ts's write-path constraint. */
|
|
42
50
|
function unmarkedEntries(agentId, configPath, recorded) {
|
|
43
51
|
if (!existsSync(configPath))
|
|
44
|
-
return [];
|
|
52
|
+
return { kind: "ok", names: [] };
|
|
45
53
|
let names = [];
|
|
46
54
|
if (agentId === "codex") {
|
|
47
55
|
const content = readFileSync(configPath, "utf-8");
|
|
56
|
+
try {
|
|
57
|
+
parseToml(content);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const tomLError = err;
|
|
61
|
+
const reason = tomLError.message?.split("\n")[0] ?? String(err);
|
|
62
|
+
return {
|
|
63
|
+
kind: "invalid",
|
|
64
|
+
path: configPath,
|
|
65
|
+
...(typeof tomLError.line === "number" ? { line: tomLError.line } : {}),
|
|
66
|
+
...(typeof tomLError.column === "number" ? { column: tomLError.column + 1 } : {}),
|
|
67
|
+
reason,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
48
70
|
const re = /^\[mcp_servers\.([^\]]+)\]\s*$/gm;
|
|
49
71
|
let match;
|
|
50
72
|
while ((match = re.exec(content)) !== null)
|
|
@@ -59,10 +81,10 @@ function unmarkedEntries(agentId, configPath, recorded) {
|
|
|
59
81
|
names = Object.keys(servers);
|
|
60
82
|
}
|
|
61
83
|
catch {
|
|
62
|
-
return []; // unparsable file is the sync guard's finding, not doctor's
|
|
84
|
+
return { kind: "ok", names: [] }; // unparsable file is the sync guard's finding, not doctor's
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
return names.filter((name) => recorded[name] === undefined);
|
|
87
|
+
return { kind: "ok", names: names.filter((name) => recorded[name] === undefined) };
|
|
66
88
|
}
|
|
67
89
|
export function collectWorkspaceDoctorSection(canonical) {
|
|
68
90
|
const workspace = canonical.workspace;
|
|
@@ -83,12 +105,17 @@ export function collectWorkspaceDoctorSection(canonical) {
|
|
|
83
105
|
const unresolvedVars = refs.filter((r) => resolved[r.name] === undefined);
|
|
84
106
|
const manifest = loadWorkspaceRendered(workspace.root);
|
|
85
107
|
const unmigrated = [];
|
|
108
|
+
const tomlFindings = [];
|
|
86
109
|
for (const agentId of canonical.managedAgents) {
|
|
87
110
|
const configPath = workspaceMcpConfigPath(agentId, workspace.root);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
111
|
+
const result = unmarkedEntries(agentId, configPath, manifest.mcp?.[agentId] ?? {});
|
|
112
|
+
if (result.kind === "invalid") {
|
|
113
|
+
tomlFindings.push({ agent: agentId, path: result.path, ...(result.line !== undefined ? { line: result.line } : {}), ...(result.column !== undefined ? { column: result.column } : {}), reason: result.reason });
|
|
114
|
+
continue; // never report an unparseable file's entries as unmigrated — spec req 2
|
|
115
|
+
}
|
|
116
|
+
if (result.names.length > 0)
|
|
117
|
+
unmigrated.push({ agent: agentId, path: configPath, entries: result.names });
|
|
91
118
|
}
|
|
92
119
|
const precedenceWarnings = canonical.managedAgents.flatMap((id) => workspacePrecedenceWarnings(id, canonical));
|
|
93
|
-
return { root: workspace.root, mcp, skills, shadowed: workspace.shadowed, tombstoned, unresolvedVars, unmigrated, precedenceWarnings };
|
|
120
|
+
return { root: workspace.root, mcp, skills, shadowed: workspace.shadowed, tombstoned, unresolvedVars, unmigrated, tomlFindings, precedenceWarnings };
|
|
94
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
66
66
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
67
|
+
"smol-toml": "^1.9.1",
|
|
67
68
|
"yaml": "^2.9.1",
|
|
68
69
|
"zod": "^4.6.5"
|
|
69
70
|
}
|