@jmanuelcorral/openteam 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,883 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { execFile } from "node:child_process";
5
+ import { readFile as readFile2 } from "node:fs/promises";
6
+ import { promisify } from "node:util";
7
+
8
+ // src/config/schema.ts
9
+ import { z } from "zod";
10
+ var ModelRefSchema = z.object({
11
+ providerID: z.string().min(1),
12
+ modelID: z.string().min(1)
13
+ });
14
+ var RouterModeSchema = z.enum(["economy", "balanced", "quality"]);
15
+ var PrivacyModeSchema = z.enum([
16
+ "forceLocalOnSensitive",
17
+ "consentBeforeFrontier",
18
+ "off"
19
+ ]);
20
+ var BaselineModeSchema = z.enum(["auto", "pinned"]);
21
+ var defaultLocalModel = {
22
+ providerID: "ollama",
23
+ modelID: "qwen3:8b"
24
+ };
25
+ var defaultFrontierModel = {
26
+ providerID: "anthropic",
27
+ modelID: "claude-sonnet-4-5"
28
+ };
29
+ var LocalRuntimeSchema = z.object({
30
+ id: z.enum(["ollama", "lmstudio", "foundry-local"]),
31
+ enabled: z.boolean().default(true),
32
+ baseURL: z.string().url().optional(),
33
+ discovery: z.enum(["cli", "sdk", "manual"]).optional(),
34
+ defaultModel: ModelRefSchema
35
+ });
36
+ var OpenTeamConfigSchema = z.object({
37
+ baseline: z.object({
38
+ mode: BaselineModeSchema.default("auto"),
39
+ pinnedModel: ModelRefSchema.nullable().default(null),
40
+ hardDefault: ModelRefSchema.default(defaultFrontierModel)
41
+ }).default({
42
+ mode: "auto",
43
+ pinnedModel: null,
44
+ hardDefault: defaultFrontierModel
45
+ }),
46
+ router: z.object({
47
+ mode: RouterModeSchema.default("balanced"),
48
+ localDefault: ModelRefSchema.default(defaultLocalModel),
49
+ trivialPromptMaxChars: z.number().int().positive().default(280),
50
+ frontierPromptMinChars: z.number().int().positive().default(2000)
51
+ }).default({
52
+ mode: "balanced",
53
+ localDefault: defaultLocalModel,
54
+ trivialPromptMaxChars: 280,
55
+ frontierPromptMinChars: 2000
56
+ }),
57
+ local: z.object({
58
+ runtimes: z.array(LocalRuntimeSchema).min(1).default([
59
+ {
60
+ id: "ollama",
61
+ enabled: true,
62
+ baseURL: "http://localhost:11434/v1",
63
+ defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
64
+ }
65
+ ])
66
+ }).default({
67
+ runtimes: [
68
+ {
69
+ id: "ollama",
70
+ enabled: true,
71
+ baseURL: "http://localhost:11434/v1",
72
+ defaultModel: defaultLocalModel
73
+ }
74
+ ]
75
+ }),
76
+ budgets: z.object({
77
+ sessionUSD: z.number().positive().optional(),
78
+ monthlyUSD: z.number().positive().optional(),
79
+ frontierTokensPerSession: z.number().int().positive().optional(),
80
+ hardStopOnBudgetExhaustion: z.boolean().default(false)
81
+ }).default({ hardStopOnBudgetExhaustion: false }),
82
+ privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive")
83
+ });
84
+
85
+ // src/commands/baseline.ts
86
+ function formatRef(ref) {
87
+ return ref === null ? "—" : `${ref.providerID}/${ref.modelID}`;
88
+ }
89
+ function effectiveBaseline(config) {
90
+ if (config.baseline.mode === "pinned" && config.baseline.pinnedModel !== null) {
91
+ return `${formatRef(config.baseline.pinnedModel)} (pinned)`;
92
+ }
93
+ return "cheapest-capable (auto)";
94
+ }
95
+ function parseModelRef(input) {
96
+ const trimmed = input.trim();
97
+ const slash = trimmed.indexOf("/");
98
+ if (slash <= 0 || slash === trimmed.length - 1) {
99
+ throw new Error(`Modelo inválido "${input}". Usa el formato provider/model, p.ej. anthropic/claude-sonnet-4-5.`);
100
+ }
101
+ return ModelRefSchema.parse({
102
+ providerID: trimmed.slice(0, slash),
103
+ modelID: trimmed.slice(slash + 1)
104
+ });
105
+ }
106
+ function showBaseline(config) {
107
+ const lines = [
108
+ "openteam baseline:",
109
+ ` modo: ${config.baseline.mode}`,
110
+ ` pinned: ${formatRef(config.baseline.pinnedModel)}`,
111
+ ` hardDefault: ${formatRef(config.baseline.hardDefault)}`,
112
+ ` efectivo: ${effectiveBaseline(config)}`
113
+ ];
114
+ return { message: lines.join(`
115
+ `) };
116
+ }
117
+ function setBaseline(config, ref) {
118
+ const next = {
119
+ ...config,
120
+ baseline: {
121
+ ...config.baseline,
122
+ mode: "pinned",
123
+ pinnedModel: ref
124
+ }
125
+ };
126
+ return {
127
+ config: next,
128
+ message: `Baseline fijado a ${formatRef(ref)} (modo pinned).`
129
+ };
130
+ }
131
+ function autoBaseline(config) {
132
+ const next = {
133
+ ...config,
134
+ baseline: {
135
+ ...config.baseline,
136
+ mode: "auto",
137
+ pinnedModel: null
138
+ }
139
+ };
140
+ return {
141
+ config: next,
142
+ message: "Baseline en modo auto (cheapest-capable)."
143
+ };
144
+ }
145
+
146
+ // src/commands/doctor.ts
147
+ function runtimeLine(snapshot) {
148
+ const mark = snapshot.reachable ? "✓" : "✗";
149
+ const detail = snapshot.reachable ? `${snapshot.models.length} modelo(s)` : snapshot.error ?? "inalcanzable";
150
+ return ` ${mark} ${snapshot.id.padEnd(14)} ${snapshot.baseURL || "(sin baseURL)"} — ${detail}`;
151
+ }
152
+ function renderDoctor(input) {
153
+ const enabledRuntimes = input.config.local.runtimes.filter((r) => r.enabled);
154
+ const reachable = input.snapshots.filter((s) => s.reachable).length;
155
+ const lines = [
156
+ "openteam doctor:",
157
+ ` baseline: ${input.config.baseline.mode}` + (input.config.baseline.mode === "pinned" && input.config.baseline.pinnedModel !== null ? ` (${input.config.baseline.pinnedModel.providerID}/${input.config.baseline.pinnedModel.modelID})` : ""),
158
+ ` privacidad: ${input.config.privacyMode}`,
159
+ ` runtimes locales (${reachable}/${enabledRuntimes.length} alcanzables):`
160
+ ];
161
+ if (input.snapshots.length === 0) {
162
+ lines.push(" (ningún runtime habilitado)");
163
+ } else {
164
+ for (const snapshot of input.snapshots) {
165
+ lines.push(runtimeLine(snapshot));
166
+ }
167
+ }
168
+ lines.push(` telemetría: ${input.telemetryPath} — ${input.telemetryRecords} registro(s)`);
169
+ if (reachable === 0) {
170
+ lines.push(" ⚠ Ningún runtime local alcanzable: todo el tráfico irá a frontier.");
171
+ }
172
+ return lines.join(`
173
+ `);
174
+ }
175
+
176
+ // src/commands/report.ts
177
+ var TIERS = [
178
+ "trivial",
179
+ "simple",
180
+ "moderate",
181
+ "hard"
182
+ ];
183
+ function emptyTierSummary() {
184
+ return {
185
+ trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
186
+ simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
187
+ moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
188
+ hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
189
+ };
190
+ }
191
+ function summarizeCostRecords(records) {
192
+ const report = {
193
+ count: 0,
194
+ localCount: 0,
195
+ frontierCount: 0,
196
+ totalEstimatedUSD: 0,
197
+ totalBaselineUSD: 0,
198
+ totalSavingsUSD: 0,
199
+ savingsPct: 0,
200
+ tokensIn: 0,
201
+ tokensOut: 0,
202
+ byTier: emptyTierSummary()
203
+ };
204
+ for (const record of records) {
205
+ report.count += 1;
206
+ if (record.routeKind === "local") {
207
+ report.localCount += 1;
208
+ } else {
209
+ report.frontierCount += 1;
210
+ }
211
+ report.totalEstimatedUSD += record.estimatedCostUSD;
212
+ report.totalBaselineUSD += record.baselineCostUSD;
213
+ report.totalSavingsUSD += record.estimatedSavingsUSD;
214
+ report.tokensIn += record.tokensIn ?? 0;
215
+ report.tokensOut += record.tokensOut ?? 0;
216
+ const tier = report.byTier[record.tier];
217
+ tier.count += 1;
218
+ tier.estimatedUSD += record.estimatedCostUSD;
219
+ tier.savingsUSD += record.estimatedSavingsUSD;
220
+ }
221
+ report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
222
+ return report;
223
+ }
224
+ function usd(value) {
225
+ return `$${value.toFixed(5)}`;
226
+ }
227
+ function renderReport(report) {
228
+ if (report.count === 0) {
229
+ return "openteam report: sin registros de telemetría todavía.";
230
+ }
231
+ const lines = [
232
+ "openteam report:",
233
+ ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
234
+ ` coste real: ${usd(report.totalEstimatedUSD)}`,
235
+ ` baseline: ${usd(report.totalBaselineUSD)}`,
236
+ ` ahorro: ${usd(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
237
+ ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
238
+ " por tier:"
239
+ ];
240
+ for (const tier of TIERS) {
241
+ const summary = report.byTier[tier];
242
+ lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd(summary.estimatedUSD)} · ahorro ${usd(summary.savingsUSD)}`);
243
+ }
244
+ return lines.join(`
245
+ `);
246
+ }
247
+
248
+ // src/commands/dispatch.ts
249
+ var HELP = [
250
+ "openteam — comandos runtime",
251
+ "",
252
+ "Uso:",
253
+ " openteam baseline show Muestra el baseline efectivo",
254
+ " openteam baseline set <p/model> Fija el baseline (modo pinned)",
255
+ " openteam baseline auto Baseline cheapest-capable (modo auto)",
256
+ " openteam doctor Diagnóstico de runtimes y config",
257
+ " openteam report Resumen de coste/ahorro (telemetría)",
258
+ "",
259
+ "Opciones:",
260
+ " --config <path> Ruta de .opencode/openteam.json",
261
+ " --telemetry <path> Ruta del JSONL de telemetría"
262
+ ].join(`
263
+ `);
264
+ function parseArgs(argv) {
265
+ const positionals = [];
266
+ const result = { positionals };
267
+ for (let i = 0;i < argv.length; i += 1) {
268
+ const arg = argv[i];
269
+ if (arg === "--config") {
270
+ const value = argv[i + 1];
271
+ if (value !== undefined) {
272
+ result.configPath = value;
273
+ }
274
+ i += 1;
275
+ } else if (arg === "--telemetry") {
276
+ const value = argv[i + 1];
277
+ if (value !== undefined) {
278
+ result.telemetryPath = value;
279
+ }
280
+ i += 1;
281
+ } else if (arg !== undefined) {
282
+ positionals.push(arg);
283
+ }
284
+ }
285
+ return result;
286
+ }
287
+ async function runBaseline(positionals, deps, configPath) {
288
+ const sub = positionals[1];
289
+ if (sub === "show" || sub === undefined) {
290
+ const config = await deps.loadConfig(configPath);
291
+ return { exitCode: 0, stdout: showBaseline(config).message };
292
+ }
293
+ if (sub === "auto") {
294
+ const config = await deps.loadConfig(configPath);
295
+ const outcome = autoBaseline(config);
296
+ if (outcome.config !== undefined) {
297
+ await deps.saveConfig(outcome.config, configPath);
298
+ }
299
+ return { exitCode: 0, stdout: outcome.message };
300
+ }
301
+ if (sub === "set") {
302
+ const target = positionals[2];
303
+ if (target === undefined) {
304
+ return {
305
+ exitCode: 1,
306
+ stdout: "Falta el modelo. Uso: openteam baseline set <provider/model>"
307
+ };
308
+ }
309
+ const config = await deps.loadConfig(configPath);
310
+ const outcome = setBaseline(config, parseModelRef(target));
311
+ if (outcome.config !== undefined) {
312
+ await deps.saveConfig(outcome.config, configPath);
313
+ }
314
+ return { exitCode: 0, stdout: outcome.message };
315
+ }
316
+ return {
317
+ exitCode: 1,
318
+ stdout: `Subcomando de baseline desconocido: ${sub}
319
+
320
+ ${HELP}`
321
+ };
322
+ }
323
+ async function runCli(argv, deps) {
324
+ const parsed = parseArgs(argv);
325
+ const command = parsed.positionals[0];
326
+ const configPath = parsed.configPath ?? deps.configPath;
327
+ const telemetryPath = parsed.telemetryPath ?? deps.telemetryPath;
328
+ try {
329
+ if (command === "baseline") {
330
+ return await runBaseline(parsed.positionals, deps, configPath);
331
+ }
332
+ if (command === "doctor") {
333
+ const config = await deps.loadConfig(configPath);
334
+ const [snapshots, records] = await Promise.all([
335
+ deps.probe(config),
336
+ deps.readTelemetry(telemetryPath)
337
+ ]);
338
+ return {
339
+ exitCode: 0,
340
+ stdout: renderDoctor({
341
+ config,
342
+ snapshots,
343
+ telemetryPath,
344
+ telemetryRecords: records.length
345
+ })
346
+ };
347
+ }
348
+ if (command === "report") {
349
+ const records = await deps.readTelemetry(telemetryPath);
350
+ return {
351
+ exitCode: 0,
352
+ stdout: renderReport(summarizeCostRecords(records))
353
+ };
354
+ }
355
+ if (command === undefined || command === "help" || command === "--help") {
356
+ return { exitCode: 0, stdout: HELP };
357
+ }
358
+ return {
359
+ exitCode: 1,
360
+ stdout: `Comando desconocido: ${command}
361
+
362
+ ${HELP}`
363
+ };
364
+ } catch (error) {
365
+ const message = error instanceof Error ? error.message : String(error);
366
+ return { exitCode: 1, stdout: `Error: ${message}` };
367
+ }
368
+ }
369
+
370
+ // src/config/load.ts
371
+ function loadOpenTeamConfig(raw) {
372
+ return OpenTeamConfigSchema.parse(raw ?? {});
373
+ }
374
+
375
+ // src/config/persist.ts
376
+ import { mkdir, writeFile } from "node:fs/promises";
377
+ import { dirname } from "node:path";
378
+ var DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
379
+ function serializeOpenTeamConfig(config) {
380
+ const validated = OpenTeamConfigSchema.parse(config);
381
+ return `${JSON.stringify(validated, null, 2)}
382
+ `;
383
+ }
384
+ async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, deps = {}) {
385
+ const mkdirFn = deps.mkdir ?? mkdir;
386
+ const writeFileFn = deps.writeFile ?? writeFile;
387
+ await mkdirFn(dirname(path), { recursive: true });
388
+ await writeFileFn(path, serializeOpenTeamConfig(config), "utf8");
389
+ }
390
+
391
+ // src/local/openai-compatible.ts
392
+ function normalizeBaseURL(baseURL) {
393
+ return baseURL.replace(/\/+$/, "");
394
+ }
395
+ function modelsEndpoint(baseURL) {
396
+ return `${normalizeBaseURL(baseURL)}/models`;
397
+ }
398
+ function errorMessage(error) {
399
+ if (error instanceof Error) {
400
+ return error.message;
401
+ }
402
+ return String(error);
403
+ }
404
+ async function listOpenAICompatibleModels(baseURL, fetch) {
405
+ const response = await fetch(modelsEndpoint(baseURL), {
406
+ method: "GET",
407
+ headers: { accept: "application/json" }
408
+ });
409
+ if (!response.ok) {
410
+ throw new Error(`GET /models failed with HTTP ${response.status}`);
411
+ }
412
+ let payload;
413
+ try {
414
+ payload = await response.json();
415
+ } catch (error) {
416
+ throw new Error(`Malformed JSON from /models: ${errorMessage(error)}`);
417
+ }
418
+ return parseOpenAIModels(payload);
419
+ }
420
+ function parseOpenAIModels(payload) {
421
+ if (!isObject(payload) || !Array.isArray(payload.data)) {
422
+ throw new Error("Malformed /models response: expected data array");
423
+ }
424
+ return payload.data.map(parseOpenAIModel);
425
+ }
426
+ function unavailableSnapshot(input) {
427
+ return {
428
+ id: input.id,
429
+ baseURL: input.baseURL,
430
+ reachable: false,
431
+ models: [],
432
+ probedAt: input.probedAt,
433
+ error: errorMessage(input.error)
434
+ };
435
+ }
436
+ async function probeOpenAICompatibleRuntime(input) {
437
+ const normalizedBaseURL = normalizeBaseURL(input.baseURL);
438
+ try {
439
+ return {
440
+ id: input.id,
441
+ baseURL: normalizedBaseURL,
442
+ reachable: true,
443
+ models: await listOpenAICompatibleModels(normalizedBaseURL, input.fetch),
444
+ probedAt: input.probedAt
445
+ };
446
+ } catch (error) {
447
+ return unavailableSnapshot({
448
+ id: input.id,
449
+ baseURL: normalizedBaseURL,
450
+ probedAt: input.probedAt,
451
+ error
452
+ });
453
+ }
454
+ }
455
+ function parseOpenAIModel(value) {
456
+ if (!isObject(value) || typeof value.id !== "string" || value.id.length === 0) {
457
+ throw new Error("Malformed /models response: model id must be a string");
458
+ }
459
+ const contextWindow = readContextWindow(value);
460
+ const model = {
461
+ modelID: value.id,
462
+ supportsTools: readSupportsTools(value)
463
+ };
464
+ if (contextWindow !== undefined) {
465
+ model.contextWindow = contextWindow;
466
+ }
467
+ return model;
468
+ }
469
+ function readSupportsTools(model) {
470
+ const direct = readBoolean(model, [
471
+ "tool_call",
472
+ "tool_calls",
473
+ "toolCalling",
474
+ "supportsToolCalling",
475
+ "supports_tools",
476
+ "supportsTools"
477
+ ]);
478
+ if (direct !== undefined) {
479
+ return direct;
480
+ }
481
+ const capabilities = readCapabilityBoolean(model.capabilities);
482
+ if (capabilities !== undefined) {
483
+ return capabilities;
484
+ }
485
+ const features = readCapabilityBoolean(model.features);
486
+ if (features !== undefined) {
487
+ return features;
488
+ }
489
+ return "unknown";
490
+ }
491
+ function readCapabilityBoolean(value) {
492
+ if (Array.isArray(value)) {
493
+ if (value.some((entry) => typeof entry === "string" && ["tools", "tool_call", "tool-calling", "function_calling"].includes(entry))) {
494
+ return true;
495
+ }
496
+ return;
497
+ }
498
+ if (!isObject(value)) {
499
+ return;
500
+ }
501
+ return readBoolean(value, [
502
+ "tools",
503
+ "tool_call",
504
+ "tool_calls",
505
+ "toolCalling",
506
+ "function_calling",
507
+ "supportsToolCalling"
508
+ ]);
509
+ }
510
+ function readContextWindow(model) {
511
+ const direct = readPositiveInteger(model, [
512
+ "contextWindow",
513
+ "context_window",
514
+ "context_length",
515
+ "max_context_length",
516
+ "n_ctx"
517
+ ]);
518
+ if (direct !== undefined) {
519
+ return direct;
520
+ }
521
+ if (isObject(model.limit)) {
522
+ return readPositiveInteger(model.limit, ["context"]);
523
+ }
524
+ return;
525
+ }
526
+ function readBoolean(object, keys) {
527
+ for (const key of keys) {
528
+ const value = object[key];
529
+ if (typeof value === "boolean") {
530
+ return value;
531
+ }
532
+ }
533
+ return;
534
+ }
535
+ function readPositiveInteger(object, keys) {
536
+ for (const key of keys) {
537
+ const value = object[key];
538
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
539
+ return value;
540
+ }
541
+ }
542
+ return;
543
+ }
544
+ function isObject(value) {
545
+ return typeof value === "object" && value !== null && !Array.isArray(value);
546
+ }
547
+
548
+ // src/local/foundry-local.ts
549
+ async function discoverFoundryLocalBaseURL(input) {
550
+ const result = await input.exec(input.command ?? "foundry", [
551
+ ...input.args ?? ["service", "status"]
552
+ ]);
553
+ if (result.exitCode !== 0) {
554
+ throw new Error(`Foundry Local discovery failed with exit code ${result.exitCode}: ${result.stderr || result.stdout}`);
555
+ }
556
+ const endpoint = parseFoundryEndpoint(`${result.stdout}
557
+ ${result.stderr}`);
558
+ if (endpoint === undefined) {
559
+ throw new Error("Foundry Local discovery did not return an endpoint");
560
+ }
561
+ return endpoint;
562
+ }
563
+ function createFoundryLocalAdapter() {
564
+ return {
565
+ id: "foundry-local",
566
+ listModels: async (options) => {
567
+ const baseURL = await resolveFoundryBaseURL(options);
568
+ return listOpenAICompatibleModels(baseURL, options.fetch);
569
+ },
570
+ probe: async (options) => {
571
+ let baseURL = options.baseURL ?? "";
572
+ try {
573
+ baseURL = await resolveFoundryBaseURL(options);
574
+ } catch (error) {
575
+ return unavailableSnapshot({
576
+ id: "foundry-local",
577
+ baseURL,
578
+ probedAt: options.probedAt,
579
+ error
580
+ });
581
+ }
582
+ return probeOpenAICompatibleRuntime({
583
+ id: "foundry-local",
584
+ baseURL,
585
+ fetch: options.fetch,
586
+ probedAt: options.probedAt
587
+ });
588
+ }
589
+ };
590
+ }
591
+ async function resolveFoundryBaseURL(options) {
592
+ if (options.baseURL !== undefined) {
593
+ return normalizeBaseURL(options.baseURL);
594
+ }
595
+ if (options.discovery === undefined) {
596
+ throw new Error("Foundry Local discovery is required when baseURL is absent");
597
+ }
598
+ return normalizeFoundryDiscovery(await options.discovery());
599
+ }
600
+ function normalizeFoundryDiscovery(discovery) {
601
+ if (typeof discovery === "string") {
602
+ return ensureFoundryV1BaseURL(discovery);
603
+ }
604
+ if ("baseURL" in discovery) {
605
+ return ensureFoundryV1BaseURL(discovery.baseURL);
606
+ }
607
+ const host = discovery.host ?? "localhost";
608
+ const protocol = discovery.protocol ?? "http";
609
+ return `${protocol}://${host}:${discovery.port}/v1`;
610
+ }
611
+ function foundryDiscoveryFromExec(exec) {
612
+ return () => discoverFoundryLocalBaseURL({ exec });
613
+ }
614
+ function parseFoundryEndpoint(output) {
615
+ const explicitURL = output.match(/https?:\/\/[^\s"'<>),]+/u)?.[0];
616
+ if (explicitURL !== undefined) {
617
+ return ensureFoundryV1BaseURL(explicitURL);
618
+ }
619
+ const localhostPort = output.match(/(?:localhost|127\.0\.0\.1|\[::1\])\s*:\s*(\d{2,5})/u);
620
+ const port = localhostPort?.[1];
621
+ if (port !== undefined) {
622
+ return `http://localhost:${port}/v1`;
623
+ }
624
+ return;
625
+ }
626
+ function ensureFoundryV1BaseURL(endpoint) {
627
+ const normalizedEndpoint = endpoint.trim().replace(/[),.;]+$/u, "");
628
+ const withProtocol = /^[a-z]+:\/\//iu.test(normalizedEndpoint) ? normalizedEndpoint : `http://${normalizedEndpoint}`;
629
+ try {
630
+ const url = new URL(withProtocol);
631
+ const path = normalizeBaseURL(url.pathname);
632
+ if (path.endsWith("/v1")) {
633
+ return normalizeBaseURL(url.toString());
634
+ }
635
+ return `${url.origin}/v1`;
636
+ } catch (error) {
637
+ throw new Error(`Invalid Foundry Local endpoint: ${errorMessage(error)}`);
638
+ }
639
+ }
640
+
641
+ // src/local/lmstudio.ts
642
+ var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
643
+ function createLMStudioAdapter() {
644
+ return {
645
+ id: "lmstudio",
646
+ defaultBaseURL: LMSTUDIO_DEFAULT_BASE_URL,
647
+ listModels(options) {
648
+ return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? LMSTUDIO_DEFAULT_BASE_URL), options.fetch);
649
+ },
650
+ probe(options) {
651
+ return probeOpenAICompatibleRuntime({
652
+ id: "lmstudio",
653
+ baseURL: options.baseURL ?? LMSTUDIO_DEFAULT_BASE_URL,
654
+ fetch: options.fetch,
655
+ probedAt: options.probedAt
656
+ });
657
+ }
658
+ };
659
+ }
660
+
661
+ // src/local/ollama.ts
662
+ var OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1";
663
+ function createOllamaAdapter() {
664
+ return {
665
+ id: "ollama",
666
+ defaultBaseURL: OLLAMA_DEFAULT_BASE_URL,
667
+ listModels(options) {
668
+ return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? OLLAMA_DEFAULT_BASE_URL), options.fetch);
669
+ },
670
+ probe(options) {
671
+ return probeOpenAICompatibleRuntime({
672
+ id: "ollama",
673
+ baseURL: options.baseURL ?? OLLAMA_DEFAULT_BASE_URL,
674
+ fetch: options.fetch,
675
+ probedAt: options.probedAt
676
+ });
677
+ }
678
+ };
679
+ }
680
+
681
+ // src/local/registry.ts
682
+ class RuntimeRegistry {
683
+ adapters;
684
+ clock;
685
+ fetch;
686
+ foundryDiscovery;
687
+ constructor(options) {
688
+ this.adapters = {
689
+ ollama: createOllamaAdapter(),
690
+ lmstudio: createLMStudioAdapter(),
691
+ "foundry-local": createFoundryLocalAdapter(),
692
+ ...options.adapters
693
+ };
694
+ this.clock = options.clock;
695
+ this.fetch = options.fetch;
696
+ this.foundryDiscovery = options.foundryDiscovery;
697
+ }
698
+ async probe(runtimes) {
699
+ const snapshots = [];
700
+ for (const runtime of runtimes) {
701
+ if (!runtime.enabled) {
702
+ continue;
703
+ }
704
+ const probedAt = this.clock();
705
+ const adapter = this.adapters[runtime.id];
706
+ const options = this.createProbeOptions(runtime, probedAt);
707
+ try {
708
+ snapshots.push(await adapter.probe(options));
709
+ } catch (error) {
710
+ snapshots.push(unavailableSnapshot({
711
+ id: runtime.id,
712
+ baseURL: runtime.baseURL ?? adapter.defaultBaseURL ?? "",
713
+ probedAt,
714
+ error: `Adapter threw unexpectedly: ${errorMessage(error)}`
715
+ }));
716
+ }
717
+ }
718
+ return snapshots;
719
+ }
720
+ createProbeOptions(runtime, probedAt) {
721
+ const options = {
722
+ fetch: this.fetch,
723
+ probedAt
724
+ };
725
+ if (runtime.baseURL !== undefined) {
726
+ options.baseURL = runtime.baseURL;
727
+ }
728
+ if (runtime.id === "foundry-local" && runtime.discovery !== "manual" && this.foundryDiscovery !== undefined) {
729
+ options.discovery = this.foundryDiscovery;
730
+ }
731
+ return options;
732
+ }
733
+ }
734
+
735
+ // src/telemetry/read.ts
736
+ import { readFile } from "node:fs/promises";
737
+
738
+ // src/telemetry/types.ts
739
+ import { z as z3 } from "zod";
740
+
741
+ // src/capabilities/types.ts
742
+ import { z as z2 } from "zod";
743
+ var CapabilityTierSchema = z2.union([
744
+ z2.literal(0),
745
+ z2.literal(1),
746
+ z2.literal(2),
747
+ z2.literal(3),
748
+ z2.literal(4),
749
+ z2.literal(5)
750
+ ]);
751
+ var ComplexityTierSchema = z2.enum([
752
+ "trivial",
753
+ "simple",
754
+ "moderate",
755
+ "hard"
756
+ ]);
757
+ var ModelCapabilityProfileSchema = z2.object({
758
+ ref: z2.object({
759
+ providerID: z2.string().min(1),
760
+ modelID: z2.string().min(1)
761
+ }),
762
+ kind: z2.enum(["local", "frontier", "router"]),
763
+ contextWindow: z2.number().int().positive(),
764
+ maxOutputTokens: z2.number().int().positive(),
765
+ supportsToolCalling: z2.boolean(),
766
+ supportsVision: z2.boolean(),
767
+ reasoningTier: CapabilityTierSchema,
768
+ codeQualityTier: CapabilityTierSchema,
769
+ costPer1M: z2.object({
770
+ inputUSD: z2.number().min(0),
771
+ outputUSD: z2.number().min(0)
772
+ }),
773
+ availability: z2.enum(["available", "degraded", "unavailable"])
774
+ });
775
+
776
+ // src/telemetry/types.ts
777
+ var CostRecordSchema = z3.object({
778
+ ts: z3.number().finite(),
779
+ sessionID: z3.string().min(1).optional(),
780
+ promptHash: z3.string().min(1),
781
+ promptChars: z3.number().int().min(0),
782
+ tier: ComplexityTierSchema,
783
+ routeKind: z3.enum(["local", "frontier"]),
784
+ selected: ModelRefSchema,
785
+ rationale: z3.string(),
786
+ estimatedCostUSD: z3.number().finite().min(0),
787
+ baselineCostUSD: z3.number().finite().min(0),
788
+ estimatedSavingsUSD: z3.number().finite(),
789
+ budgetAction: z3.string().min(1),
790
+ tokensIn: z3.number().int().min(0).optional(),
791
+ tokensOut: z3.number().int().min(0).optional()
792
+ });
793
+
794
+ // src/telemetry/read.ts
795
+ var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
796
+ function parseCostRecordsJsonl(text) {
797
+ const records = [];
798
+ for (const line of text.split(`
799
+ `)) {
800
+ const trimmed = line.trim();
801
+ if (trimmed.length === 0) {
802
+ continue;
803
+ }
804
+ let candidate;
805
+ try {
806
+ candidate = JSON.parse(trimmed);
807
+ } catch {
808
+ continue;
809
+ }
810
+ const parsed = CostRecordSchema.safeParse(candidate);
811
+ if (parsed.success) {
812
+ records.push(parsed.data);
813
+ }
814
+ }
815
+ return records;
816
+ }
817
+ function isMissingFile(error) {
818
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
819
+ }
820
+ async function readCostRecords(path, deps = {}) {
821
+ const readFileFn = deps.readFile ?? readFile;
822
+ try {
823
+ const content = await readFileFn(path, "utf8");
824
+ return parseCostRecordsJsonl(content);
825
+ } catch (error) {
826
+ if (isMissingFile(error)) {
827
+ return [];
828
+ }
829
+ throw error;
830
+ }
831
+ }
832
+
833
+ // src/cli.ts
834
+ var execFileAsync = promisify(execFile);
835
+ function isMissingFile2(error) {
836
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
837
+ }
838
+ var nodeExec = async (command, args) => {
839
+ try {
840
+ const { stdout, stderr } = await execFileAsync(command, [...args]);
841
+ return { exitCode: 0, stdout, stderr };
842
+ } catch (error) {
843
+ const err = error;
844
+ return {
845
+ exitCode: typeof err.code === "number" ? err.code : 1,
846
+ stdout: err.stdout ?? "",
847
+ stderr: err.stderr ?? String(error)
848
+ };
849
+ }
850
+ };
851
+ async function loadConfig(path) {
852
+ try {
853
+ const content = await readFile2(path, "utf8");
854
+ return loadOpenTeamConfig(JSON.parse(content));
855
+ } catch (error) {
856
+ if (isMissingFile2(error)) {
857
+ return loadOpenTeamConfig({});
858
+ }
859
+ throw error;
860
+ }
861
+ }
862
+ var deps = {
863
+ loadConfig,
864
+ saveConfig: (config, path) => writeOpenTeamConfigFile(config, path),
865
+ probe: (config) => {
866
+ const registry = new RuntimeRegistry({
867
+ fetch: globalThis.fetch,
868
+ clock: () => new Date().toISOString(),
869
+ foundryDiscovery: foundryDiscoveryFromExec(nodeExec)
870
+ });
871
+ return registry.probe(config.local.runtimes);
872
+ },
873
+ readTelemetry: (path) => readCostRecords(path),
874
+ configPath: DEFAULT_CONFIG_PATH,
875
+ telemetryPath: DEFAULT_TELEMETRY_PATH
876
+ };
877
+ async function main() {
878
+ const result = await runCli(process.argv.slice(2), deps);
879
+ process.stdout.write(`${result.stdout}
880
+ `);
881
+ process.exitCode = result.exitCode;
882
+ }
883
+ main();