@vellumai/assistant 0.10.7-dev.202607100313.fffdfea → 0.10.7-dev.202607100720.fba456e

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.
Files changed (99) hide show
  1. package/openapi.yaml +846 -27
  2. package/package.json +1 -1
  3. package/src/__tests__/config-watcher-skill-reseed.test.ts +1 -0
  4. package/src/__tests__/config-watcher.test.ts +22 -4
  5. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  6. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -9
  7. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -2
  8. package/src/cli/commands/__tests__/inference-models.test.ts +86 -0
  9. package/src/cli/commands/__tests__/inference-profiles.test.ts +254 -0
  10. package/src/cli/commands/__tests__/inference-providers.test.ts +171 -0
  11. package/src/cli/commands/__tests__/schedules.test.ts +133 -1
  12. package/src/cli/commands/__tests__/usage.test.ts +131 -0
  13. package/src/cli/commands/inference-callsites.ts +158 -0
  14. package/src/cli/commands/inference-models.ts +84 -0
  15. package/src/cli/commands/inference-profiles.ts +371 -0
  16. package/src/cli/commands/inference-providers-default.ts +89 -0
  17. package/src/cli/commands/inference-providers.ts +87 -16
  18. package/src/cli/commands/inference.ts +6 -0
  19. package/src/cli/commands/memory/nodes.ts +90 -50
  20. package/src/cli/commands/schedules.ts +17 -2
  21. package/src/cli/commands/usage.ts +59 -21
  22. package/src/cli/lib/__tests__/cli-output.test.ts +20 -0
  23. package/src/cli/lib/cli-output.ts +55 -0
  24. package/src/config/__tests__/loader-last-known-good.test.ts +229 -0
  25. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +166 -0
  26. package/src/config/default-profile-catalog.ts +28 -0
  27. package/src/config/loader.ts +276 -46
  28. package/src/daemon/config-watcher.ts +65 -20
  29. package/src/daemon/message-types/sync.ts +1 -0
  30. package/src/oauth/byo-connection.test.ts +1 -1
  31. package/src/oauth/connection-resolver.test.ts +140 -0
  32. package/src/oauth/connection-resolver.ts +48 -12
  33. package/src/plugins/defaults/memory/anisotropy.ts +1 -1
  34. package/src/plugins/defaults/memory/config.ts +1 -1
  35. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +1 -1
  36. package/src/plugins/defaults/memory/context-search/search.ts +1 -1
  37. package/src/plugins/defaults/memory/graph/bootstrap.ts +1 -1
  38. package/src/plugins/defaults/memory/graph/consolidation.ts +1 -1
  39. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +11 -1
  40. package/src/plugins/defaults/memory/graph/extraction.ts +1 -1
  41. package/src/plugins/defaults/memory/graph/narrative.ts +1 -1
  42. package/src/plugins/defaults/memory/graph/pattern-scan.ts +1 -1
  43. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -1
  44. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  45. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +43 -16
  46. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +1 -1
  47. package/src/plugins/defaults/memory/host-utils.ts +80 -0
  48. package/src/plugins/defaults/memory/identity-context.ts +1 -1
  49. package/src/plugins/defaults/memory/injectors.ts +1 -1
  50. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +2 -2
  51. package/src/plugins/defaults/memory/jobs-worker.ts +2 -9
  52. package/src/plugins/defaults/memory/memory-retrospective-state.ts +1 -1
  53. package/src/plugins/defaults/memory/paths.ts +48 -0
  54. package/src/plugins/defaults/memory/pkb/autoinject.ts +1 -1
  55. package/src/plugins/defaults/memory/pkb/context.ts +2 -2
  56. package/src/plugins/defaults/memory/pkb/types.ts +1 -1
  57. package/src/plugins/defaults/memory/pkb-schedule.ts +2 -2
  58. package/src/plugins/defaults/memory/rerank-local.ts +2 -3
  59. package/src/plugins/defaults/memory/routes/memory-eval-routes.ts +1 -1
  60. package/src/plugins/defaults/memory/routes/memory-item-routes.test.ts +149 -1
  61. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +144 -1
  62. package/src/plugins/defaults/memory/routes/memory-v2-routes.ts +1 -1
  63. package/src/plugins/defaults/memory/startup.ts +1 -1
  64. package/src/plugins/defaults/memory/v2/backfill-jobs.ts +1 -1
  65. package/src/plugins/defaults/memory/v2/consolidation-job.ts +2 -2
  66. package/src/plugins/defaults/memory/v2/injection.ts +1 -1
  67. package/src/plugins/defaults/memory/v2/memory-v2-startup.ts +1 -1
  68. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +1 -1
  69. package/src/plugins/defaults/memory/v2/qdrant.ts +1 -1
  70. package/src/plugins/defaults/memory/v2/reranker.ts +1 -1
  71. package/src/plugins/defaults/memory/v2/static-context.ts +1 -1
  72. package/src/plugins/defaults/memory/v2/sweep-job.ts +1 -1
  73. package/src/plugins/defaults/memory/v3/candidate-match.ts +1 -1
  74. package/src/plugins/defaults/memory/v3/maintain-job.ts +1 -1
  75. package/src/plugins/defaults/memory/v3/page-content.ts +1 -1
  76. package/src/plugins/defaults/memory/v3/pool-select.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/selection-log-store.ts +1 -1
  78. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +2 -5
  79. package/src/plugins/defaults/memory/worker.ts +1 -1
  80. package/src/plugins/defaults/workspace/injectors.ts +17 -15
  81. package/src/plugins/types.ts +9 -14
  82. package/src/providers/inference/connection-availability.ts +193 -0
  83. package/src/runtime/routes/__tests__/inference-callsites-routes.test.ts +54 -0
  84. package/src/runtime/routes/__tests__/inference-models-routes.test.ts +54 -0
  85. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +405 -0
  86. package/src/runtime/routes/__tests__/inference-routes-registration.test.ts +42 -0
  87. package/src/runtime/routes/__tests__/theme-routes.test.ts +66 -0
  88. package/src/runtime/routes/conversation-query-routes.ts +5 -3
  89. package/src/runtime/routes/default-provider-routes.ts +5 -152
  90. package/src/runtime/routes/index.ts +8 -0
  91. package/src/runtime/routes/inference-callsites-routes.ts +211 -0
  92. package/src/runtime/routes/inference-models-routes.ts +104 -0
  93. package/src/runtime/routes/inference-profiles-routes.ts +717 -0
  94. package/src/runtime/routes/theme-routes.ts +36 -0
  95. package/src/runtime/sync/resource-sync-events.ts +4 -0
  96. package/src/theme/workspace-theme.test.ts +269 -0
  97. package/src/theme/workspace-theme.ts +304 -0
  98. package/src/watcher/__tests__/engine-auth-notify.test.ts +316 -0
  99. package/src/watcher/engine.ts +87 -2
@@ -64,7 +64,9 @@ async function runCommand(
64
64
  registerSchedulesCommand(program);
65
65
  await program.parseAsync(["node", "assistant", ...args]);
66
66
  } catch {
67
- if (process.exitCode === 0) process.exitCode = 1;
67
+ if (process.exitCode === 0) {
68
+ process.exitCode = 1;
69
+ }
68
70
  } finally {
69
71
  process.stdout.write = originalStdoutWrite;
70
72
  }
@@ -469,6 +471,136 @@ describe("schedules runs", () => {
469
471
  expect(logLines.join("\n")).toContain("conversation-1");
470
472
  });
471
473
 
474
+ test("renders a COST column with the shared variable-precision USD format", async () => {
475
+ mockIpcResult = {
476
+ ok: true,
477
+ result: {
478
+ runs: [
479
+ {
480
+ id: "run-1",
481
+ jobId: "schedule-1",
482
+ status: "ok",
483
+ startedAt: 1_778_799_000_000,
484
+ finishedAt: 1_778_799_002_500,
485
+ durationMs: 2_500,
486
+ output: "done",
487
+ error: null,
488
+ conversationId: "conversation-1",
489
+ estimatedCostUsd: 0.0123,
490
+ createdAt: 1_778_799_000_000,
491
+ },
492
+ {
493
+ id: "run-subcent",
494
+ jobId: "schedule-1",
495
+ status: "ok",
496
+ startedAt: 1_778_799_000_000,
497
+ finishedAt: 1_778_799_002_500,
498
+ durationMs: 2_500,
499
+ output: "done",
500
+ error: null,
501
+ conversationId: "conversation-1",
502
+ estimatedCostUsd: 0.0042,
503
+ createdAt: 1_778_799_000_000,
504
+ },
505
+ ],
506
+ },
507
+ };
508
+
509
+ const { exitCode } = await runCommand(["schedules", "runs", "schedule-1"]);
510
+
511
+ expect(exitCode).toBe(0);
512
+ const output = logLines.join("\n");
513
+ expect(output).toContain("COST");
514
+ // Matches usage totals/daily/breakdown: 2dp at a cent or more, 6dp sub-cent.
515
+ expect(output).toContain("$0.01");
516
+ expect(output).toContain("$0.004200");
517
+ });
518
+
519
+ test("renders — for zero or unpriced run cost", async () => {
520
+ mockIpcResult = {
521
+ ok: true,
522
+ result: {
523
+ runs: [
524
+ {
525
+ id: "run-zero",
526
+ jobId: "schedule-1",
527
+ status: "ok",
528
+ startedAt: 1_778_799_000_000,
529
+ finishedAt: 1_778_799_002_500,
530
+ durationMs: 2_500,
531
+ output: "done",
532
+ error: null,
533
+ conversationId: "conversation-1",
534
+ estimatedCostUsd: 0,
535
+ createdAt: 1_778_799_000_000,
536
+ },
537
+ {
538
+ id: "run-unpriced",
539
+ jobId: "schedule-1",
540
+ status: "ok",
541
+ startedAt: 1_778_799_000_000,
542
+ finishedAt: 1_778_799_002_500,
543
+ durationMs: 2_500,
544
+ output: "done",
545
+ error: null,
546
+ conversationId: "conversation-1",
547
+ createdAt: 1_778_799_000_000,
548
+ },
549
+ ],
550
+ },
551
+ };
552
+
553
+ const { exitCode } = await runCommand(["schedules", "runs", "schedule-1"]);
554
+
555
+ expect(exitCode).toBe(0);
556
+ const output = logLines.join("\n");
557
+ expect(output).toContain("COST");
558
+ expect(output).not.toContain("$");
559
+ const runZeroLine = logLines.find((line) => line.includes("run-zero"));
560
+ const runUnpricedLine = logLines.find((line) =>
561
+ line.includes("run-unpriced"),
562
+ );
563
+ expect(runZeroLine).toContain("—");
564
+ expect(runUnpricedLine).toContain("—");
565
+ });
566
+
567
+ test("includes estimatedCostUsd in --json output", async () => {
568
+ mockIpcResult = {
569
+ ok: true,
570
+ result: {
571
+ runs: [
572
+ {
573
+ id: "run-1",
574
+ jobId: "schedule-1",
575
+ status: "ok",
576
+ startedAt: 1_778_799_000_000,
577
+ finishedAt: 1_778_799_002_500,
578
+ durationMs: 2_500,
579
+ output: "done",
580
+ error: null,
581
+ conversationId: "conversation-1",
582
+ estimatedCostUsd: 0.0123,
583
+ createdAt: 1_778_799_000_000,
584
+ },
585
+ ],
586
+ },
587
+ };
588
+
589
+ const { stdout, exitCode } = await runCommand([
590
+ "schedules",
591
+ "runs",
592
+ "schedule-1",
593
+ "--json",
594
+ ]);
595
+
596
+ expect(exitCode).toBe(0);
597
+ expect(JSON.parse(stdout)).toEqual({
598
+ runs: [
599
+ expect.objectContaining({ id: "run-1", estimatedCostUsd: 0.0123 }),
600
+ ],
601
+ });
602
+ });
603
+
472
604
  test("formats run durations into human-friendly units", async () => {
473
605
  const durations = [
474
606
  { ms: 450, expected: "450ms" },
@@ -0,0 +1,131 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ let ipcCalls: Array<{ method: string; params?: Record<string, unknown> }> = [];
6
+ let mockIpcResult: {
7
+ ok: boolean;
8
+ result?: unknown;
9
+ error?: string;
10
+ } = { ok: true, result: {} };
11
+ let exitFromIpcResultCalls: unknown[] = [];
12
+
13
+ mock.module("../../../ipc/cli-client.js", () => ({
14
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
15
+ ipcCalls.push({ method, params });
16
+ return mockIpcResult;
17
+ },
18
+ exitFromIpcResult: (result: unknown) => {
19
+ exitFromIpcResultCalls.push(result);
20
+ process.exitCode = 10;
21
+ throw new Error("exitFromIpcResult called");
22
+ },
23
+ }));
24
+
25
+ mock.module("../../../util/logger.js", () => ({
26
+ getLogger: () => ({
27
+ info: () => {},
28
+ warn: () => {},
29
+ error: () => {},
30
+ debug: () => {},
31
+ }),
32
+ getCliLogger: () => ({
33
+ info: () => {},
34
+ warn: () => {},
35
+ error: () => {},
36
+ debug: () => {},
37
+ }),
38
+ }));
39
+
40
+ const { registerUsageCommand } = await import("../usage.js");
41
+
42
+ async function runCommand(args: string[]): Promise<{ exitCode: number }> {
43
+ process.exitCode = 0;
44
+ try {
45
+ const program = new Command();
46
+ program.exitOverride();
47
+ program.configureOutput({
48
+ writeErr: () => {},
49
+ writeOut: () => {},
50
+ });
51
+ registerUsageCommand(program);
52
+ await program.parseAsync(["node", "assistant", "usage", ...args]);
53
+ } catch {
54
+ if (process.exitCode === 0) {
55
+ process.exitCode = 1;
56
+ }
57
+ }
58
+ const exitCode = process.exitCode ?? 0;
59
+ process.exitCode = 0;
60
+ return { exitCode };
61
+ }
62
+
63
+ function lastQueryParams(): Record<string, unknown> {
64
+ const call = ipcCalls.at(-1);
65
+ return (call?.params?.queryParams ?? {}) as Record<string, unknown>;
66
+ }
67
+
68
+ describe("usage --schedule option", () => {
69
+ beforeEach(() => {
70
+ ipcCalls = [];
71
+ exitFromIpcResultCalls = [];
72
+ mockIpcResult = { ok: true, result: {} };
73
+ });
74
+
75
+ test("totals forwards --schedule as scheduleId", async () => {
76
+ mockIpcResult = { ok: true, result: { buckets: [] } };
77
+ await runCommand([
78
+ "totals",
79
+ "--range",
80
+ "all",
81
+ "--schedule",
82
+ "sched-abc123",
83
+ ]);
84
+ expect(ipcCalls.at(-1)?.method).toBe("usage_totals");
85
+ expect(lastQueryParams().scheduleId).toBe("sched-abc123");
86
+ });
87
+
88
+ test("totals omits scheduleId when --schedule absent", async () => {
89
+ await runCommand(["totals", "--range", "all"]);
90
+ expect(ipcCalls.at(-1)?.method).toBe("usage_totals");
91
+ expect(lastQueryParams()).not.toHaveProperty("scheduleId");
92
+ });
93
+
94
+ test("daily forwards --schedule as scheduleId", async () => {
95
+ mockIpcResult = { ok: true, result: { buckets: [] } };
96
+ await runCommand(["daily", "--range", "all", "--schedule", "sched-abc123"]);
97
+ expect(ipcCalls.at(-1)?.method).toBe("usage_daily");
98
+ expect(lastQueryParams().scheduleId).toBe("sched-abc123");
99
+ });
100
+
101
+ test("daily omits scheduleId when --schedule absent", async () => {
102
+ mockIpcResult = { ok: true, result: { buckets: [] } };
103
+ await runCommand(["daily", "--range", "all"]);
104
+ expect(ipcCalls.at(-1)?.method).toBe("usage_daily");
105
+ expect(lastQueryParams()).not.toHaveProperty("scheduleId");
106
+ });
107
+
108
+ test("breakdown forwards --schedule as scheduleId alongside groupBy", async () => {
109
+ mockIpcResult = { ok: true, result: { breakdown: [] } };
110
+ await runCommand([
111
+ "breakdown",
112
+ "--range",
113
+ "all",
114
+ "--group-by",
115
+ "provider",
116
+ "--schedule",
117
+ "sched-abc123",
118
+ ]);
119
+ expect(ipcCalls.at(-1)?.method).toBe("usage_breakdown");
120
+ const qp = lastQueryParams();
121
+ expect(qp.scheduleId).toBe("sched-abc123");
122
+ expect(qp.groupBy).toBe("provider");
123
+ });
124
+
125
+ test("breakdown omits scheduleId when --schedule absent", async () => {
126
+ mockIpcResult = { ok: true, result: { breakdown: [] } };
127
+ await runCommand(["breakdown", "--range", "all", "--group-by", "provider"]);
128
+ expect(ipcCalls.at(-1)?.method).toBe("usage_breakdown");
129
+ expect(lastQueryParams()).not.toHaveProperty("scheduleId");
130
+ });
131
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `assistant inference callsites` CLI namespace.
3
+ *
4
+ * callsites list — effective resolution for every call site
5
+ * callsites get <site> — resolution detail + chain for one call site
6
+ *
7
+ * Read-only. Delegates to the daemon (`inference_callsites_*`), which computes
8
+ * resolution via the shared `resolveCallSiteConfig` / `selectWinningProfile`
9
+ * machinery. Output is deliberately compact and aligned — it is mostly read by
10
+ * the assistant itself.
11
+ */
12
+
13
+ import type { Command } from "commander";
14
+
15
+ import { cliIpcCall } from "../../ipc/cli-client.js";
16
+ import { renderTable, writeCliError, writeLine } from "../lib/cli-output.js";
17
+
18
+ type Source = "override" | "active" | "call_site" | "default";
19
+
20
+ interface CallSiteSummary {
21
+ callSite: string;
22
+ profile: string | null;
23
+ label: string | null;
24
+ source: Source;
25
+ provider: string;
26
+ model: string;
27
+ effort: string;
28
+ maxTokens: number;
29
+ maxInputTokens?: number;
30
+ }
31
+
32
+ interface CallSiteDetail {
33
+ callSite: string;
34
+ winner: { profile: string | null; label: string | null; source: Source };
35
+ resolved: {
36
+ provider: string;
37
+ model: string;
38
+ maxTokens: number;
39
+ effort: string;
40
+ temperature: number | null;
41
+ maxInputTokens?: number;
42
+ };
43
+ resolutionChain: { requested: string; reason: string }[];
44
+ shippedDefault: Record<string, unknown>;
45
+ userPin: Record<string, unknown> | null;
46
+ resolutionError?: { reason: string; message: string };
47
+ }
48
+
49
+ const SOURCE_LABEL: Record<Source, string> = {
50
+ override: "override",
51
+ active: "active",
52
+ call_site: "pin",
53
+ default: "default",
54
+ };
55
+
56
+ function formatCtxIn(tokens: number | undefined): string {
57
+ if (tokens == null) {
58
+ return "-";
59
+ }
60
+ if (tokens >= 1_000_000 && tokens % 1_000_000 === 0) {
61
+ return `${tokens / 1_000_000}M`;
62
+ }
63
+ return tokens.toLocaleString();
64
+ }
65
+
66
+ export function attachCallsitesSubcommand(inference: Command): void {
67
+ const callsites = inference
68
+ .command("callsites")
69
+ .description("Inspect how each LLM call site resolves to a profile");
70
+
71
+ callsites
72
+ .command("list")
73
+ .description("List the effective resolution for every call site")
74
+ .option("--json", "Output as machine-readable JSON")
75
+ .action(async (opts: { json?: boolean }) => {
76
+ const ipcResult = await cliIpcCall<{ callSites: CallSiteSummary[] }>(
77
+ "inference_callsites_list",
78
+ {},
79
+ );
80
+ if (!ipcResult.ok) {
81
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
82
+ return;
83
+ }
84
+ const rows = ipcResult.result!.callSites;
85
+ if (opts.json) {
86
+ process.stdout.write(
87
+ JSON.stringify({ ok: true, callSites: rows }) + "\n",
88
+ );
89
+ return;
90
+ }
91
+ renderTable(
92
+ [
93
+ "CALL SITE",
94
+ "PROFILE",
95
+ "SRC",
96
+ "PROVIDER",
97
+ "MODEL",
98
+ "EFFORT",
99
+ "CTX-IN",
100
+ ],
101
+ rows.map((r) => [
102
+ r.callSite,
103
+ r.profile ?? "(anchor)",
104
+ SOURCE_LABEL[r.source],
105
+ r.provider,
106
+ r.model,
107
+ r.effort,
108
+ formatCtxIn(r.maxInputTokens),
109
+ ]),
110
+ );
111
+ });
112
+
113
+ callsites
114
+ .command("get <site>")
115
+ .description("Show the resolution detail and chain for one call site")
116
+ .option("--json", "Output as machine-readable JSON")
117
+ .action(async (site: string, opts: { json?: boolean }) => {
118
+ const ipcResult = await cliIpcCall<CallSiteDetail>(
119
+ "inference_callsites_get",
120
+ { pathParams: { site } },
121
+ );
122
+ if (!ipcResult.ok) {
123
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
124
+ return;
125
+ }
126
+ const d = ipcResult.result!;
127
+ if (opts.json) {
128
+ process.stdout.write(JSON.stringify({ ok: true, ...d }) + "\n");
129
+ return;
130
+ }
131
+ writeLine(`call site: ${d.callSite}`);
132
+ writeLine(
133
+ ` winner: ${d.winner.profile ?? "(anchor)"} [${SOURCE_LABEL[d.winner.source]}]` +
134
+ (d.winner.label ? ` "${d.winner.label}"` : ""),
135
+ );
136
+ writeLine(
137
+ ` resolved: ${d.resolved.provider}/${d.resolved.model} effort=${d.resolved.effort} maxTokens=${d.resolved.maxTokens}` +
138
+ (d.resolved.maxInputTokens != null
139
+ ? ` ctxIn=${formatCtxIn(d.resolved.maxInputTokens)}`
140
+ : ""),
141
+ );
142
+ if (d.resolutionChain.length > 0) {
143
+ writeLine(" resolution chain (skipped rungs):");
144
+ for (const step of d.resolutionChain) {
145
+ writeLine(` - ${step.requested}: ${step.reason}`);
146
+ }
147
+ }
148
+ writeLine(` shipped default: ${JSON.stringify(d.shippedDefault)}`);
149
+ writeLine(
150
+ ` user pin: ${d.userPin ? JSON.stringify(d.userPin) : "(none)"}`,
151
+ );
152
+ if (d.resolutionError) {
153
+ writeLine(
154
+ ` resolution error [${d.resolutionError.reason}]: ${d.resolutionError.message}`,
155
+ );
156
+ }
157
+ });
158
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `assistant inference models` CLI namespace.
3
+ *
4
+ * `assistant inference models list [--provider <p>] [--json]`
5
+ *
6
+ * Lists the code-owned model catalog so the assistant can discover valid
7
+ * model ids instead of guessing. Delegates to the daemon via IPC
8
+ * (`inference_models_list`).
9
+ */
10
+
11
+ import type { Command } from "commander";
12
+
13
+ import { cliIpcCall } from "../../ipc/cli-client.js";
14
+ import { renderTable, writeCliError, writeLine } from "../lib/cli-output.js";
15
+
16
+ interface CatalogModel {
17
+ provider: string;
18
+ id: string;
19
+ displayName: string;
20
+ contextWindowTokens?: number;
21
+ maxOutputTokens?: number;
22
+ supportsThinking?: boolean;
23
+ supportsVision?: boolean;
24
+ supportsToolUse?: boolean;
25
+ featureFlag?: string;
26
+ }
27
+
28
+ export function attachModelsSubcommand(inference: Command): void {
29
+ const models = inference
30
+ .command("models")
31
+ .description("Inspect the inference model catalog");
32
+
33
+ models
34
+ .command("list")
35
+ .description("List catalog models (optionally filtered by provider)")
36
+ .option("--provider <p>", "Filter by provider id")
37
+ .option("--json", "Output as machine-readable JSON")
38
+ .addHelpText(
39
+ "after",
40
+ `
41
+ Lists every model in the code-owned provider catalog. Use the ids here
42
+ when creating an inference profile:
43
+
44
+ Examples:
45
+ $ assistant inference models list
46
+ $ assistant inference models list --provider anthropic
47
+ $ assistant inference models list --json`,
48
+ )
49
+ .action(async (opts: { provider?: string; json?: boolean }) => {
50
+ const ipcResult = await cliIpcCall<{ models: CatalogModel[] }>(
51
+ "inference_models_list",
52
+ { queryParams: opts.provider ? { provider: opts.provider } : {} },
53
+ );
54
+
55
+ if (!ipcResult.ok) {
56
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
57
+ return;
58
+ }
59
+
60
+ const rows = ipcResult.result!.models;
61
+
62
+ if (opts.json) {
63
+ process.stdout.write(JSON.stringify({ ok: true, models: rows }) + "\n");
64
+ return;
65
+ }
66
+
67
+ if (rows.length === 0) {
68
+ writeLine("No models found.");
69
+ return;
70
+ }
71
+
72
+ renderTable(
73
+ ["PROVIDER", "MODEL ID", "DISPLAY NAME", "CONTEXT"],
74
+ rows.map((m) => [
75
+ m.provider,
76
+ m.id,
77
+ m.displayName,
78
+ m.contextWindowTokens != null
79
+ ? m.contextWindowTokens.toLocaleString()
80
+ : "-",
81
+ ]),
82
+ );
83
+ });
84
+ }