gsd-pi 2.17.0 → 2.19.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.
Files changed (217) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -38,6 +38,11 @@ export interface Args {
38
38
  themes?: string[];
39
39
  noThemes?: boolean;
40
40
  listModels?: string | true;
41
+ discover?: boolean;
42
+ addProvider?: string;
43
+ addProviderBaseUrl?: string;
44
+ addProviderApiKey?: string;
45
+ discoverModels?: string | true;
41
46
  offline?: boolean;
42
47
  verbose?: boolean;
43
48
  messages: string[];
@@ -150,6 +155,18 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
150
155
  } else {
151
156
  result.listModels = true;
152
157
  }
158
+ } else if (arg === "--discover") {
159
+ result.discover = true;
160
+ } else if (arg === "--add-provider" && i + 1 < args.length) {
161
+ result.addProvider = args[++i];
162
+ } else if (arg === "--base-url" && i + 1 < args.length) {
163
+ result.addProviderBaseUrl = args[++i];
164
+ } else if (arg === "--discover-models") {
165
+ if (i + 1 < args.length && !args[i + 1].startsWith("-") && !args[i + 1].startsWith("@")) {
166
+ result.discoverModels = args[++i];
167
+ } else {
168
+ result.discoverModels = true;
169
+ }
153
170
  } else if (arg === "--verbose") {
154
171
  result.verbose = true;
155
172
  } else if (arg === "--offline") {
@@ -219,6 +236,10 @@ ${chalk.bold("Options:")}
219
236
  --no-themes Disable theme discovery and loading
220
237
  --export <file> Export session file to HTML and exit
221
238
  --list-models [search] List available models (with optional fuzzy search)
239
+ --discover Include discovered models in --list-models output
240
+ --discover-models [provider] Discover models from provider APIs (all or specific)
241
+ --add-provider <name> Add a provider to models.json (use with --base-url, --api-key)
242
+ --base-url <url> Base URL for --add-provider
222
243
  --verbose Force verbose startup (overrides quietStartup setting)
223
244
  --offline Disable startup network operations (same as PI_OFFLINE=1)
224
245
  --help, -h Show this help
@@ -1,11 +1,18 @@
1
1
  /**
2
- * List available models with optional fuzzy search
2
+ * List available models with optional fuzzy search and discovery support
3
3
  */
4
4
 
5
5
  import type { Api, Model } from "@gsd/pi-ai";
6
6
  import { fuzzyFilter } from "@gsd/pi-tui";
7
7
  import type { ModelRegistry } from "../core/model-registry.js";
8
8
 
9
+ export interface ListModelsOptions {
10
+ /** Include discovered models in output */
11
+ discover?: boolean;
12
+ /** Search pattern for fuzzy filtering */
13
+ searchPattern?: string;
14
+ }
15
+
9
16
  /**
10
17
  * Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M")
11
18
  */
@@ -22,10 +29,48 @@ function formatTokenCount(count: number): string {
22
29
  }
23
30
 
24
31
  /**
25
- * List available models, optionally filtered by search pattern
32
+ * Discover models from provider APIs and print results.
26
33
  */
27
- export async function listModels(modelRegistry: ModelRegistry, searchPattern?: string): Promise<void> {
28
- const models = modelRegistry.getAvailable();
34
+ export async function discoverAndPrintModels(
35
+ modelRegistry: ModelRegistry,
36
+ provider?: string,
37
+ ): Promise<void> {
38
+ const providers = provider ? [provider] : undefined;
39
+
40
+ console.log("Discovering models...");
41
+ const results = await modelRegistry.discoverModels(providers);
42
+
43
+ for (const result of results) {
44
+ if (result.error) {
45
+ console.log(` ${result.provider}: error - ${result.error}`);
46
+ } else {
47
+ console.log(` ${result.provider}: ${result.models.length} models found`);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * List available models, optionally filtered by search pattern.
54
+ * Accepts either a string (backward compat) or ListModelsOptions.
55
+ */
56
+ export async function listModels(
57
+ modelRegistry: ModelRegistry,
58
+ optionsOrSearch?: string | ListModelsOptions,
59
+ ): Promise<void> {
60
+ const options: ListModelsOptions =
61
+ typeof optionsOrSearch === "string"
62
+ ? { searchPattern: optionsOrSearch }
63
+ : optionsOrSearch ?? {};
64
+
65
+ // If discover flag is set, run discovery first
66
+ if (options.discover) {
67
+ await modelRegistry.discoverModels();
68
+ }
69
+
70
+ // Get models — include discovered if discovery was run
71
+ const models = options.discover
72
+ ? modelRegistry.getAllWithDiscovered()
73
+ : modelRegistry.getAvailable();
29
74
 
30
75
  if (models.length === 0) {
31
76
  console.log("No models available. Set API keys in environment variables.");
@@ -34,12 +79,12 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
34
79
 
35
80
  // Apply fuzzy filter if search pattern provided
36
81
  let filteredModels: Model<Api>[] = models;
37
- if (searchPattern) {
38
- filteredModels = fuzzyFilter(models, searchPattern, (m) => `${m.provider} ${m.id}`);
82
+ if (options.searchPattern) {
83
+ filteredModels = fuzzyFilter(models, options.searchPattern, (m) => `${m.provider} ${m.id}`);
39
84
  }
40
85
 
41
86
  if (filteredModels.length === 0) {
42
- console.log(`No models matching "${searchPattern}"`);
87
+ console.log(`No models matching "${options.searchPattern}"`);
43
88
  return;
44
89
  }
45
90
 
@@ -53,15 +98,19 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
53
98
  });
54
99
 
55
100
  // Calculate column widths
56
- const rows = filteredModels.map((m) => ({
57
- provider: m.provider,
58
- model: m.id,
59
- name: m.name,
60
- context: formatTokenCount(m.contextWindow),
61
- maxOut: formatTokenCount(m.maxTokens),
62
- thinking: m.reasoning ? "yes" : "no",
63
- images: m.input.includes("image") ? "yes" : "no",
64
- }));
101
+ const rows = filteredModels.map((m) => {
102
+ const isDiscovered = options.discover && modelRegistry.isDiscovered(m);
103
+ return {
104
+ provider: m.provider,
105
+ model: m.id,
106
+ name: m.name,
107
+ context: formatTokenCount(m.contextWindow),
108
+ maxOut: formatTokenCount(m.maxTokens),
109
+ thinking: m.reasoning ? "yes" : "no",
110
+ images: m.input.includes("image") ? "yes" : "no",
111
+ badge: isDiscovered ? "[discovered]" : "",
112
+ };
113
+ });
65
114
 
66
115
  const headers = {
67
116
  provider: "provider",
@@ -71,6 +120,7 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
71
120
  maxOut: "max-out",
72
121
  thinking: "thinking",
73
122
  images: "images",
123
+ badge: "",
74
124
  };
75
125
 
76
126
  const widths = {
@@ -105,7 +155,10 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
105
155
  row.maxOut.padEnd(widths.maxOut),
106
156
  row.thinking.padEnd(widths.thinking),
107
157
  row.images.padEnd(widths.images),
108
- ].join(" ");
158
+ row.badge,
159
+ ]
160
+ .join(" ")
161
+ .trimEnd();
109
162
  console.log(line);
110
163
  }
111
164
  }
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { ModelDiscoveryCache } from "./discovery-cache.js";
7
+
8
+ let testDir: string;
9
+ let cachePath: string;
10
+
11
+ beforeEach(() => {
12
+ testDir = join(tmpdir(), `discovery-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
13
+ mkdirSync(testDir, { recursive: true });
14
+ cachePath = join(testDir, "discovery-cache.json");
15
+ });
16
+
17
+ afterEach(() => {
18
+ try {
19
+ rmSync(testDir, { recursive: true, force: true });
20
+ } catch {
21
+ // Cleanup best-effort
22
+ }
23
+ });
24
+
25
+ // ─── basic operations ────────────────────────────────────────────────────────
26
+
27
+ describe("ModelDiscoveryCache — basic operations", () => {
28
+ it("starts with no entries", () => {
29
+ const cache = new ModelDiscoveryCache(cachePath);
30
+ assert.equal(cache.get("openai"), undefined);
31
+ });
32
+
33
+ it("stores and retrieves models", () => {
34
+ const cache = new ModelDiscoveryCache(cachePath);
35
+ const models = [{ id: "gpt-4o", name: "GPT-4o" }];
36
+ cache.set("openai", models);
37
+
38
+ const entry = cache.get("openai");
39
+ assert.ok(entry);
40
+ assert.deepEqual(entry.models, models);
41
+ assert.ok(entry.fetchedAt > 0);
42
+ assert.ok(entry.ttlMs > 0);
43
+ });
44
+
45
+ it("persists to disk and reloads", () => {
46
+ const cache1 = new ModelDiscoveryCache(cachePath);
47
+ cache1.set("openai", [{ id: "gpt-4o" }]);
48
+
49
+ const cache2 = new ModelDiscoveryCache(cachePath);
50
+ const entry = cache2.get("openai");
51
+ assert.ok(entry);
52
+ assert.equal(entry.models[0].id, "gpt-4o");
53
+ });
54
+
55
+ it("clear removes a specific provider", () => {
56
+ const cache = new ModelDiscoveryCache(cachePath);
57
+ cache.set("openai", [{ id: "gpt-4o" }]);
58
+ cache.set("google", [{ id: "gemini-pro" }]);
59
+
60
+ cache.clear("openai");
61
+ assert.equal(cache.get("openai"), undefined);
62
+ assert.ok(cache.get("google"));
63
+ });
64
+
65
+ it("clear without provider removes all entries", () => {
66
+ const cache = new ModelDiscoveryCache(cachePath);
67
+ cache.set("openai", [{ id: "gpt-4o" }]);
68
+ cache.set("google", [{ id: "gemini-pro" }]);
69
+
70
+ cache.clear();
71
+ assert.equal(cache.get("openai"), undefined);
72
+ assert.equal(cache.get("google"), undefined);
73
+ });
74
+ });
75
+
76
+ // ─── staleness ───────────────────────────────────────────────────────────────
77
+
78
+ describe("ModelDiscoveryCache — staleness", () => {
79
+ it("newly set entries are not stale", () => {
80
+ const cache = new ModelDiscoveryCache(cachePath);
81
+ cache.set("openai", [{ id: "gpt-4o" }]);
82
+ assert.equal(cache.isStale("openai"), false);
83
+ });
84
+
85
+ it("missing providers are stale", () => {
86
+ const cache = new ModelDiscoveryCache(cachePath);
87
+ assert.equal(cache.isStale("unknown"), true);
88
+ });
89
+
90
+ it("entries with expired TTL are stale", () => {
91
+ const cache = new ModelDiscoveryCache(cachePath);
92
+ cache.set("openai", [{ id: "gpt-4o" }], 1); // 1ms TTL
93
+
94
+ // Wait for TTL to expire
95
+ const start = Date.now();
96
+ while (Date.now() - start < 5) {
97
+ // busy wait
98
+ }
99
+
100
+ assert.equal(cache.isStale("openai"), true);
101
+ });
102
+ });
103
+
104
+ // ─── getAll ──────────────────────────────────────────────────────────────────
105
+
106
+ describe("ModelDiscoveryCache — getAll", () => {
107
+ it("returns non-stale entries by default", () => {
108
+ const cache = new ModelDiscoveryCache(cachePath);
109
+ cache.set("openai", [{ id: "gpt-4o" }]);
110
+ cache.set("stale", [{ id: "old" }], 1);
111
+
112
+ // Wait for stale TTL
113
+ const start = Date.now();
114
+ while (Date.now() - start < 5) {
115
+ // busy wait
116
+ }
117
+
118
+ const all = cache.getAll();
119
+ assert.ok(all.has("openai"));
120
+ assert.ok(!all.has("stale"));
121
+ });
122
+
123
+ it("returns all entries when includeStale is true", () => {
124
+ const cache = new ModelDiscoveryCache(cachePath);
125
+ cache.set("openai", [{ id: "gpt-4o" }]);
126
+ cache.set("stale", [{ id: "old" }], 1);
127
+
128
+ // Wait for stale TTL
129
+ const start = Date.now();
130
+ while (Date.now() - start < 5) {
131
+ // busy wait
132
+ }
133
+
134
+ const all = cache.getAll(true);
135
+ assert.ok(all.has("openai"));
136
+ assert.ok(all.has("stale"));
137
+ });
138
+ });
139
+
140
+ // ─── edge cases ──────────────────────────────────────────────────────────────
141
+
142
+ describe("ModelDiscoveryCache — edge cases", () => {
143
+ it("handles corrupted cache file gracefully", () => {
144
+ writeFileSync(cachePath, "not valid json", "utf-8");
145
+ const cache = new ModelDiscoveryCache(cachePath);
146
+ assert.equal(cache.get("openai"), undefined);
147
+ });
148
+
149
+ it("handles wrong version gracefully", () => {
150
+ writeFileSync(cachePath, JSON.stringify({ version: 99, entries: {} }), "utf-8");
151
+ const cache = new ModelDiscoveryCache(cachePath);
152
+ assert.equal(cache.get("openai"), undefined);
153
+ });
154
+
155
+ it("handles missing cache file", () => {
156
+ const cache = new ModelDiscoveryCache(join(testDir, "nonexistent", "cache.json"));
157
+ assert.equal(cache.get("openai"), undefined);
158
+ });
159
+
160
+ it("overwrites existing entry for same provider", () => {
161
+ const cache = new ModelDiscoveryCache(cachePath);
162
+ cache.set("openai", [{ id: "gpt-4o" }]);
163
+ cache.set("openai", [{ id: "gpt-4o-mini" }]);
164
+
165
+ const entry = cache.get("openai");
166
+ assert.ok(entry);
167
+ assert.equal(entry.models.length, 1);
168
+ assert.equal(entry.models[0].id, "gpt-4o-mini");
169
+ });
170
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Disk-based cache for discovered models.
3
+ * Stores results at {agentDir}/discovery-cache.json with per-provider TTLs.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { getAgentDir } from "../config.js";
9
+ import { type DiscoveredModel, getDefaultTTL } from "./model-discovery.js";
10
+
11
+ export interface DiscoveryCacheEntry {
12
+ models: DiscoveredModel[];
13
+ fetchedAt: number;
14
+ ttlMs: number;
15
+ }
16
+
17
+ export interface DiscoveryCacheData {
18
+ version: 1;
19
+ entries: Record<string, DiscoveryCacheEntry>;
20
+ }
21
+
22
+ export class ModelDiscoveryCache {
23
+ private data: DiscoveryCacheData;
24
+ private cachePath: string;
25
+
26
+ constructor(cachePath?: string) {
27
+ this.cachePath = cachePath ?? join(getAgentDir(), "discovery-cache.json");
28
+ this.data = { version: 1, entries: {} };
29
+ this.load();
30
+ }
31
+
32
+ get(provider: string): DiscoveryCacheEntry | undefined {
33
+ const entry = this.data.entries[provider];
34
+ return entry;
35
+ }
36
+
37
+ set(provider: string, models: DiscoveredModel[], ttlMs?: number): void {
38
+ this.data.entries[provider] = {
39
+ models,
40
+ fetchedAt: Date.now(),
41
+ ttlMs: ttlMs ?? getDefaultTTL(provider),
42
+ };
43
+ this.save();
44
+ }
45
+
46
+ isStale(provider: string): boolean {
47
+ const entry = this.data.entries[provider];
48
+ if (!entry) return true;
49
+ return Date.now() - entry.fetchedAt > entry.ttlMs;
50
+ }
51
+
52
+ clear(provider?: string): void {
53
+ if (provider) {
54
+ delete this.data.entries[provider];
55
+ } else {
56
+ this.data.entries = {};
57
+ }
58
+ this.save();
59
+ }
60
+
61
+ getAll(includeStale = false): Map<string, DiscoveryCacheEntry> {
62
+ const result = new Map<string, DiscoveryCacheEntry>();
63
+ for (const [provider, entry] of Object.entries(this.data.entries)) {
64
+ if (includeStale || !this.isStale(provider)) {
65
+ result.set(provider, entry);
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ load(): void {
72
+ try {
73
+ if (existsSync(this.cachePath)) {
74
+ const content = readFileSync(this.cachePath, "utf-8");
75
+ const parsed = JSON.parse(content) as DiscoveryCacheData;
76
+ if (parsed.version === 1 && parsed.entries) {
77
+ this.data = parsed;
78
+ }
79
+ }
80
+ } catch {
81
+ // Corrupted or unreadable cache — start fresh
82
+ this.data = { version: 1, entries: {} };
83
+ }
84
+ }
85
+
86
+ save(): void {
87
+ try {
88
+ const dir = dirname(this.cachePath);
89
+ if (!existsSync(dir)) {
90
+ mkdirSync(dir, { recursive: true });
91
+ }
92
+ writeFileSync(this.cachePath, JSON.stringify(this.data, null, 2), "utf-8");
93
+ } catch {
94
+ // Silently ignore write failures (read-only FS, permissions, etc.)
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,125 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ DISCOVERY_TTLS,
5
+ getDefaultTTL,
6
+ getDiscoverableProviders,
7
+ getDiscoveryAdapter,
8
+ } from "./model-discovery.js";
9
+
10
+ // ─── getDiscoveryAdapter ─────────────────────────────────────────────────────
11
+
12
+ describe("getDiscoveryAdapter", () => {
13
+ it("returns an adapter for openai", () => {
14
+ const adapter = getDiscoveryAdapter("openai");
15
+ assert.equal(adapter.provider, "openai");
16
+ assert.equal(adapter.supportsDiscovery, true);
17
+ });
18
+
19
+ it("returns an adapter for ollama", () => {
20
+ const adapter = getDiscoveryAdapter("ollama");
21
+ assert.equal(adapter.provider, "ollama");
22
+ assert.equal(adapter.supportsDiscovery, true);
23
+ });
24
+
25
+ it("returns an adapter for openrouter", () => {
26
+ const adapter = getDiscoveryAdapter("openrouter");
27
+ assert.equal(adapter.provider, "openrouter");
28
+ assert.equal(adapter.supportsDiscovery, true);
29
+ });
30
+
31
+ it("returns an adapter for google", () => {
32
+ const adapter = getDiscoveryAdapter("google");
33
+ assert.equal(adapter.provider, "google");
34
+ assert.equal(adapter.supportsDiscovery, true);
35
+ });
36
+
37
+ it("returns a static adapter for anthropic", () => {
38
+ const adapter = getDiscoveryAdapter("anthropic");
39
+ assert.equal(adapter.provider, "anthropic");
40
+ assert.equal(adapter.supportsDiscovery, false);
41
+ });
42
+
43
+ it("returns a static adapter for bedrock", () => {
44
+ const adapter = getDiscoveryAdapter("bedrock");
45
+ assert.equal(adapter.provider, "bedrock");
46
+ assert.equal(adapter.supportsDiscovery, false);
47
+ });
48
+
49
+ it("returns a static adapter for unknown providers", () => {
50
+ const adapter = getDiscoveryAdapter("unknown-provider");
51
+ assert.equal(adapter.provider, "unknown-provider");
52
+ assert.equal(adapter.supportsDiscovery, false);
53
+ });
54
+
55
+ it("static adapter fetchModels returns empty array", async () => {
56
+ const adapter = getDiscoveryAdapter("anthropic");
57
+ const models = await adapter.fetchModels("key");
58
+ assert.deepEqual(models, []);
59
+ });
60
+ });
61
+
62
+ // ─── getDiscoverableProviders ────────────────────────────────────────────────
63
+
64
+ describe("getDiscoverableProviders", () => {
65
+ it("returns only providers that support discovery", () => {
66
+ const providers = getDiscoverableProviders();
67
+ assert.ok(providers.includes("openai"));
68
+ assert.ok(providers.includes("ollama"));
69
+ assert.ok(providers.includes("openrouter"));
70
+ assert.ok(providers.includes("google"));
71
+ assert.ok(!providers.includes("anthropic"));
72
+ assert.ok(!providers.includes("bedrock"));
73
+ });
74
+
75
+ it("returns an array of strings", () => {
76
+ const providers = getDiscoverableProviders();
77
+ assert.ok(Array.isArray(providers));
78
+ for (const p of providers) {
79
+ assert.equal(typeof p, "string");
80
+ }
81
+ });
82
+ });
83
+
84
+ // ─── getDefaultTTL ───────────────────────────────────────────────────────────
85
+
86
+ describe("getDefaultTTL", () => {
87
+ it("returns 5 minutes for ollama", () => {
88
+ assert.equal(getDefaultTTL("ollama"), 5 * 60 * 1000);
89
+ });
90
+
91
+ it("returns 1 hour for openai", () => {
92
+ assert.equal(getDefaultTTL("openai"), 60 * 60 * 1000);
93
+ });
94
+
95
+ it("returns 1 hour for google", () => {
96
+ assert.equal(getDefaultTTL("google"), 60 * 60 * 1000);
97
+ });
98
+
99
+ it("returns 1 hour for openrouter", () => {
100
+ assert.equal(getDefaultTTL("openrouter"), 60 * 60 * 1000);
101
+ });
102
+
103
+ it("returns 24 hours for unknown providers", () => {
104
+ assert.equal(getDefaultTTL("some-custom"), 24 * 60 * 60 * 1000);
105
+ });
106
+ });
107
+
108
+ // ─── DISCOVERY_TTLS ──────────────────────────────────────────────────────────
109
+
110
+ describe("DISCOVERY_TTLS", () => {
111
+ it("has expected keys", () => {
112
+ assert.ok("ollama" in DISCOVERY_TTLS);
113
+ assert.ok("openai" in DISCOVERY_TTLS);
114
+ assert.ok("google" in DISCOVERY_TTLS);
115
+ assert.ok("openrouter" in DISCOVERY_TTLS);
116
+ assert.ok("default" in DISCOVERY_TTLS);
117
+ });
118
+
119
+ it("all values are positive numbers", () => {
120
+ for (const [, value] of Object.entries(DISCOVERY_TTLS)) {
121
+ assert.equal(typeof value, "number");
122
+ assert.ok(value > 0);
123
+ }
124
+ });
125
+ });