agent-trellis 0.15.0 → 0.15.1

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 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: dedupIndex >= 0 ? rest[dedupIndex + 1] : undefined,
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 fromIndex = rest.indexOf("--from");
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 fromIndex = skillRest.indexOf("--from");
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;
@@ -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, same posture as every other
124
- * command's own inline flag parsing in src/cli.ts. */
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
  }>;
@@ -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 flag = (name) => {
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: flag("--transport"),
242
- auth: flag("--auth"),
243
- command: flag("--command"),
244
- args: flag("--args"),
245
- url: flag("--url"),
246
- headers: flag("--headers"),
247
- env: flag("--env"),
248
- staticEnv: flag("--static-env"),
249
- agents: flag("--agents"),
250
- enabled: flag("--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 flag = (name) => {
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: flag("--auth"),
260
- clientId: flag("--client-id"),
261
- clientSecretEnv: flag("--client-secret-env"),
262
- authOwner: flag("--auth-owner"),
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
- console.error(`"${name}" is not a canonical MCP server — nothing to remove.`);
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 index = argv.indexOf("--agent");
37
- if (index === -1 || index === argv.length - 1) {
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
- /** `argv.indexOf`-style reader, same idiom as every other command's flags. */
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
- /** `argv.indexOf`-style reader, same idiom as every other command's flags. */
11
+ /** Shared reader — accepts `--agents <ids>` and `--agents=<ids>`. */
11
12
  export function parseScopeArgs(argv) {
12
- const i = argv.indexOf("--agents");
13
+ const agents = flagValue(argv, "--agents");
13
14
  return {
14
- ...(i >= 0 ? { agents: argv[i + 1] ?? "" } : {}),
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 into `.agents/config.json` (`mcp add --workspace`).
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 into `.agents/config.json` (`mcp add --workspace`).
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trellis",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
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",