@duckmind/dm-darwin-arm64 0.32.9 → 0.33.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.
Files changed (95) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +14 -65
  3. package/extensions/dm-9router-ext/README.md +142 -0
  4. package/extensions/dm-9router-ext/package.json +45 -0
  5. package/extensions/dm-9router-ext/src/index.ts +541 -0
  6. package/extensions/dm-9router-ext/tsconfig.json +17 -0
  7. package/extensions/dm-subagents/package-lock.json +3 -3
  8. package/extensions/dm-tasks/node_modules/.package-lock.json +3 -3
  9. package/extensions/dm-tasks/node_modules/typebox/build/type/script/mapping.d.mts +2 -2
  10. package/extensions/dm-tasks/node_modules/typebox/build/type/script/parser.d.mts +2 -2
  11. package/extensions/dm-tasks/node_modules/typebox/build/type/script/parser.mjs +2 -2
  12. package/extensions/dm-tasks/node_modules/typebox/build/type/types/number.d.mts +1 -1
  13. package/extensions/dm-tasks/node_modules/typebox/build/type/types/number.mjs +1 -1
  14. package/extensions/dm-tasks/node_modules/typebox/build/type/types/record.d.mts +1 -1
  15. package/extensions/dm-tasks/node_modules/typebox/package.json +1 -1
  16. package/extensions/dm-tasks/package-lock.json +3 -3
  17. package/package.json +1 -1
  18. package/theme/theme-alps.json +93 -0
  19. package/extensions/dm-chime/README.md +0 -11
  20. package/extensions/dm-chime/docs/protocols.md +0 -107
  21. package/extensions/dm-chime/index.ts +0 -205
  22. package/extensions/dm-chime/package.json +0 -33
  23. package/extensions/dm-phone/README.md +0 -24
  24. package/extensions/dm-phone/index.ts +0 -12
  25. package/extensions/dm-phone/node_modules/.package-lock.json +0 -29
  26. package/extensions/dm-phone/node_modules/ws/LICENSE +0 -20
  27. package/extensions/dm-phone/node_modules/ws/README.md +0 -548
  28. package/extensions/dm-phone/node_modules/ws/browser.js +0 -8
  29. package/extensions/dm-phone/node_modules/ws/index.js +0 -22
  30. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +0 -131
  31. package/extensions/dm-phone/node_modules/ws/lib/constants.js +0 -19
  32. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +0 -292
  33. package/extensions/dm-phone/node_modules/ws/lib/extension.js +0 -203
  34. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +0 -55
  35. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +0 -528
  36. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +0 -760
  37. package/extensions/dm-phone/node_modules/ws/lib/sender.js +0 -607
  38. package/extensions/dm-phone/node_modules/ws/lib/stream.js +0 -161
  39. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +0 -62
  40. package/extensions/dm-phone/node_modules/ws/lib/validation.js +0 -152
  41. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +0 -562
  42. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +0 -1407
  43. package/extensions/dm-phone/node_modules/ws/package.json +0 -70
  44. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +0 -21
  45. package/extensions/dm-phone/package-lock.json +0 -66
  46. package/extensions/dm-phone/package.json +0 -35
  47. package/extensions/dm-phone/phone-session-pool.ts +0 -8
  48. package/extensions/dm-phone/public/app/attachments.js +0 -233
  49. package/extensions/dm-phone/public/app/autocomplete-controller.js +0 -81
  50. package/extensions/dm-phone/public/app/autocomplete.js +0 -135
  51. package/extensions/dm-phone/public/app/bindings.js +0 -178
  52. package/extensions/dm-phone/public/app/command-catalog.js +0 -76
  53. package/extensions/dm-phone/public/app/commands.js +0 -376
  54. package/extensions/dm-phone/public/app/constants.js +0 -60
  55. package/extensions/dm-phone/public/app/formatters.js +0 -131
  56. package/extensions/dm-phone/public/app/handlers.js +0 -442
  57. package/extensions/dm-phone/public/app/main.js +0 -6
  58. package/extensions/dm-phone/public/app/markdown.js +0 -105
  59. package/extensions/dm-phone/public/app/messages.js +0 -418
  60. package/extensions/dm-phone/public/app/sheet-actions.js +0 -113
  61. package/extensions/dm-phone/public/app/sheet-navigation.js +0 -19
  62. package/extensions/dm-phone/public/app/sheets-view.js +0 -287
  63. package/extensions/dm-phone/public/app/state.js +0 -95
  64. package/extensions/dm-phone/public/app/tool-rendering.js +0 -562
  65. package/extensions/dm-phone/public/app/transport.js +0 -176
  66. package/extensions/dm-phone/public/app/ui.js +0 -417
  67. package/extensions/dm-phone/public/app.js +0 -1
  68. package/extensions/dm-phone/public/icon.svg +0 -15
  69. package/extensions/dm-phone/public/index.html +0 -146
  70. package/extensions/dm-phone/public/manifest.webmanifest +0 -17
  71. package/extensions/dm-phone/public/styles.css +0 -1139
  72. package/extensions/dm-phone/public/sw.js +0 -78
  73. package/extensions/dm-phone/src/extension/duckmind-models.js +0 -264
  74. package/extensions/dm-phone/src/extension/phone-args.ts +0 -121
  75. package/extensions/dm-phone/src/extension/phone-paths.ts +0 -250
  76. package/extensions/dm-phone/src/extension/phone-quota.ts +0 -188
  77. package/extensions/dm-phone/src/extension/phone-runtime.ts +0 -154
  78. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +0 -1217
  79. package/extensions/dm-phone/src/extension/phone-sessions.ts +0 -139
  80. package/extensions/dm-phone/src/extension/phone-static.ts +0 -30
  81. package/extensions/dm-phone/src/extension/phone-tailscale.ts +0 -148
  82. package/extensions/dm-phone/src/extension/phone-theme.ts +0 -85
  83. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +0 -112
  84. package/extensions/dm-phone/src/extension/register-phone-extension.ts +0 -106
  85. package/extensions/dm-phone/src/extension/types.ts +0 -73
  86. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +0 -882
  87. package/extensions/dm-phone/src/session-pool/session-pool.ts +0 -470
  88. package/extensions/dm-phone/src/session-pool/session-worker.ts +0 -739
  89. package/extensions/dm-phone/src/session-pool/types.ts +0 -111
  90. package/extensions/dm-phone/src/session-pool/utils.ts +0 -23
  91. package/extensions/dm-phone/test/duckmind-models.test.js +0 -147
  92. package/extensions/dm-thinking-timer/LICENSE +0 -21
  93. package/extensions/dm-thinking-timer/README.md +0 -7
  94. package/extensions/dm-thinking-timer/package.json +0 -20
  95. package/extensions/dm-thinking-timer/thinking-timer.ts +0 -250
@@ -0,0 +1,541 @@
1
+ /**
2
+ * dm-9router-ext
3
+ *
4
+ * DM extension for 9router — an open-source AI routing proxy.
5
+ * Connects DM to your 9router instance via its OpenAI-compatible API.
6
+ *
7
+ * Features:
8
+ * - Auto-discovers models and combos from 9router on startup
9
+ * - Registers 9router as a DM provider with dynamic base URL and API key
10
+ * - Status commands to view connection info and available models
11
+ * - User-persisted configuration shared by all DM instances
12
+ *
13
+ * Environment variables:
14
+ * NINE_ROUTER_BASE_URL - 9router endpoint (default: http://localhost:20128)
15
+ * NINE_ROUTER_API_KEY - API key if 9router requires authentication
16
+ */
17
+
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { dirname, join } from "node:path";
21
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
22
+ import { Type } from "typebox";
23
+
24
+ // =============================================================================
25
+ // Types
26
+ // =============================================================================
27
+
28
+ interface NineRouterConfig {
29
+ baseUrl: string;
30
+ apiKey: string | undefined;
31
+ }
32
+
33
+ interface NineRouterModel {
34
+ id: string;
35
+ object: string;
36
+ owned_by?: string;
37
+ kind?: string;
38
+ }
39
+
40
+ interface NineRouterModelsResponse {
41
+ object: string;
42
+ data: NineRouterModel[];
43
+ }
44
+
45
+ // =============================================================================
46
+ // Constants
47
+ // =============================================================================
48
+
49
+ const DEFAULT_BASE_URL = "http://localhost:20128";
50
+ const ENV_BASE_URL = process.env.NINE_ROUTER_BASE_URL;
51
+ const ENV_API_KEY = process.env.NINE_ROUTER_API_KEY;
52
+ const CONFIG_PATH = join(homedir(), ".dm", "agent", "9router-config.json");
53
+
54
+ const CUSTOM_TYPE_CONFIG = "9router-config";
55
+ const CUSTOM_TYPE_LAST_ROUTE = "9router-last-route";
56
+
57
+ // Headers that may indicate the actual upstream model used
58
+ const ROUTING_HEADERS = [
59
+ "x-9router-model",
60
+ "x-routed-model",
61
+ "x-actual-model",
62
+ "x-upstream-model",
63
+ "x-provider-model",
64
+ ];
65
+
66
+ // =============================================================================
67
+ // Config Helpers
68
+ // =============================================================================
69
+
70
+ function normalizeBaseUrl(url: string): string {
71
+ return url.replace(/\/$/, "");
72
+ }
73
+
74
+ function maskApiKey(key: string): string {
75
+ if (key.length <= 8) return "●".repeat(key.length);
76
+ return key.slice(0, 4) + "●".repeat(Math.max(0, key.length - 8)) + key.slice(-4);
77
+ }
78
+
79
+ function applyEnvOverrides(config: NineRouterConfig): NineRouterConfig {
80
+ return {
81
+ baseUrl: normalizeBaseUrl(ENV_BASE_URL || config.baseUrl),
82
+ apiKey: ENV_API_KEY || config.apiKey,
83
+ };
84
+ }
85
+
86
+ function loadConfigFromDisk(): NineRouterConfig | null {
87
+ try {
88
+ if (!existsSync(CONFIG_PATH)) return null;
89
+ const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<NineRouterConfig>;
90
+ if (!data.baseUrl || typeof data.baseUrl !== "string") return null;
91
+ return {
92
+ baseUrl: normalizeBaseUrl(data.baseUrl),
93
+ apiKey: typeof data.apiKey === "string" && data.apiKey.trim()
94
+ ? data.apiKey.trim()
95
+ : undefined,
96
+ };
97
+ } catch (err) {
98
+ console.error("[dm-9router-ext] Failed to load persisted config:", err);
99
+ return null;
100
+ }
101
+ }
102
+
103
+ function saveConfigToDisk(config: NineRouterConfig) {
104
+ try {
105
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
106
+ writeFileSync(
107
+ CONFIG_PATH,
108
+ `${JSON.stringify({ baseUrl: config.baseUrl, apiKey: config.apiKey }, null, 2)}\n`,
109
+ { mode: 0o600 },
110
+ );
111
+ } catch (err) {
112
+ console.error("[dm-9router-ext] Failed to persist config:", err);
113
+ }
114
+ }
115
+
116
+ function getInitialConfig(): NineRouterConfig {
117
+ return applyEnvOverrides(loadConfigFromDisk() || {
118
+ baseUrl: DEFAULT_BASE_URL,
119
+ apiKey: undefined,
120
+ });
121
+ }
122
+
123
+ function loadConfigFromSession(ctx: ExtensionContext): NineRouterConfig | null {
124
+ for (const entry of ctx.sessionManager.getEntries()) {
125
+ if (entry.type === "custom" && entry.customType === CUSTOM_TYPE_CONFIG) {
126
+ const data = entry.data as Partial<NineRouterConfig> | undefined;
127
+ if (data?.baseUrl) {
128
+ return applyEnvOverrides({
129
+ baseUrl: normalizeBaseUrl(data.baseUrl),
130
+ apiKey: data.apiKey,
131
+ });
132
+ }
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function persistConfig(pi: ExtensionAPI, config: NineRouterConfig) {
139
+ saveConfigToDisk(config);
140
+ pi.appendEntry(CUSTOM_TYPE_CONFIG, {
141
+ baseUrl: config.baseUrl,
142
+ apiKey: config.apiKey,
143
+ });
144
+ }
145
+
146
+ // =============================================================================
147
+ // 9router API Client
148
+ // =============================================================================
149
+
150
+ async function fetchModels(
151
+ config: NineRouterConfig,
152
+ signal?: AbortSignal,
153
+ ): Promise<NineRouterModel[]> {
154
+ const headers: Record<string, string> = {
155
+ Accept: "application/json",
156
+ };
157
+ if (config.apiKey) {
158
+ headers.Authorization = `Bearer ${config.apiKey}`;
159
+ }
160
+
161
+ const response = await fetch(`${config.baseUrl}/v1/models`, {
162
+ method: "GET",
163
+ headers,
164
+ signal,
165
+ });
166
+
167
+ if (!response.ok) {
168
+ const text = await response.text().catch(() => "");
169
+ throw new Error(
170
+ `9router returned ${response.status}: ${text || response.statusText}`,
171
+ );
172
+ }
173
+
174
+ const payload = (await response.json()) as NineRouterModelsResponse;
175
+ return payload.data || [];
176
+ }
177
+
178
+ async function testConnection(
179
+ config: NineRouterConfig,
180
+ signal?: AbortSignal,
181
+ ): Promise<{ ok: boolean; error?: string }> {
182
+ try {
183
+ const headers: Record<string, string> = {};
184
+ if (config.apiKey) {
185
+ headers.Authorization = `Bearer ${config.apiKey}`;
186
+ }
187
+
188
+ const response = await fetch(`${config.baseUrl}/v1/models`, {
189
+ method: "GET",
190
+ headers,
191
+ signal,
192
+ });
193
+
194
+ if (response.ok) {
195
+ return { ok: true };
196
+ }
197
+ return { ok: false, error: `HTTP ${response.status}: ${response.statusText}` };
198
+ } catch (err) {
199
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
200
+ }
201
+ }
202
+
203
+ // =============================================================================
204
+ // Model Mapping
205
+ // =============================================================================
206
+
207
+ function mapNineRouterModel(model: NineRouterModel) {
208
+ const isCombo = model.owned_by === "combo";
209
+
210
+ return {
211
+ id: model.id,
212
+ name: isCombo ? `🔀 ${model.id}` : model.id,
213
+ reasoning: false,
214
+ input: ["text"] as ("text" | "image")[],
215
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
216
+ contextWindow: 128000,
217
+ maxTokens: 4096,
218
+ compat: {
219
+ // 9router is an OpenAI-compatible proxy; keep requests conservative so
220
+ // using this extension does not force built-in-provider-specific features.
221
+ supportsDeveloperRole: false,
222
+ supportsReasoningEffort: false,
223
+ },
224
+ };
225
+ }
226
+
227
+ // =============================================================================
228
+ // Provider Registration
229
+ // =============================================================================
230
+
231
+ function registerNineRouterProvider(
232
+ pi: ExtensionAPI,
233
+ config: NineRouterConfig,
234
+ models: NineRouterModel[],
235
+ ) {
236
+ // Always use a dedicated provider id ("9router") and never override built-in
237
+ // providers like ollama-cloud/openrouter. DM requires apiKey for custom
238
+ // providers with models; 9router receives the real key only when configured,
239
+ // otherwise a harmless placeholder is scoped to the 9router provider.
240
+ pi.registerProvider("9router", {
241
+ name: "9router",
242
+ baseUrl: `${config.baseUrl}/v1`,
243
+ apiKey: config.apiKey || "9router-no-api-key",
244
+ api: "openai-completions",
245
+ models: models.map(mapNineRouterModel),
246
+ });
247
+ }
248
+
249
+ function unregisterNineRouterProvider(pi: ExtensionAPI) {
250
+ pi.unregisterProvider("9router");
251
+ }
252
+
253
+ // =============================================================================
254
+ // Extension Factory
255
+ // =============================================================================
256
+
257
+ export default async function (pi: ExtensionAPI) {
258
+ // ---------------------------------------------------------------------------
259
+ // Load configuration (env vars are defaults; session config applied later)
260
+ // ---------------------------------------------------------------------------
261
+ let config: NineRouterConfig = getInitialConfig();
262
+
263
+ // State that survives across the extension lifetime
264
+ let discoveredModels: NineRouterModel[] = [];
265
+ let lastRoutedModel: string | undefined;
266
+ let activeProvider: string | undefined;
267
+ let isConnected = false;
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Provider registration (async factory = models available immediately)
271
+ // ---------------------------------------------------------------------------
272
+ try {
273
+ const models = await fetchModels(config);
274
+ discoveredModels = models;
275
+ isConnected = true;
276
+ registerNineRouterProvider(pi, config, models);
277
+ } catch (err) {
278
+ // Do not register an empty/broken provider on startup. Leaving the provider
279
+ // absent is safer for built-in DM providers and model selection. Commands
280
+ // remain available so the user can configure/reload 9router later.
281
+ unregisterNineRouterProvider(pi);
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Session start: rehydrate config from session
286
+ // ---------------------------------------------------------------------------
287
+ pi.on("session_start", async (_event, ctx) => {
288
+ const restored = loadConfigFromSession(ctx);
289
+ if (!loadConfigFromDisk() && restored) {
290
+ // Migrate old session-persisted config to the new user-wide config file.
291
+ config = restored;
292
+ persistConfig(pi, config);
293
+ try {
294
+ const models = await fetchModels(config, ctx.signal);
295
+ discoveredModels = models;
296
+ isConnected = true;
297
+ registerNineRouterProvider(pi, config, models);
298
+ } catch (err) {
299
+ isConnected = false;
300
+ console.error("[dm-9router-ext] Failed to refresh migrated config:", err);
301
+ }
302
+ }
303
+
304
+ if (isConnected && discoveredModels.length > 0) {
305
+ ctx.ui.notify(
306
+ `9router connected — ${discoveredModels.length} models available`,
307
+ "info",
308
+ );
309
+ }
310
+ });
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Detect routed model from response headers
314
+ // ---------------------------------------------------------------------------
315
+ pi.on("after_provider_response", (event) => {
316
+ if (event.status >= 400 || activeProvider !== "9router") {
317
+ return;
318
+ }
319
+
320
+ for (const header of ROUTING_HEADERS) {
321
+ const value = event.headers[header];
322
+ if (value && typeof value === "string") {
323
+ lastRoutedModel = value;
324
+ pi.appendEntry(CUSTOM_TYPE_LAST_ROUTE, {
325
+ model: value,
326
+ timestamp: Date.now(),
327
+ });
328
+ break;
329
+ }
330
+ }
331
+ });
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Clear routing info when model changes
335
+ // ---------------------------------------------------------------------------
336
+ pi.on("model_select", async (event) => {
337
+ activeProvider = event.model.provider;
338
+ if (event.model.provider !== "9router") {
339
+ lastRoutedModel = undefined;
340
+ }
341
+ });
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // Command: /9router-status
345
+ // ---------------------------------------------------------------------------
346
+ pi.registerCommand("9router-status", {
347
+ description: "Show 9router connection status and configuration",
348
+ handler: async (_args, ctx) => {
349
+ const test = await testConnection(config, ctx.signal);
350
+ const lines: string[] = [
351
+ `🔗 9router Status`,
352
+ ``,
353
+ `Base URL: ${config.baseUrl}`,
354
+ `API Key: ${config.apiKey ? maskApiKey(config.apiKey) : "not set"}`,
355
+ `Connection: ${test.ok ? "🟢 connected" : `🔴 ${test.error || "disconnected"}`}`,
356
+ `Models: ${discoveredModels.length} available`,
357
+ ];
358
+
359
+ if (lastRoutedModel) {
360
+ lines.push(`Last routed: ${lastRoutedModel}`);
361
+ }
362
+
363
+ const combos = discoveredModels.filter((m) => m.owned_by === "combo");
364
+ const regular = discoveredModels.filter((m) => m.owned_by !== "combo");
365
+ if (regular.length > 0) {
366
+ lines.push(``, `Regular models: ${regular.length}`);
367
+ }
368
+ if (combos.length > 0) {
369
+ lines.push(`Combos: ${combos.length}`);
370
+ }
371
+
372
+ ctx.ui.notify(lines.join("\n"), test.ok ? "info" : "warning");
373
+ },
374
+ });
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Command: /9router-models
378
+ // ---------------------------------------------------------------------------
379
+ pi.registerCommand("9router-models", {
380
+ description: "Browse 9router available models and combos",
381
+ handler: async (_args, ctx) => {
382
+ if (discoveredModels.length === 0) {
383
+ ctx.ui.notify(
384
+ "No 9router models discovered. Check connection with /9router-status",
385
+ "warning",
386
+ );
387
+ return;
388
+ }
389
+
390
+ const items = discoveredModels.map((m) => {
391
+ const isCombo = m.owned_by === "combo";
392
+ return {
393
+ value: m.id,
394
+ label: isCombo ? `🔀 ${m.id}` : m.id,
395
+ };
396
+ });
397
+
398
+ const selected = await ctx.ui.select(
399
+ "Select a 9router model to use:",
400
+ items.map((i) => i.label),
401
+ );
402
+ if (!selected) return;
403
+
404
+ const modelId = items.find((i) => i.label === selected)?.value;
405
+ if (!modelId) return;
406
+
407
+ const fullModelId = `9router/${modelId}`;
408
+ ctx.ui.notify(`Switching to ${fullModelId}...`, "info");
409
+ pi.sendUserMessage(`/model ${fullModelId}`, { deliverAs: "followUp" });
410
+ },
411
+ });
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Command: /9router-config
415
+ // ---------------------------------------------------------------------------
416
+ pi.registerCommand("9router-config", {
417
+ description: "Configure 9router base URL and API key",
418
+ handler: async (_args, ctx) => {
419
+ // Show current config first
420
+ const test = await testConnection(config, ctx.signal);
421
+ const currentStatus = test.ok ? "🟢 connected" : "🔴 disconnected";
422
+ const currentApiKeyDisplay = config.apiKey ? "●●●●●●●● (set)" : "not set";
423
+
424
+ const currentLines = [
425
+ "Current config:",
426
+ ` Base URL: ${config.baseUrl}`,
427
+ ` API Key: ${config.apiKey ? maskApiKey(config.apiKey) : "not set"}`,
428
+ ` Status: ${currentStatus}`,
429
+ "",
430
+ "Enter new values (press Enter to keep current):",
431
+ ].join("\n");
432
+
433
+ ctx.ui.notify(currentLines, "info");
434
+ const newBaseUrl = await ctx.ui.input("Base URL", config.baseUrl);
435
+ if (newBaseUrl === undefined) return; // cancelled
436
+
437
+ const newApiKey = await ctx.ui.input(
438
+ "API key (press Enter to keep current, leave blank to remove):",
439
+ config.apiKey || "",
440
+ );
441
+ if (newApiKey === undefined) return; // cancelled
442
+
443
+ config = {
444
+ baseUrl: normalizeBaseUrl(newBaseUrl),
445
+ apiKey: newApiKey.trim() || undefined,
446
+ };
447
+
448
+ persistConfig(pi, config);
449
+
450
+ // Try to refresh models and re-register provider
451
+ try {
452
+ const models = await fetchModels(config, ctx.signal);
453
+ discoveredModels = models;
454
+ isConnected = true;
455
+
456
+ registerNineRouterProvider(pi, config, models);
457
+
458
+ ctx.ui.notify(
459
+ `9router updated — ${models.length} models at ${config.baseUrl}`,
460
+ "info",
461
+ );
462
+ } catch (err) {
463
+ isConnected = false;
464
+ unregisterNineRouterProvider(pi);
465
+ const msg = err instanceof Error ? err.message : String(err);
466
+ ctx.ui.notify(`Failed to connect: ${msg}`, "error");
467
+ }
468
+ },
469
+ });
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // Command: /9router-reload
473
+ // ---------------------------------------------------------------------------
474
+ pi.registerCommand("9router-reload", {
475
+ description: "Reload models from 9router",
476
+ handler: async (_args, ctx) => {
477
+ try {
478
+ const models = await fetchModels(config, ctx.signal);
479
+ discoveredModels = models;
480
+ isConnected = true;
481
+
482
+ registerNineRouterProvider(pi, config, models);
483
+
484
+ ctx.ui.notify(
485
+ `9router reloaded — ${models.length} models`,
486
+ "info",
487
+ );
488
+ } catch (err) {
489
+ isConnected = false;
490
+ unregisterNineRouterProvider(pi);
491
+ const msg = err instanceof Error ? err.message : String(err);
492
+ ctx.ui.notify(`Reload failed: ${msg}`, "error");
493
+ }
494
+ },
495
+ });
496
+
497
+ // ---------------------------------------------------------------------------
498
+ // Tool: ninerouter_status
499
+ // ---------------------------------------------------------------------------
500
+ pi.registerTool({
501
+ name: "ninerouter_status",
502
+ label: "9router Status",
503
+ description:
504
+ "Check 9router connection status and list available models",
505
+ parameters: Type.Object({}),
506
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
507
+ const test = await testConnection(config, ctx.signal);
508
+ const combos = discoveredModels.filter((m) => m.owned_by === "combo");
509
+ const regular = discoveredModels.filter((m) => m.owned_by !== "combo");
510
+
511
+ return {
512
+ content: [
513
+ {
514
+ type: "text",
515
+ text: [
516
+ `9router: ${test.ok ? "connected" : `disconnected (${test.error})`}`,
517
+ `Base URL: ${config.baseUrl}`,
518
+ `Total models: ${discoveredModels.length}`,
519
+ ` Regular: ${regular.length}`,
520
+ ` Combos: ${combos.length}`,
521
+ lastRoutedModel
522
+ ? `Last routed model: ${lastRoutedModel}`
523
+ : "",
524
+ ]
525
+ .filter(Boolean)
526
+ .join("\n"),
527
+ },
528
+ ],
529
+ details: {
530
+ connected: test.ok,
531
+ baseUrl: config.baseUrl,
532
+ modelCount: discoveredModels.length,
533
+ regularCount: regular.length,
534
+ comboCount: combos.length,
535
+ lastRoutedModel,
536
+ models: discoveredModels.map((m) => m.id),
537
+ },
538
+ };
539
+ },
540
+ });
541
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "skipLibCheck": true,
10
+ "types": [
11
+ "node"
12
+ ]
13
+ },
14
+ "include": [
15
+ "src/**/*.ts"
16
+ ]
17
+ }
@@ -4006,9 +4006,9 @@
4006
4006
  "license": "0BSD"
4007
4007
  },
4008
4008
  "node_modules/typebox": {
4009
- "version": "1.2.0",
4010
- "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.0.tgz",
4011
- "integrity": "sha512-5zmwtMQArPorr1PVqo5Ye3GRmaiHQ7iicRnlmRXWWDLg5k5fky8OG40B12ckVq5YIxpGM8s1qzcfcA8SKNjPVw==",
4009
+ "version": "1.2.1",
4010
+ "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.1.tgz",
4011
+ "integrity": "sha512-0upGv6+mxJR7/Wc7yoxjc/U6SjOk2aNDNzbihYacSHh+JfOsf28IJ8ggW4/3tRlDKfbInvEDPVneEywjOWYCzw==",
4012
4012
  "dev": true,
4013
4013
  "license": "MIT"
4014
4014
  },
@@ -5,9 +5,9 @@
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "node_modules/typebox": {
8
- "version": "1.2.0",
9
- "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.0.tgz",
10
- "integrity": "sha512-5zmwtMQArPorr1PVqo5Ye3GRmaiHQ7iicRnlmRXWWDLg5k5fky8OG40B12ckVq5YIxpGM8s1qzcfcA8SKNjPVw==",
8
+ "version": "1.2.1",
9
+ "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.1.tgz",
10
+ "integrity": "sha512-0upGv6+mxJR7/Wc7yoxjc/U6SjOk2aNDNzbihYacSHh+JfOsf28IJ8ggW4/3tRlDKfbInvEDPVneEywjOWYCzw==",
11
11
  "license": "MIT"
12
12
  }
13
13
  }
@@ -309,8 +309,8 @@ export type TPatternBigIntMapping<Input extends '-?(?:0|[1-9][0-9]*)n'> = (T.TBi
309
309
  export declare function PatternBigIntMapping(input: '-?(?:0|[1-9][0-9]*)n'): unknown;
310
310
  export type TPatternStringMapping<Input extends '.*'> = (T.TString);
311
311
  export declare function PatternStringMapping(input: '.*'): unknown;
312
- export type TPatternNumberMapping<Input extends '-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?'> = (T.TNumber);
313
- export declare function PatternNumberMapping(input: '-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?'): unknown;
312
+ export type TPatternNumberMapping<Input extends '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?'> = (T.TNumber);
313
+ export declare function PatternNumberMapping(input: '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?'): unknown;
314
314
  export type TPatternIntegerMapping<Input extends '-?(?:0|[1-9][0-9]*)'> = (T.TInteger);
315
315
  export declare function PatternIntegerMapping(input: '-?(?:0|[1-9][0-9]*)'): unknown;
316
316
  export type TPatternNeverMapping<Input extends '(?!)'> = (T.TNever);
@@ -106,10 +106,10 @@ export type TJsonArray<Input extends string> = (Token.TConst<'[', Input> extends
106
106
  export type TJson<Input extends string> = (TJsonNumber<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TJsonBoolean<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TJsonString<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TJsonNull<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TJsonObject<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TJsonArray<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : []) extends [infer _0 extends unknown, infer Input extends string] ? [S.TJsonMapping<_0>, Input] : [];
107
107
  export type TPatternBigInt<Input extends string> = Token.TConst<'-?(?:0|[1-9][0-9]*)n', Input> extends [infer _0 extends '-?(?:0|[1-9][0-9]*)n', infer Input extends string] ? [S.TPatternBigIntMapping<_0>, Input] : [];
108
108
  export type TPatternString<Input extends string> = Token.TConst<'.*', Input> extends [infer _0 extends '.*', infer Input extends string] ? [S.TPatternStringMapping<_0>, Input] : [];
109
- export type TPatternNumber<Input extends string> = Token.TConst<'-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?', Input> extends [infer _0 extends '-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?', infer Input extends string] ? [S.TPatternNumberMapping<_0>, Input] : [];
109
+ export type TPatternNumber<Input extends string> = Token.TConst<'-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?', Input> extends [infer _0 extends '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?', infer Input extends string] ? [S.TPatternNumberMapping<_0>, Input] : [];
110
110
  export type TPatternInteger<Input extends string> = Token.TConst<'-?(?:0|[1-9][0-9]*)', Input> extends [infer _0 extends '-?(?:0|[1-9][0-9]*)', infer Input extends string] ? [S.TPatternIntegerMapping<_0>, Input] : [];
111
111
  export type TPatternNever<Input extends string> = Token.TConst<'(?!)', Input> extends [infer _0 extends '(?!)', infer Input extends string] ? [S.TPatternNeverMapping<_0>, Input] : [];
112
- export type TPatternText<Input extends string> = Token.TUntil_1<['-?(?:0|[1-9][0-9]*)n', '.*', '-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?', '-?(?:0|[1-9][0-9]*)', '(?!)', '(', ')', '$', '|'], Input> extends [infer _0 extends string, infer Input extends string] ? [S.TPatternTextMapping<_0>, Input] : [];
112
+ export type TPatternText<Input extends string> = Token.TUntil_1<['-?(?:0|[1-9][0-9]*)n', '.*', '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?', '-?(?:0|[1-9][0-9]*)', '(?!)', '(', ')', '$', '|'], Input> extends [infer _0 extends string, infer Input extends string] ? [S.TPatternTextMapping<_0>, Input] : [];
113
113
  export type TPatternBase<Input extends string> = (TPatternBigInt<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternString<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternNumber<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternInteger<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternNever<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternGroup<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : TPatternText<Input> extends [infer _0, infer Input extends string] ? [_0, Input] : []) extends [infer _0 extends unknown, infer Input extends string] ? [S.TPatternBaseMapping<_0>, Input] : [];
114
114
  export type TPatternGroup<Input extends string> = (Token.TConst<'(', Input> extends [infer _0, infer Input extends string] ? (TPatternBody<Input> extends [infer _1, infer Input extends string] ? (Token.TConst<')', Input> extends [infer _2, infer Input extends string] ? [[_0, _1, _2], Input] : []) : []) : []) extends [infer _0 extends [unknown, unknown, unknown], infer Input extends string] ? [S.TPatternGroupMapping<_0>, Input] : [];
115
115
  export type TPatternUnion<Input extends string> = ((TPatternTerm<Input> extends [infer _0, infer Input extends string] ? (Token.TConst<'|', Input> extends [infer _1, infer Input extends string] ? (TPatternUnion<Input> extends [infer _2, infer Input extends string] ? [[_0, _1, _2], Input] : []) : []) : []) extends [infer _0, infer Input extends string] ? [_0, Input] : (TPatternTerm<Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] ? [_0, Input] : [[], Input] extends [infer _0, infer Input extends string] ? [_0, Input] : []) extends [infer _0 extends [unknown, unknown, unknown] | [unknown] | [], infer Input extends string] ? [S.TPatternUnionMapping<_0>, Input] : [];
@@ -110,10 +110,10 @@ export const JsonArray = (input) => If(If(Token.Const('[', input), ([_0, input])
110
110
  export const Json = (input) => If(If(JsonNumber(input), ([_0, input]) => [_0, input], () => If(JsonBoolean(input), ([_0, input]) => [_0, input], () => If(JsonString(input), ([_0, input]) => [_0, input], () => If(JsonNull(input), ([_0, input]) => [_0, input], () => If(JsonObject(input), ([_0, input]) => [_0, input], () => If(JsonArray(input), ([_0, input]) => [_0, input], () => [])))))), ([_0, input]) => [S.JsonMapping(_0), input]);
111
111
  export const PatternBigInt = (input) => If(Token.Const('-?(?:0|[1-9][0-9]*)n', input), ([_0, input]) => [S.PatternBigIntMapping(_0), input]);
112
112
  export const PatternString = (input) => If(Token.Const('.*', input), ([_0, input]) => [S.PatternStringMapping(_0), input]);
113
- export const PatternNumber = (input) => If(Token.Const('-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?', input), ([_0, input]) => [S.PatternNumberMapping(_0), input]);
113
+ export const PatternNumber = (input) => If(Token.Const('-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?', input), ([_0, input]) => [S.PatternNumberMapping(_0), input]);
114
114
  export const PatternInteger = (input) => If(Token.Const('-?(?:0|[1-9][0-9]*)', input), ([_0, input]) => [S.PatternIntegerMapping(_0), input]);
115
115
  export const PatternNever = (input) => If(Token.Const('(?!)', input), ([_0, input]) => [S.PatternNeverMapping(_0), input]);
116
- export const PatternText = (input) => If(Token.Until_1(['-?(?:0|[1-9][0-9]*)n', '.*', '-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?', '-?(?:0|[1-9][0-9]*)', '(?!)', '(', ')', '$', '|'], input), ([_0, input]) => [S.PatternTextMapping(_0), input]);
116
+ export const PatternText = (input) => If(Token.Until_1(['-?(?:0|[1-9][0-9]*)n', '.*', '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?', '-?(?:0|[1-9][0-9]*)', '(?!)', '(', ')', '$', '|'], input), ([_0, input]) => [S.PatternTextMapping(_0), input]);
117
117
  export const PatternBase = (input) => If(If(PatternBigInt(input), ([_0, input]) => [_0, input], () => If(PatternString(input), ([_0, input]) => [_0, input], () => If(PatternNumber(input), ([_0, input]) => [_0, input], () => If(PatternInteger(input), ([_0, input]) => [_0, input], () => If(PatternNever(input), ([_0, input]) => [_0, input], () => If(PatternGroup(input), ([_0, input]) => [_0, input], () => If(PatternText(input), ([_0, input]) => [_0, input], () => []))))))), ([_0, input]) => [S.PatternBaseMapping(_0), input]);
118
118
  export const PatternGroup = (input) => If(If(Token.Const('(', input), ([_0, input]) => If(PatternBody(input), ([_1, input]) => If(Token.Const(')', input), ([_2, input]) => [[_0, _1, _2], input]))), ([_0, input]) => [S.PatternGroupMapping(_0), input]);
119
119
  export const PatternUnion = (input) => If(If(If(PatternTerm(input), ([_0, input]) => If(Token.Const('|', input), ([_1, input]) => If(PatternUnion(input), ([_2, input]) => [[_0, _1, _2], input]))), ([_0, input]) => [_0, input], () => If(If(PatternTerm(input), ([_0, input]) => [[_0], input]), ([_0, input]) => [_0, input], () => If([[], input], ([_0, input]) => [_0, input], () => []))), ([_0, input]) => [S.PatternUnionMapping(_0), input]);
@@ -1,5 +1,5 @@
1
1
  import { type TSchema, type TNumberOptions } from './schema.mjs';
2
- export declare const NumberPattern = "-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?";
2
+ export declare const NumberPattern = "-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?";
3
3
  export type StaticNumber = number;
4
4
  /** Represents a Number type. */
5
5
  export interface TNumber extends TSchema {
@@ -4,7 +4,7 @@ import { IsKind } from './schema.mjs';
4
4
  // ------------------------------------------------------------------
5
5
  // Pattern
6
6
  // ------------------------------------------------------------------
7
- export const NumberPattern = '-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?';
7
+ export const NumberPattern = '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?';
8
8
  // ------------------------------------------------------------------
9
9
  // Factory
10
10
  // ------------------------------------------------------------------
@@ -14,7 +14,7 @@ export type TStringKey = typeof StringKey;
14
14
  export type TIntegerKey = typeof IntegerKey;
15
15
  export type TNumberKey = typeof NumberKey;
16
16
  export declare const IntegerKey = "^-?(?:0|[1-9][0-9]*)$";
17
- export declare const NumberKey = "^-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?$";
17
+ export declare const NumberKey = "^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$";
18
18
  export declare const StringKey = "^.*$";
19
19
  export interface TRecord<Key extends string = string, Value extends TSchema = TSchema> extends TSchema {
20
20
  '~kind': 'Record';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typebox",
3
3
  "description": "Json Schema Type Builder with Static Type Resolution for TypeScript",
4
- "version": "1.2.0",
4
+ "version": "1.2.1",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "jsonschema"