llm-cli-gateway 2.2.0 → 2.4.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.
@@ -10,6 +10,34 @@ const PERMISSION_MODES = [
10
10
  "bypassPermissions",
11
11
  ];
12
12
  const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
13
+ function scFlag(name, arity = "optional") {
14
+ return [
15
+ name,
16
+ {
17
+ arity,
18
+ description: `Subcommand-local ${name} option tracked for help-surface drift only`,
19
+ },
20
+ ];
21
+ }
22
+ function scFlags(flags, arityOverrides = {}) {
23
+ return Object.fromEntries(flags.map(flag => scFlag(flag, arityOverrides[flag] ?? "optional")));
24
+ }
25
+ function subcommand(commandPath, summary, risk, flags = [], options = {}) {
26
+ return {
27
+ commandPath,
28
+ helpArgs: [["--help"]],
29
+ flags: scFlags(flags, options.flagArities),
30
+ maxPositionals: options.maxPositionals ?? 0,
31
+ aliases: options.aliases ?? [],
32
+ children: options.children ?? {},
33
+ risk,
34
+ exposure: options.exposure ?? "tracked_only",
35
+ tier: options.tier ?? "catalog",
36
+ tokenCost: options.tokenCost ?? "small",
37
+ summary,
38
+ conformanceFixtures: options.fixtures ?? [],
39
+ };
40
+ }
13
41
  export const UPSTREAM_CLI_CONTRACTS = {
14
42
  claude: {
15
43
  cli: "claude",
@@ -23,6 +51,57 @@ export const UPSTREAM_CLI_CONTRACTS = {
23
51
  watchCategories: ["flags", "output-formats", "permission-modes", "session-resume", "models"],
24
52
  },
25
53
  helpArgs: [["--help"]],
54
+ subcommands: {
55
+ doctor: subcommand(["doctor"], "Run Claude Code diagnostic checks.", "read_only", [], {
56
+ tier: "diagnostic",
57
+ }),
58
+ mcp: subcommand(["mcp"], "Manage Claude MCP server configuration.", "writes_local_config"),
59
+ plugin: subcommand(["plugin"], "Manage Claude plugins.", "writes_local_config", [], {
60
+ aliases: ["plugins"],
61
+ }),
62
+ plugins: subcommand(["plugins"], "Alias for Claude plugin management.", "writes_local_config", [], {
63
+ aliases: ["plugin"],
64
+ }),
65
+ agents: subcommand(["agents"], "Inspect and manage Claude agent definitions.", "writes_local_config", [
66
+ "--add-dir",
67
+ "--agent",
68
+ "--allow-dangerously-skip-permissions",
69
+ "--cwd",
70
+ "--dangerously-skip-permissions",
71
+ "--effort",
72
+ "--json",
73
+ "--mcp-config",
74
+ "--model",
75
+ "--permission-mode",
76
+ "--plugin-dir",
77
+ "--setting-sources",
78
+ "--settings",
79
+ "--strict-mcp-config",
80
+ ], { tier: "inspect", flagArities: { "--json": "none", "--strict-mcp-config": "none" } }),
81
+ auth: subcommand(["auth"], "Manage Claude authentication state.", "auth", [], {
82
+ exposure: "not_exposed",
83
+ }),
84
+ project: subcommand(["project"], "Manage Claude project configuration.", "writes_local_config"),
85
+ update: subcommand(["update"], "Update the Claude Code binary.", "updates_binary", [], {
86
+ exposure: "not_exposed",
87
+ }),
88
+ upgrade: subcommand(["upgrade"], "Alias for updating the Claude Code binary.", "updates_binary", [], {
89
+ exposure: "not_exposed",
90
+ }),
91
+ install: subcommand(["install"], "Install Claude Code shell integrations.", "writes_local_config", ["--force"], {
92
+ exposure: "not_exposed",
93
+ flagArities: { "--force": "none" },
94
+ }),
95
+ "auto-mode": subcommand(["auto-mode"], "Configure Claude auto-mode behavior.", "writes_local_config"),
96
+ ultrareview: subcommand(["ultrareview"], "Run Claude ultrareview diagnostics.", "executes_agent", ["--json", "--timeout"], {
97
+ tier: "diagnostic",
98
+ tokenCost: "medium",
99
+ flagArities: { "--json": "none" },
100
+ }),
101
+ "setup-token": subcommand(["setup-token"], "Configure Claude setup token authentication.", "auth", [], {
102
+ exposure: "not_exposed",
103
+ }),
104
+ },
26
105
  maxPositionals: 0,
27
106
  mcpTools: ["claude_request", "claude_request_async"],
28
107
  mcpParameters: [
@@ -282,6 +361,206 @@ export const UPSTREAM_CLI_CONTRACTS = {
282
361
  ["exec", "--help"],
283
362
  ["exec", "resume", "--help"],
284
363
  ],
364
+ subcommands: {
365
+ exec: subcommand(["exec"], "Run Codex in non-interactive execution mode.", "executes_agent", [
366
+ "--add-dir",
367
+ "--cd",
368
+ "--color",
369
+ "--config",
370
+ "--dangerously-bypass-approvals-and-sandbox",
371
+ "--dangerously-bypass-hook-trust",
372
+ "--disable",
373
+ "--enable",
374
+ "--ephemeral",
375
+ "--ignore-rules",
376
+ "--ignore-user-config",
377
+ "--image",
378
+ "--json",
379
+ "--local-provider",
380
+ "--model",
381
+ "--oss",
382
+ "--output-last-message",
383
+ "--output-schema",
384
+ "--profile",
385
+ "--sandbox",
386
+ "--skip-git-repo-check",
387
+ "--strict-config",
388
+ "--version",
389
+ ]),
390
+ review: subcommand(["review"], "Run Codex code review workflows.", "executes_agent", [
391
+ "--base",
392
+ "--commit",
393
+ "--config",
394
+ "--disable",
395
+ "--enable",
396
+ "--strict-config",
397
+ "--title",
398
+ "--uncommitted",
399
+ ]),
400
+ login: subcommand(["login"], "Authenticate Codex CLI.", "auth", [
401
+ "--config",
402
+ "--device-auth",
403
+ "--disable",
404
+ "--enable",
405
+ "--with-access-token",
406
+ "--with-api-key",
407
+ ], { exposure: "not_exposed" }),
408
+ logout: subcommand(["logout"], "Clear Codex authentication state.", "auth", ["--config", "--disable", "--enable"], { exposure: "not_exposed" }),
409
+ mcp: subcommand(["mcp"], "Manage Codex MCP configuration.", "writes_local_config", [
410
+ "--config",
411
+ "--disable",
412
+ "--enable",
413
+ ]),
414
+ plugin: subcommand(["plugin"], "Manage Codex plugins.", "writes_local_config", [
415
+ "--config",
416
+ "--disable",
417
+ "--enable",
418
+ ]),
419
+ "mcp-server": subcommand(["mcp-server"], "Start Codex MCP server mode.", "starts_server", ["--config", "--disable", "--enable", "--strict-config"], { exposure: "not_exposed" }),
420
+ "app-server": subcommand(["app-server"], "Start Codex app server mode.", "starts_server", [
421
+ "--analytics-default-enabled",
422
+ "--config",
423
+ "--disable",
424
+ "--enable",
425
+ "--listen",
426
+ "--stdio",
427
+ "--strict-config",
428
+ "--ws-audience",
429
+ "--ws-auth",
430
+ "--ws-issuer",
431
+ "--ws-max-clock-skew-seconds",
432
+ "--ws-shared-secret-file",
433
+ "--ws-token-file",
434
+ "--ws-token-sha256",
435
+ ], { exposure: "not_exposed" }),
436
+ "remote-control": subcommand(["remote-control"], "Inspect or manage Codex remote control state.", "network", ["--config", "--disable", "--enable", "--json"]),
437
+ completion: subcommand(["completion"], "Generate Codex shell completions.", "read_only", ["--config", "--disable", "--enable"], { tier: "inspect" }),
438
+ update: subcommand(["update"], "Update the Codex CLI binary.", "updates_binary", ["--config", "--disable", "--enable"], { exposure: "not_exposed" }),
439
+ doctor: subcommand(["doctor"], "Run Codex diagnostic checks.", "read_only", [
440
+ "--all",
441
+ "--ascii",
442
+ "--config",
443
+ "--disable",
444
+ "--enable",
445
+ "--json",
446
+ "--no-color",
447
+ "--summary",
448
+ ], { tier: "diagnostic" }),
449
+ sandbox: subcommand(["sandbox"], "Run or inspect Codex sandbox behavior.", "executes_agent", [
450
+ "--cd",
451
+ "--config",
452
+ "--disable",
453
+ "--enable",
454
+ "--include-managed-config",
455
+ "--permissions-profile",
456
+ "--profile",
457
+ ], { exposure: "not_exposed" }),
458
+ debug: subcommand(["debug"], "Run Codex debugging utilities.", "read_only", ["--config", "--disable", "--enable"], { tier: "diagnostic" }),
459
+ apply: subcommand(["apply"], "Apply a Codex patch to the workspace.", "destructive", ["--config", "--disable", "--enable"], { exposure: "not_exposed" }),
460
+ resume: subcommand(["resume"], "Resume Codex sessions from the interactive CLI.", "executes_agent", [
461
+ "--add-dir",
462
+ "--all",
463
+ "--ask-for-approval",
464
+ "--cd",
465
+ "--config",
466
+ "--dangerously-bypass-approvals-and-sandbox",
467
+ "--dangerously-bypass-hook-trust",
468
+ "--disable",
469
+ "--enable",
470
+ "--image",
471
+ "--include-non-interactive",
472
+ "--last",
473
+ "--local-provider",
474
+ "--model",
475
+ "--no-alt-screen",
476
+ "--oss",
477
+ "--profile",
478
+ "--remote",
479
+ "--remote-auth-token-env",
480
+ "--sandbox",
481
+ "--search",
482
+ "--strict-config",
483
+ "--version",
484
+ ]),
485
+ archive: subcommand(["archive"], "Archive Codex session state.", "writes_local_config", [
486
+ "--add-dir",
487
+ "--cd",
488
+ "--config",
489
+ "--dangerously-bypass-approvals-and-sandbox",
490
+ "--dangerously-bypass-hook-trust",
491
+ "--disable",
492
+ "--enable",
493
+ "--image",
494
+ "--local-provider",
495
+ "--model",
496
+ "--oss",
497
+ "--profile",
498
+ "--remote",
499
+ "--remote-auth-token-env",
500
+ "--sandbox",
501
+ "--strict-config",
502
+ ], { exposure: "not_exposed" }),
503
+ unarchive: subcommand(["unarchive"], "Restore archived Codex session state.", "writes_local_config", [
504
+ "--add-dir",
505
+ "--cd",
506
+ "--config",
507
+ "--dangerously-bypass-approvals-and-sandbox",
508
+ "--dangerously-bypass-hook-trust",
509
+ "--disable",
510
+ "--enable",
511
+ "--image",
512
+ "--local-provider",
513
+ "--model",
514
+ "--oss",
515
+ "--profile",
516
+ "--remote",
517
+ "--remote-auth-token-env",
518
+ "--sandbox",
519
+ "--strict-config",
520
+ ], { exposure: "not_exposed" }),
521
+ fork: subcommand(["fork"], "Fork a Codex session.", "executes_agent", [
522
+ "--add-dir",
523
+ "--all",
524
+ "--ask-for-approval",
525
+ "--cd",
526
+ "--config",
527
+ "--dangerously-bypass-approvals-and-sandbox",
528
+ "--dangerously-bypass-hook-trust",
529
+ "--disable",
530
+ "--enable",
531
+ "--image",
532
+ "--last",
533
+ "--local-provider",
534
+ "--model",
535
+ "--no-alt-screen",
536
+ "--oss",
537
+ "--profile",
538
+ "--remote",
539
+ "--remote-auth-token-env",
540
+ "--sandbox",
541
+ "--search",
542
+ "--strict-config",
543
+ "--version",
544
+ ]),
545
+ cloud: subcommand(["cloud"], "Inspect or manage Codex cloud features.", "network", [
546
+ "--config",
547
+ "--disable",
548
+ "--enable",
549
+ "--version",
550
+ ]),
551
+ "exec-server": subcommand(["exec-server"], "Start Codex exec server mode.", "starts_server", [
552
+ "--config",
553
+ "--disable",
554
+ "--enable",
555
+ "--environment-id",
556
+ "--listen",
557
+ "--name",
558
+ "--remote",
559
+ "--strict-config",
560
+ "--use-agent-identity-auth",
561
+ ], { exposure: "not_exposed" }),
562
+ features: subcommand(["features"], "Inspect or configure Codex feature flags.", "writes_local_config", ["--config", "--disable", "--enable"]),
563
+ },
285
564
  command: { requiredFirstArg: "exec", optionalSecondArg: "resume" },
286
565
  maxPositionals: 1,
287
566
  resumeMaxPositionals: 2,
@@ -504,6 +783,38 @@ export const UPSTREAM_CLI_CONTRACTS = {
504
783
  watchCategories: ["flags", "approval-modes", "output-formats", "session-resume"],
505
784
  },
506
785
  helpArgs: [["--help"]],
786
+ subcommands: {
787
+ mcp: subcommand(["mcp"], "Manage Gemini MCP server configuration.", "writes_local_config", ["--debug"], {
788
+ flagArities: { "--debug": "none" },
789
+ }),
790
+ extensions: subcommand(["extensions"], "Manage Gemini extensions.", "writes_local_config", ["--debug"], {
791
+ aliases: ["extension"],
792
+ flagArities: { "--debug": "none" },
793
+ }),
794
+ extension: subcommand(["extension"], "Alias for Gemini extension management.", "writes_local_config", ["--debug"], {
795
+ aliases: ["extensions"],
796
+ flagArities: { "--debug": "none" },
797
+ }),
798
+ skills: subcommand(["skills"], "Manage Gemini skills.", "writes_local_config", ["--debug"], {
799
+ aliases: ["skill"],
800
+ flagArities: { "--debug": "none" },
801
+ }),
802
+ skill: subcommand(["skill"], "Alias for Gemini skill management.", "writes_local_config", ["--debug"], {
803
+ aliases: ["skills"],
804
+ flagArities: { "--debug": "none" },
805
+ }),
806
+ hooks: subcommand(["hooks"], "Manage Gemini hooks.", "writes_local_config", ["--debug"], {
807
+ aliases: ["hook"],
808
+ flagArities: { "--debug": "none" },
809
+ }),
810
+ hook: subcommand(["hook"], "Alias for Gemini hook management.", "writes_local_config", ["--debug"], {
811
+ aliases: ["hooks"],
812
+ flagArities: { "--debug": "none" },
813
+ }),
814
+ gemma: subcommand(["gemma"], "Run Gemini Gemma local-model helper surfaces.", "executes_agent", ["--debug"], {
815
+ flagArities: { "--debug": "none" },
816
+ }),
817
+ },
507
818
  maxPositionals: 0,
508
819
  mcpTools: ["gemini_request", "gemini_request_async"],
509
820
  mcpParameters: [
@@ -625,6 +936,88 @@ export const UPSTREAM_CLI_CONTRACTS = {
625
936
  watchCategories: ["flags", "permission-modes", "session-resume", "sandbox", "output-formats"],
626
937
  },
627
938
  helpArgs: [["--help"]],
939
+ subcommands: {
940
+ agent: subcommand(["agent"], "Run Grok agent service helpers.", "executes_agent", [
941
+ "--agent-profile",
942
+ "--always-approve",
943
+ "--cli-chat-proxy-base-url",
944
+ "--grok-ws-origin",
945
+ "--grok-ws-url",
946
+ "--leader",
947
+ "--leader-socket",
948
+ "--model",
949
+ "--no-leader",
950
+ "--reasoning-effort",
951
+ "--reauth",
952
+ "--xai-api-base-url",
953
+ ], {
954
+ children: {
955
+ stdio: subcommand(["agent", "stdio"], "Run Grok agent stdio mode.", "starts_server", ["--leader-socket"], { exposure: "not_exposed" }),
956
+ headless: subcommand(["agent", "headless"], "Run Grok headless agent mode.", "executes_agent", ["--grok-ws-origin", "--grok-ws-url", "--leader-socket"]),
957
+ serve: subcommand(["agent", "serve"], "Start Grok agent server mode.", "starts_server", [
958
+ "--bind",
959
+ "--grok-ws-origin",
960
+ "--grok-ws-url",
961
+ "--leader-socket",
962
+ "--remote",
963
+ "--secret",
964
+ ], { exposure: "not_exposed" }),
965
+ leader: subcommand(["agent", "leader"], "Start Grok agent leader mode.", "starts_server", [
966
+ "--grok-ws-origin",
967
+ "--grok-ws-url",
968
+ "--leader-socket",
969
+ "--no-auto-update",
970
+ "--no-exit-on-disconnect",
971
+ ], { exposure: "not_exposed" }),
972
+ },
973
+ }),
974
+ completions: subcommand(["completions"], "Generate Grok shell completions.", "read_only", ["--leader-socket"], { tier: "inspect" }),
975
+ export: subcommand(["export"], "Export Grok session data.", "read_only", ["--clipboard", "--leader-socket"], { tier: "inspect" }),
976
+ import: subcommand(["import"], "Import Grok session data.", "writes_local_config", [
977
+ "--json",
978
+ "--leader-socket",
979
+ "--list",
980
+ ]),
981
+ inspect: subcommand(["inspect"], "Inspect Grok local state.", "read_only", ["--json", "--leader-socket"], { tier: "inspect" }),
982
+ leader: subcommand(["leader"], "Manage Grok leader process.", "starts_server", ["--leader-socket"], {
983
+ exposure: "not_exposed",
984
+ }),
985
+ login: subcommand(["login"], "Authenticate Grok CLI.", "auth", ["--device-auth", "--leader-socket", "--oauth"], { exposure: "not_exposed" }),
986
+ logout: subcommand(["logout"], "Clear Grok authentication state.", "auth", ["--leader-socket"], {
987
+ exposure: "not_exposed",
988
+ }),
989
+ mcp: subcommand(["mcp"], "Manage Grok MCP configuration.", "writes_local_config", [
990
+ "--leader-socket",
991
+ ]),
992
+ memory: subcommand(["memory"], "Manage Grok memory state.", "writes_local_config", [
993
+ "--leader-socket",
994
+ ]),
995
+ models: subcommand(["models"], "Inspect Grok model catalog.", "network", ["--leader-socket"], {
996
+ tier: "diagnostic",
997
+ }),
998
+ plugin: subcommand(["plugin"], "Manage Grok plugins.", "writes_local_config", [
999
+ "--leader-socket",
1000
+ ]),
1001
+ sessions: subcommand(["sessions"], "Inspect Grok sessions.", "read_only", ["--leader-socket"], {
1002
+ tier: "inspect",
1003
+ }),
1004
+ setup: subcommand(["setup"], "Configure Grok CLI local setup.", "writes_local_config", ["--leader-socket"], { exposure: "not_exposed" }),
1005
+ ssh: subcommand(["ssh"], "Manage Grok SSH integration.", "network", ["--leader-socket"]),
1006
+ trace: subcommand(["trace"], "Inspect Grok trace data.", "read_only", ["--json", "--leader-socket", "--local", "--output"], { tier: "diagnostic" }),
1007
+ update: subcommand(["update"], "Update the Grok CLI binary.", "updates_binary", [
1008
+ "--alpha",
1009
+ "--check",
1010
+ "--force-reinstall",
1011
+ "--json",
1012
+ "--leader-socket",
1013
+ "--stable",
1014
+ "--version",
1015
+ ], { exposure: "not_exposed" }),
1016
+ version: subcommand(["version"], "Print Grok version information.", "read_only", ["--json", "--leader-socket"], { tier: "diagnostic" }),
1017
+ worktree: subcommand(["worktree"], "Manage Grok worktree sessions.", "writes_local_config", [
1018
+ "--leader-socket",
1019
+ ]),
1020
+ },
628
1021
  maxPositionals: 0,
629
1022
  mcpTools: ["grok_request", "grok_request_async"],
630
1023
  mcpParameters: [
@@ -931,6 +1324,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
931
1324
  watchCategories: ["flags", "agent-modes", "session-logging", "output-formats", "env-model"],
932
1325
  },
933
1326
  helpArgs: [["--help"]],
1327
+ subcommands: {},
934
1328
  maxPositionals: 0,
935
1329
  mcpTools: ["mistral_request", "mistral_request_async"],
936
1330
  mcpParameters: [
@@ -1232,6 +1626,210 @@ export function assertUpstreamCliArgs(cli, args) {
1232
1626
  throw new Error(`Upstream ${cli} CLI contract violation: ${details}`);
1233
1627
  }
1234
1628
  }
1629
+ function subcommandKey(commandPath) {
1630
+ return commandPath.join(" ");
1631
+ }
1632
+ function subcommandResourceUri(cli, commandPath) {
1633
+ return `provider-subcommands://${cli}/${commandPath.map(encodeURIComponent).join("/")}`;
1634
+ }
1635
+ export function flattenCliSubcommands(subcommands) {
1636
+ const flattened = [];
1637
+ const visit = (node) => {
1638
+ flattened.push(node);
1639
+ for (const child of Object.values(node.children ?? {}))
1640
+ visit(child);
1641
+ };
1642
+ for (const node of Object.values(subcommands ?? {}))
1643
+ visit(node);
1644
+ return flattened.sort((a, b) => subcommandKey(a.commandPath).localeCompare(subcommandKey(b.commandPath)));
1645
+ }
1646
+ export function getCliSubcommandContract(cli, commandPath) {
1647
+ const wanted = subcommandKey(commandPath);
1648
+ return (flattenCliSubcommands(UPSTREAM_CLI_CONTRACTS[cli].subcommands).find(contract => subcommandKey(contract.commandPath) === wanted) ?? null);
1649
+ }
1650
+ function serializeFlagContract(flag) {
1651
+ return {
1652
+ arity: flag.arity,
1653
+ values: flag.values ?? null,
1654
+ pattern: flag.pattern?.source ?? null,
1655
+ description: flag.description,
1656
+ hiddenFromHelp: flag.hiddenFromHelp ?? false,
1657
+ };
1658
+ }
1659
+ export function serializeCliSubcommandContract(cli, contract) {
1660
+ return {
1661
+ provider: cli,
1662
+ commandPath: contract.commandPath,
1663
+ helpArgs: contract.helpArgs,
1664
+ flags: Object.fromEntries(Object.entries(contract.flags).map(([name, flag]) => [name, serializeFlagContract(flag)])),
1665
+ maxPositionals: contract.maxPositionals,
1666
+ aliases: contract.aliases ?? [],
1667
+ children: Object.values(contract.children ?? {}).map(child => ({
1668
+ commandPath: child.commandPath,
1669
+ summary: child.summary,
1670
+ resourceUri: subcommandResourceUri(cli, child.commandPath),
1671
+ })),
1672
+ risk: contract.risk,
1673
+ exposure: contract.exposure,
1674
+ tier: contract.tier,
1675
+ tokenCost: contract.tokenCost,
1676
+ summary: contract.summary,
1677
+ conformanceFixtures: contract.conformanceFixtures.map(fixture => ({
1678
+ id: fixture.id,
1679
+ description: fixture.description,
1680
+ expect: fixture.expect,
1681
+ })),
1682
+ resourceUri: subcommandResourceUri(cli, contract.commandPath),
1683
+ };
1684
+ }
1685
+ export function listProviderSubcommands(options = {}) {
1686
+ const providers = options.provider
1687
+ ? [options.provider]
1688
+ : Object.keys(UPSTREAM_CLI_CONTRACTS);
1689
+ const prefix = options.commandPathPrefix ?? [];
1690
+ const rows = [];
1691
+ for (const provider of providers) {
1692
+ for (const contract of flattenCliSubcommands(UPSTREAM_CLI_CONTRACTS[provider].subcommands)) {
1693
+ if (options.tier && contract.tier !== options.tier)
1694
+ continue;
1695
+ if (options.risk && contract.risk !== options.risk)
1696
+ continue;
1697
+ if (options.exposure && contract.exposure !== options.exposure)
1698
+ continue;
1699
+ if (prefix.length > 0 &&
1700
+ !prefix.every((part, index) => contract.commandPath[index] === part)) {
1701
+ continue;
1702
+ }
1703
+ rows.push({
1704
+ provider,
1705
+ commandPath: contract.commandPath,
1706
+ aliases: contract.aliases ?? [],
1707
+ tier: contract.tier,
1708
+ risk: contract.risk,
1709
+ exposure: contract.exposure,
1710
+ tokenCost: contract.tokenCost,
1711
+ summary: contract.summary.length > 48
1712
+ ? `${contract.summary.slice(0, 45).trimEnd()}...`
1713
+ : contract.summary,
1714
+ driftStatus: "unknown",
1715
+ resourceUri: subcommandResourceUri(provider, contract.commandPath),
1716
+ });
1717
+ }
1718
+ }
1719
+ return rows.sort((a, b) => `${a.provider}:${subcommandKey(a.commandPath)}`.localeCompare(`${b.provider}:${subcommandKey(b.commandPath)}`));
1720
+ }
1721
+ export function buildProviderSubcommandsCompactCatalog(options = {}) {
1722
+ return {
1723
+ schemaVersion: "provider-subcommands-catalog.v1",
1724
+ columns: [
1725
+ "provider",
1726
+ "commandPath",
1727
+ "aliases",
1728
+ "tier",
1729
+ "risk",
1730
+ "exposure",
1731
+ "tokenCost",
1732
+ "summary",
1733
+ "driftStatus",
1734
+ "resourceUri",
1735
+ ],
1736
+ rows: listProviderSubcommands(options).map(row => [
1737
+ row.provider,
1738
+ row.commandPath.join(" "),
1739
+ row.aliases.join(","),
1740
+ row.tier,
1741
+ row.risk,
1742
+ row.exposure,
1743
+ row.tokenCost,
1744
+ row.summary,
1745
+ row.driftStatus,
1746
+ row.resourceUri,
1747
+ ]),
1748
+ };
1749
+ }
1750
+ export function validateUpstreamCliSubcommandArgs(cli, commandPath, args) {
1751
+ const contract = getCliSubcommandContract(cli, commandPath);
1752
+ const violations = [];
1753
+ if (!contract) {
1754
+ violations.push({
1755
+ cli,
1756
+ message: `${cli} subcommand "${subcommandKey(commandPath)}" is not declared in the upstream subcommand contract`,
1757
+ });
1758
+ return { ok: false, violations, commandPath };
1759
+ }
1760
+ const positionals = [];
1761
+ for (let i = 0; i < args.length; i++) {
1762
+ const arg = args[i];
1763
+ const flag = contract.flags[arg];
1764
+ if (!flag) {
1765
+ if (arg.startsWith("-")) {
1766
+ violations.push({
1767
+ cli,
1768
+ arg,
1769
+ index: i,
1770
+ message: `Unsupported ${cli} subcommand flag "${arg}" for ${subcommandKey(commandPath)}`,
1771
+ });
1772
+ }
1773
+ else {
1774
+ positionals.push(arg);
1775
+ }
1776
+ continue;
1777
+ }
1778
+ if (flag.arity === "none")
1779
+ continue;
1780
+ if (flag.arity === "one") {
1781
+ const value = args[i + 1];
1782
+ if (value === undefined) {
1783
+ violations.push({
1784
+ cli,
1785
+ arg,
1786
+ index: i,
1787
+ message: `${cli} subcommand flag "${arg}" requires one value`,
1788
+ });
1789
+ continue;
1790
+ }
1791
+ validateFlagValue(cli, arg, flag, value, i + 1, violations);
1792
+ i += 1;
1793
+ continue;
1794
+ }
1795
+ if (flag.arity === "optional") {
1796
+ const value = args[i + 1];
1797
+ if (value !== undefined && !value.startsWith("-")) {
1798
+ validateFlagValue(cli, arg, flag, value, i + 1, violations);
1799
+ i += 1;
1800
+ }
1801
+ continue;
1802
+ }
1803
+ let consumed = 0;
1804
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1805
+ validateFlagValue(cli, arg, flag, args[i + 1], i + 1, violations);
1806
+ i += 1;
1807
+ consumed += 1;
1808
+ }
1809
+ if (consumed === 0) {
1810
+ violations.push({
1811
+ cli,
1812
+ arg,
1813
+ index: i,
1814
+ message: `${cli} subcommand flag "${arg}" requires at least one value`,
1815
+ });
1816
+ }
1817
+ }
1818
+ if (positionals.length > contract.maxPositionals) {
1819
+ violations.push({
1820
+ cli,
1821
+ message: `${cli} subcommand "${subcommandKey(commandPath)}" has ${positionals.length} positional values; upstream subcommand contract allows ${contract.maxPositionals}`,
1822
+ });
1823
+ }
1824
+ return {
1825
+ ok: violations.length === 0,
1826
+ violations,
1827
+ commandPath,
1828
+ risk: contract.risk,
1829
+ exposure: contract.exposure,
1830
+ tier: contract.tier,
1831
+ };
1832
+ }
1235
1833
  export function validateUpstreamCliEnv(cli, env) {
1236
1834
  if (!env || Object.keys(env).length === 0)
1237
1835
  return { ok: true, violations: [] };
@@ -1329,6 +1927,42 @@ export function computeFlagDrift(contract, helpText, discoveredFlags) {
1329
1927
  }
1330
1928
  return { missingFlags, extraFlags, acknowledgedExtraFlags, warnings };
1331
1929
  }
1930
+ export function computeSubcommandFlagDrift(contract, executable, helpText, discoveredFlags) {
1931
+ const warnings = [];
1932
+ const missingFlags = [];
1933
+ for (const [flag, spec] of Object.entries(contract.flags)) {
1934
+ const inHelp = helpText.includes(flag);
1935
+ if (spec.hiddenFromHelp) {
1936
+ if (inHelp) {
1937
+ warnings.push(`${subcommandKey(contract.commandPath)} ${flag} is marked hiddenFromHelp but now appears in ${executable} help output; remove the hiddenFromHelp marker from the subcommand contract`);
1938
+ }
1939
+ continue;
1940
+ }
1941
+ if (!inHelp)
1942
+ missingFlags.push(flag);
1943
+ }
1944
+ const contractFlagSet = new Set(Object.keys(contract.flags));
1945
+ const acknowledged = new Set(contract.acknowledgedUpstreamFlags ?? []);
1946
+ const extraFlags = [];
1947
+ const acknowledgedExtraFlags = [];
1948
+ for (const flag of discoveredFlags) {
1949
+ if (contractFlagSet.has(flag))
1950
+ continue;
1951
+ if (acknowledged.has(flag)) {
1952
+ acknowledgedExtraFlags.push(flag);
1953
+ }
1954
+ else {
1955
+ extraFlags.push(flag);
1956
+ }
1957
+ }
1958
+ const discoveredSet = new Set(discoveredFlags);
1959
+ for (const flag of acknowledged) {
1960
+ if (!discoveredSet.has(flag)) {
1961
+ warnings.push(`acknowledged upstream subcommand flag ${flag} no longer appears in ${executable} ${subcommandKey(contract.commandPath)} help output; remove it from acknowledgedUpstreamFlags`);
1962
+ }
1963
+ }
1964
+ return { missingFlags, extraFlags, acknowledgedExtraFlags, warnings };
1965
+ }
1332
1966
  export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
1333
1967
  const contract = UPSTREAM_CLI_CONTRACTS[cli];
1334
1968
  const outputs = [];
@@ -1365,6 +1999,7 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
1365
1999
  discoveredFlags: [],
1366
2000
  helpHash: undefined,
1367
2001
  versionHint: undefined,
2002
+ subcommands: {},
1368
2003
  probedAt: new Date().toISOString(),
1369
2004
  warnings: [result.error.message],
1370
2005
  };
@@ -1381,6 +2016,7 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
1381
2016
  const versionMatch = helpText.match(/^\s*(?:[A-Za-z][\w .-]+)?v?\d+\.\d+\S*/m);
1382
2017
  const versionHint = versionMatch ? versionMatch[0].trim().slice(0, 80) : undefined;
1383
2018
  const helpHash = createHash("sha256").update(helpText).digest("hex");
2019
+ const subcommands = probeInstalledCliSubcommands(cli, timeoutMs);
1384
2020
  return {
1385
2021
  cli,
1386
2022
  executable: contract.executable,
@@ -1394,10 +2030,69 @@ export function probeInstalledCliContract(cli, timeoutMs = 5_000) {
1394
2030
  discoveredFlags,
1395
2031
  helpHash,
1396
2032
  versionHint,
2033
+ subcommands,
1397
2034
  probedAt: new Date().toISOString(),
1398
2035
  warnings,
1399
2036
  };
1400
2037
  }
2038
+ function probeInstalledCliSubcommands(cli, timeoutMs) {
2039
+ const contract = UPSTREAM_CLI_CONTRACTS[cli];
2040
+ const probes = {};
2041
+ for (const sub of flattenCliSubcommands(contract.subcommands)) {
2042
+ const outputs = [];
2043
+ const warnings = [];
2044
+ let available = true;
2045
+ const checkedHelpCommands = sub.helpArgs.map(helpArgs => [...sub.commandPath, ...helpArgs]);
2046
+ for (const helpArgs of sub.helpArgs) {
2047
+ const args = [...sub.commandPath, ...helpArgs];
2048
+ const extendedPath = getExtendedPath();
2049
+ const env = envWithExtendedPath(process.env, extendedPath);
2050
+ const resolved = resolveCommandForSpawn(contract.executable, args, {
2051
+ envPath: extendedPath,
2052
+ });
2053
+ const result = spawnSync(resolved.command, resolved.args, {
2054
+ encoding: "utf8",
2055
+ timeout: timeoutMs,
2056
+ maxBuffer: 1024 * 1024,
2057
+ env,
2058
+ windowsHide: true,
2059
+ windowsVerbatimArguments: resolved.windowsVerbatimArguments,
2060
+ });
2061
+ if (result.error) {
2062
+ available = false;
2063
+ warnings.push(result.error.message);
2064
+ break;
2065
+ }
2066
+ outputs.push(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
2067
+ if (result.status !== 0) {
2068
+ warnings.push(`${contract.executable} ${args.join(" ")} exited with status ${result.status}`);
2069
+ }
2070
+ }
2071
+ const helpText = outputs.join("\n");
2072
+ const discoveredFlags = available ? extractDiscoveredFlags(helpText) : [];
2073
+ const drift = available
2074
+ ? computeSubcommandFlagDrift(sub, contract.executable, helpText, discoveredFlags)
2075
+ : { missingFlags: [], extraFlags: [], acknowledgedExtraFlags: [], warnings: [] };
2076
+ warnings.push(...drift.warnings);
2077
+ probes[subcommandKey(sub.commandPath)] = {
2078
+ commandPath: sub.commandPath,
2079
+ checkedHelpCommands,
2080
+ available,
2081
+ missingFlags: drift.missingFlags,
2082
+ extraFlags: drift.extraFlags,
2083
+ acknowledgedExtraFlags: drift.acknowledgedExtraFlags,
2084
+ discoveredFlags,
2085
+ helpHash: available ? createHash("sha256").update(helpText).digest("hex") : undefined,
2086
+ probedAt: new Date().toISOString(),
2087
+ warnings,
2088
+ risk: sub.risk,
2089
+ exposure: sub.exposure,
2090
+ tier: sub.tier,
2091
+ summary: sub.summary,
2092
+ };
2093
+ }
2094
+ return probes;
2095
+ }
1401
2096
  export function buildUpstreamContractReport(options = {}) {
1402
2097
  const selected = options.cli ? [options.cli] : Object.keys(UPSTREAM_CLI_CONTRACTS);
1403
2098
  const contracts = Object.fromEntries(selected.map(cli => {
@@ -1423,13 +2118,10 @@ export function buildUpstreamContractReport(options = {}) {
1423
2118
  mcpParameters: contract.mcpParameters,
1424
2119
  flags: Object.fromEntries(Object.entries(contract.flags).map(([name, flag]) => [
1425
2120
  name,
1426
- {
1427
- arity: flag.arity,
1428
- values: flag.values ?? null,
1429
- pattern: flag.pattern?.source ?? null,
1430
- description: flag.description,
1431
- },
2121
+ serializeFlagContract(flag),
1432
2122
  ])),
2123
+ subcommandCount: flattenCliSubcommands(contract.subcommands).length,
2124
+ subcommandsCatalog: buildProviderSubcommandsCompactCatalog({ provider: cli }),
1433
2125
  env: Object.fromEntries(Object.entries(contract.env ?? {}).map(([name, envContract]) => [
1434
2126
  name,
1435
2127
  {