@jmanuelcorral/openteam 0.1.3 → 0.1.5

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 CHANGED
@@ -2,9 +2,475 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execFile } from "node:child_process";
5
- import { readFile as readFile2 } from "node:fs/promises";
5
+ import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
6
+ import { dirname as dirname2 } from "node:path";
6
7
  import { promisify } from "node:util";
7
8
 
9
+ // src/cli/initAdapters.ts
10
+ import {
11
+ cancel,
12
+ confirm,
13
+ intro,
14
+ isCancel,
15
+ multiselect,
16
+ note,
17
+ outro,
18
+ select,
19
+ text
20
+ } from "@clack/prompts";
21
+
22
+ // src/local/openai-compatible.ts
23
+ function normalizeBaseURL(baseURL) {
24
+ return baseURL.replace(/\/+$/, "");
25
+ }
26
+ function modelsEndpoint(baseURL) {
27
+ return `${normalizeBaseURL(baseURL)}/models`;
28
+ }
29
+ function errorMessage(error) {
30
+ if (error instanceof Error) {
31
+ return error.message;
32
+ }
33
+ return String(error);
34
+ }
35
+ async function listOpenAICompatibleModels(baseURL, fetch) {
36
+ const response = await fetch(modelsEndpoint(baseURL), {
37
+ method: "GET",
38
+ headers: { accept: "application/json" }
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`GET /models failed with HTTP ${response.status}`);
42
+ }
43
+ let payload;
44
+ try {
45
+ payload = await response.json();
46
+ } catch (error) {
47
+ throw new Error(`Malformed JSON from /models: ${errorMessage(error)}`);
48
+ }
49
+ return parseOpenAIModels(payload);
50
+ }
51
+ function parseOpenAIModels(payload) {
52
+ if (!isObject(payload) || !Array.isArray(payload.data)) {
53
+ throw new Error("Malformed /models response: expected data array");
54
+ }
55
+ return payload.data.map(parseOpenAIModel);
56
+ }
57
+ function unavailableSnapshot(input) {
58
+ return {
59
+ id: input.id,
60
+ baseURL: input.baseURL,
61
+ reachable: false,
62
+ models: [],
63
+ probedAt: input.probedAt,
64
+ error: errorMessage(input.error)
65
+ };
66
+ }
67
+ async function probeOpenAICompatibleRuntime(input) {
68
+ const normalizedBaseURL = normalizeBaseURL(input.baseURL);
69
+ try {
70
+ return {
71
+ id: input.id,
72
+ baseURL: normalizedBaseURL,
73
+ reachable: true,
74
+ models: await listOpenAICompatibleModels(normalizedBaseURL, input.fetch),
75
+ probedAt: input.probedAt
76
+ };
77
+ } catch (error) {
78
+ return unavailableSnapshot({
79
+ id: input.id,
80
+ baseURL: normalizedBaseURL,
81
+ probedAt: input.probedAt,
82
+ error
83
+ });
84
+ }
85
+ }
86
+ function parseOpenAIModel(value) {
87
+ if (!isObject(value) || typeof value.id !== "string" || value.id.length === 0) {
88
+ throw new Error("Malformed /models response: model id must be a string");
89
+ }
90
+ const contextWindow = readContextWindow(value);
91
+ const model = {
92
+ modelID: value.id,
93
+ supportsTools: readSupportsTools(value)
94
+ };
95
+ if (contextWindow !== undefined) {
96
+ model.contextWindow = contextWindow;
97
+ }
98
+ return model;
99
+ }
100
+ function readSupportsTools(model) {
101
+ const direct = readBoolean(model, [
102
+ "tool_call",
103
+ "tool_calls",
104
+ "toolCalling",
105
+ "supportsToolCalling",
106
+ "supports_tools",
107
+ "supportsTools"
108
+ ]);
109
+ if (direct !== undefined) {
110
+ return direct;
111
+ }
112
+ const capabilities = readCapabilityBoolean(model.capabilities);
113
+ if (capabilities !== undefined) {
114
+ return capabilities;
115
+ }
116
+ const features = readCapabilityBoolean(model.features);
117
+ if (features !== undefined) {
118
+ return features;
119
+ }
120
+ return "unknown";
121
+ }
122
+ function readCapabilityBoolean(value) {
123
+ if (Array.isArray(value)) {
124
+ if (value.some((entry) => typeof entry === "string" && ["tools", "tool_call", "tool-calling", "function_calling"].includes(entry))) {
125
+ return true;
126
+ }
127
+ return;
128
+ }
129
+ if (!isObject(value)) {
130
+ return;
131
+ }
132
+ return readBoolean(value, [
133
+ "tools",
134
+ "tool_call",
135
+ "tool_calls",
136
+ "toolCalling",
137
+ "function_calling",
138
+ "supportsToolCalling"
139
+ ]);
140
+ }
141
+ function readContextWindow(model) {
142
+ const direct = readPositiveInteger(model, [
143
+ "contextWindow",
144
+ "context_window",
145
+ "context_length",
146
+ "max_context_length",
147
+ "n_ctx"
148
+ ]);
149
+ if (direct !== undefined) {
150
+ return direct;
151
+ }
152
+ if (isObject(model.limit)) {
153
+ return readPositiveInteger(model.limit, ["context"]);
154
+ }
155
+ return;
156
+ }
157
+ function readBoolean(object, keys) {
158
+ for (const key of keys) {
159
+ const value = object[key];
160
+ if (typeof value === "boolean") {
161
+ return value;
162
+ }
163
+ }
164
+ return;
165
+ }
166
+ function readPositiveInteger(object, keys) {
167
+ for (const key of keys) {
168
+ const value = object[key];
169
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
170
+ return value;
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ function isObject(value) {
176
+ return typeof value === "object" && value !== null && !Array.isArray(value);
177
+ }
178
+
179
+ // src/local/foundry-local.ts
180
+ async function discoverFoundryLocalBaseURL(input) {
181
+ const result = await input.exec(input.command ?? "foundry", [
182
+ ...input.args ?? ["service", "status"]
183
+ ]);
184
+ if (result.exitCode !== 0) {
185
+ throw new Error(`Foundry Local discovery failed with exit code ${result.exitCode}: ${result.stderr || result.stdout}`);
186
+ }
187
+ const endpoint = parseFoundryEndpoint(`${result.stdout}
188
+ ${result.stderr}`);
189
+ if (endpoint === undefined) {
190
+ throw new Error("Foundry Local discovery did not return an endpoint");
191
+ }
192
+ return endpoint;
193
+ }
194
+ function createFoundryLocalAdapter() {
195
+ return {
196
+ id: "foundry-local",
197
+ listModels: async (options) => {
198
+ const baseURL = await resolveFoundryBaseURL(options);
199
+ return listOpenAICompatibleModels(baseURL, options.fetch);
200
+ },
201
+ probe: async (options) => {
202
+ let baseURL = options.baseURL ?? "";
203
+ try {
204
+ baseURL = await resolveFoundryBaseURL(options);
205
+ } catch (error) {
206
+ return unavailableSnapshot({
207
+ id: "foundry-local",
208
+ baseURL,
209
+ probedAt: options.probedAt,
210
+ error
211
+ });
212
+ }
213
+ return probeOpenAICompatibleRuntime({
214
+ id: "foundry-local",
215
+ baseURL,
216
+ fetch: options.fetch,
217
+ probedAt: options.probedAt
218
+ });
219
+ }
220
+ };
221
+ }
222
+ async function resolveFoundryBaseURL(options) {
223
+ if (options.baseURL !== undefined) {
224
+ return normalizeBaseURL(options.baseURL);
225
+ }
226
+ if (options.discovery === undefined) {
227
+ throw new Error("Foundry Local discovery is required when baseURL is absent");
228
+ }
229
+ return normalizeFoundryDiscovery(await options.discovery());
230
+ }
231
+ function normalizeFoundryDiscovery(discovery) {
232
+ if (typeof discovery === "string") {
233
+ return ensureFoundryV1BaseURL(discovery);
234
+ }
235
+ if ("baseURL" in discovery) {
236
+ return ensureFoundryV1BaseURL(discovery.baseURL);
237
+ }
238
+ const host = discovery.host ?? "localhost";
239
+ const protocol = discovery.protocol ?? "http";
240
+ return `${protocol}://${host}:${discovery.port}/v1`;
241
+ }
242
+ function foundryDiscoveryFromExec(exec) {
243
+ return () => discoverFoundryLocalBaseURL({ exec });
244
+ }
245
+ function parseFoundryEndpoint(output) {
246
+ const explicitURL = output.match(/https?:\/\/[^\s"'<>),]+/u)?.[0];
247
+ if (explicitURL !== undefined) {
248
+ return ensureFoundryV1BaseURL(explicitURL);
249
+ }
250
+ const localhostPort = output.match(/(?:localhost|127\.0\.0\.1|\[::1\])\s*:\s*(\d{2,5})/u);
251
+ const port = localhostPort?.[1];
252
+ if (port !== undefined) {
253
+ return `http://localhost:${port}/v1`;
254
+ }
255
+ return;
256
+ }
257
+ function ensureFoundryV1BaseURL(endpoint) {
258
+ const normalizedEndpoint = endpoint.trim().replace(/[),.;]+$/u, "");
259
+ const withProtocol = /^[a-z]+:\/\//iu.test(normalizedEndpoint) ? normalizedEndpoint : `http://${normalizedEndpoint}`;
260
+ try {
261
+ const url = new URL(withProtocol);
262
+ const path = normalizeBaseURL(url.pathname);
263
+ if (path.endsWith("/v1")) {
264
+ return normalizeBaseURL(url.toString());
265
+ }
266
+ return `${url.origin}/v1`;
267
+ } catch (error) {
268
+ throw new Error(`Invalid Foundry Local endpoint: ${errorMessage(error)}`);
269
+ }
270
+ }
271
+
272
+ // src/local/lmstudio.ts
273
+ var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
274
+ function createLMStudioAdapter() {
275
+ return {
276
+ id: "lmstudio",
277
+ defaultBaseURL: LMSTUDIO_DEFAULT_BASE_URL,
278
+ listModels(options) {
279
+ return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? LMSTUDIO_DEFAULT_BASE_URL), options.fetch);
280
+ },
281
+ probe(options) {
282
+ return probeOpenAICompatibleRuntime({
283
+ id: "lmstudio",
284
+ baseURL: options.baseURL ?? LMSTUDIO_DEFAULT_BASE_URL,
285
+ fetch: options.fetch,
286
+ probedAt: options.probedAt
287
+ });
288
+ }
289
+ };
290
+ }
291
+
292
+ // src/local/ollama.ts
293
+ var OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1";
294
+ function createOllamaAdapter() {
295
+ return {
296
+ id: "ollama",
297
+ defaultBaseURL: OLLAMA_DEFAULT_BASE_URL,
298
+ listModels(options) {
299
+ return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? OLLAMA_DEFAULT_BASE_URL), options.fetch);
300
+ },
301
+ probe(options) {
302
+ return probeOpenAICompatibleRuntime({
303
+ id: "ollama",
304
+ baseURL: options.baseURL ?? OLLAMA_DEFAULT_BASE_URL,
305
+ fetch: options.fetch,
306
+ probedAt: options.probedAt
307
+ });
308
+ }
309
+ };
310
+ }
311
+
312
+ // src/local/registry.ts
313
+ class RuntimeRegistry {
314
+ adapters;
315
+ clock;
316
+ fetch;
317
+ foundryDiscovery;
318
+ constructor(options) {
319
+ this.adapters = {
320
+ ollama: createOllamaAdapter(),
321
+ lmstudio: createLMStudioAdapter(),
322
+ "foundry-local": createFoundryLocalAdapter(),
323
+ ...options.adapters
324
+ };
325
+ this.clock = options.clock;
326
+ this.fetch = options.fetch;
327
+ this.foundryDiscovery = options.foundryDiscovery;
328
+ }
329
+ async probe(runtimes) {
330
+ const snapshots = [];
331
+ for (const runtime of runtimes) {
332
+ if (!runtime.enabled) {
333
+ continue;
334
+ }
335
+ const probedAt = this.clock();
336
+ const adapter = this.adapters[runtime.id];
337
+ const options = this.createProbeOptions(runtime, probedAt);
338
+ try {
339
+ snapshots.push(await adapter.probe(options));
340
+ } catch (error) {
341
+ snapshots.push(unavailableSnapshot({
342
+ id: runtime.id,
343
+ baseURL: runtime.baseURL ?? adapter.defaultBaseURL ?? "",
344
+ probedAt,
345
+ error: `Adapter threw unexpectedly: ${errorMessage(error)}`
346
+ }));
347
+ }
348
+ }
349
+ return snapshots;
350
+ }
351
+ createProbeOptions(runtime, probedAt) {
352
+ const options = {
353
+ fetch: this.fetch,
354
+ probedAt
355
+ };
356
+ if (runtime.baseURL !== undefined) {
357
+ options.baseURL = runtime.baseURL;
358
+ }
359
+ if (runtime.id === "foundry-local" && runtime.discovery !== "manual" && this.foundryDiscovery !== undefined) {
360
+ options.discovery = this.foundryDiscovery;
361
+ }
362
+ return options;
363
+ }
364
+ }
365
+
366
+ // src/cli/initAdapters.ts
367
+ class PromptCancelledError extends Error {
368
+ constructor() {
369
+ super("prompt cancelado");
370
+ this.name = "PromptCancelledError";
371
+ }
372
+ }
373
+ function unwrap(value) {
374
+ if (isCancel(value)) {
375
+ cancel("Operación cancelada.");
376
+ throw new PromptCancelledError;
377
+ }
378
+ return value;
379
+ }
380
+ function toOption(choice) {
381
+ const option = { value: choice.value, label: choice.label };
382
+ if (choice.hint !== undefined) {
383
+ option.hint = choice.hint;
384
+ }
385
+ return option;
386
+ }
387
+ var clackPrompter = {
388
+ intro(title) {
389
+ intro(title);
390
+ },
391
+ outro(message) {
392
+ outro(message);
393
+ },
394
+ note(message, title) {
395
+ note(message, title);
396
+ },
397
+ async select(opts) {
398
+ const result = await select({
399
+ message: opts.message,
400
+ options: opts.choices.map(toOption),
401
+ ...opts.initial !== undefined ? { initialValue: opts.initial } : {}
402
+ });
403
+ return unwrap(result);
404
+ },
405
+ async multiselect(opts) {
406
+ const initialValues = opts.choices.filter((choice) => choice.selected === true).map((choice) => choice.value);
407
+ const result = await multiselect({
408
+ message: opts.message,
409
+ options: opts.choices.map(toOption),
410
+ initialValues,
411
+ required: opts.required ?? false
412
+ });
413
+ return unwrap(result);
414
+ },
415
+ async confirm(opts) {
416
+ const result = await confirm({
417
+ message: opts.message,
418
+ initialValue: opts.initial ?? false
419
+ });
420
+ return unwrap(result);
421
+ },
422
+ async text(opts) {
423
+ const result = await text({
424
+ message: opts.message,
425
+ ...opts.placeholder !== undefined ? { placeholder: opts.placeholder } : {},
426
+ ...opts.initial !== undefined ? { initialValue: opts.initial } : {}
427
+ });
428
+ return unwrap(result);
429
+ }
430
+ };
431
+ var DETECTION_RUNTIMES = [
432
+ {
433
+ id: "ollama",
434
+ enabled: true,
435
+ baseURL: "http://localhost:11434/v1",
436
+ defaultModel: { providerID: "ollama", modelID: "probe" }
437
+ },
438
+ {
439
+ id: "lmstudio",
440
+ enabled: true,
441
+ baseURL: "http://localhost:1234/v1",
442
+ defaultModel: { providerID: "lmstudio", modelID: "probe" }
443
+ },
444
+ {
445
+ id: "foundry-local",
446
+ enabled: true,
447
+ discovery: "cli",
448
+ defaultModel: { providerID: "foundry-local", modelID: "probe" }
449
+ }
450
+ ];
451
+ function createDetect(exec) {
452
+ return async () => {
453
+ const registry = new RuntimeRegistry({
454
+ fetch: globalThis.fetch,
455
+ clock: () => new Date().toISOString(),
456
+ foundryDiscovery: foundryDiscoveryFromExec(exec)
457
+ });
458
+ const snapshots = await registry.probe(DETECTION_RUNTIMES);
459
+ return snapshots.map((snapshot) => {
460
+ const detected = {
461
+ id: snapshot.id,
462
+ baseURL: snapshot.baseURL,
463
+ reachable: snapshot.reachable,
464
+ models: snapshot.models.map((model) => model.modelID)
465
+ };
466
+ if (snapshot.error !== undefined) {
467
+ detected.error = snapshot.error;
468
+ }
469
+ return detected;
470
+ });
471
+ };
472
+ }
473
+
8
474
  // src/config/schema.ts
9
475
  import { z } from "zod";
10
476
  var ModelRefSchema = z.object({
@@ -250,6 +716,7 @@ var HELP = [
250
716
  "openteam — comandos runtime",
251
717
  "",
252
718
  "Uso:",
719
+ " openteam init Configura opencode + openteam (interactivo)",
253
720
  " openteam baseline show Muestra el baseline efectivo",
254
721
  " openteam baseline set <p/model> Fija el baseline (modo pinned)",
255
722
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
@@ -362,376 +829,462 @@ async function runCli(argv, deps) {
362
829
  ${HELP}`
363
830
  };
364
831
  } 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
- }
832
+ const message = error instanceof Error ? error.message : String(error);
833
+ return { exitCode: 1, stdout: `Error: ${message}` };
532
834
  }
533
- return;
534
835
  }
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
- }
836
+
837
+ // src/commands/init.ts
838
+ import { join } from "node:path";
839
+
840
+ // src/capabilities/curated.ts
841
+ var CURATED_FRONTIER_PROFILES = [
842
+ {
843
+ ref: { providerID: "openai", modelID: "gpt-5.4-mini" },
844
+ kind: "frontier",
845
+ contextWindow: 128000,
846
+ maxOutputTokens: 16384,
847
+ supportsToolCalling: true,
848
+ supportsVision: true,
849
+ reasoningTier: 3,
850
+ codeQualityTier: 3,
851
+ costPer1M: { inputUSD: 0.8, outputUSD: 3.2 },
852
+ availability: "available"
853
+ },
854
+ {
855
+ ref: { providerID: "google", modelID: "gemini-3.5-flash" },
856
+ kind: "frontier",
857
+ contextWindow: 1e6,
858
+ maxOutputTokens: 65536,
859
+ supportsToolCalling: true,
860
+ supportsVision: true,
861
+ reasoningTier: 3,
862
+ codeQualityTier: 3,
863
+ costPer1M: { inputUSD: 0.35, outputUSD: 1.25 },
864
+ availability: "available"
865
+ },
866
+ {
867
+ ref: { providerID: "anthropic", modelID: "claude-sonnet-4-5" },
868
+ kind: "frontier",
869
+ contextWindow: 200000,
870
+ maxOutputTokens: 64000,
871
+ supportsToolCalling: true,
872
+ supportsVision: true,
873
+ reasoningTier: 4,
874
+ codeQualityTier: 5,
875
+ costPer1M: { inputUSD: 3, outputUSD: 15 },
876
+ availability: "available"
877
+ },
878
+ {
879
+ ref: { providerID: "openai", modelID: "gpt-5.3-codex" },
880
+ kind: "frontier",
881
+ contextWindow: 400000,
882
+ maxOutputTokens: 64000,
883
+ supportsToolCalling: true,
884
+ supportsVision: true,
885
+ reasoningTier: 5,
886
+ codeQualityTier: 5,
887
+ costPer1M: { inputUSD: 5, outputUSD: 20 },
888
+ availability: "available"
541
889
  }
542
- return;
890
+ ];
891
+
892
+ // src/config/persist.ts
893
+ import { mkdir, writeFile } from "node:fs/promises";
894
+ import { dirname } from "node:path";
895
+ var DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
896
+ function serializeOpenTeamConfig(config) {
897
+ const validated = OpenTeamConfigSchema.parse(config);
898
+ return `${JSON.stringify(validated, null, 2)}
899
+ `;
543
900
  }
544
- function isObject(value) {
545
- return typeof value === "object" && value !== null && !Array.isArray(value);
901
+ async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, deps = {}) {
902
+ const mkdirFn = deps.mkdir ?? mkdir;
903
+ const writeFileFn = deps.writeFile ?? writeFile;
904
+ await mkdirFn(dirname(path), { recursive: true });
905
+ await writeFileFn(path, serializeOpenTeamConfig(config), "utf8");
546
906
  }
547
907
 
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}`);
908
+ // src/commands/orchestratorAgent.ts
909
+ var ORCHESTRATOR_AGENT_PATH = ".opencode/agent/openteam.md";
910
+ function buildOrchestratorAgent(frontier) {
911
+ const model = `${frontier.providerID}/${frontier.modelID}`;
912
+ const frontmatter = [
913
+ "---",
914
+ 'description: "Orquestador openteam estilo Squad: analiza el objetivo, diseña y crea el equipo de subagentes a demanda, y coordina el trabajo con routing local-first."',
915
+ "mode: primary",
916
+ `model: ${model}`,
917
+ "temperature: 0.2",
918
+ "permission:",
919
+ " read: allow",
920
+ " glob: allow",
921
+ " grep: allow",
922
+ " list: allow",
923
+ " edit: allow",
924
+ " bash: ask",
925
+ " task: allow",
926
+ " webfetch: allow",
927
+ " websearch: ask",
928
+ " external_directory: deny",
929
+ "---"
930
+ ].join(`
931
+ `);
932
+ const body = [
933
+ "Eres el orquestador de **openteam**, al estilo Squad. Eres el único agente",
934
+ "primario: no existe un equipo precreado. Tú **construyes el equipo de",
935
+ "subagentes a demanda** según el objetivo del usuario y del repositorio.",
936
+ "",
937
+ "## Primer arranque (primer prompt de la sesión)",
938
+ "",
939
+ "1. Analiza el objetivo y explora el repositorio (`read`/`glob`/`grep`) para",
940
+ " entender stack, dominio y alcance real.",
941
+ "2. Diseña el **equipo mínimo** necesario: define solo los roles que el",
942
+ " trabajo requiera (p. ej. `architect`, `backend`, `frontend`, `reviewer`,",
943
+ " `tester`, `docs`). No inventes roles que no vayas a usar.",
944
+ "3. Crea cada subagente **a demanda** escribiendo `.opencode/agent/<rol>.md`:",
945
+ " - Frontmatter: `description` (obligatorio), `mode: subagent`,",
946
+ " `temperature`, `permission` con lo mínimo necesario, y `model`",
947
+ " **opcional**.",
948
+ " - Deja `model` sin fijar para que herede el default y openteam lo enrute",
949
+ " local-first, o fija un modelo local para roles mecánicos/bulk. Reserva",
950
+ " frontier solo para arquitectura, seguridad o debugging ambiguo:",
951
+ " openteam ya enruta cheapest-capable automáticamente.",
952
+ " - Un prompt de rol enfocado y accionable.",
953
+ "",
954
+ "## Operación",
955
+ "",
956
+ "- Delega el trabajo con la herramienta `task` al subagente adecuado; tú te",
957
+ " mantienes como coordinador y no implementas lo que puede hacer un subagente.",
958
+ "- Crea subagentes **justo cuando el trabajo lo pide**, nunca “por si acaso”.",
959
+ " Revisa `.opencode/agent/` y reutiliza los subagentes existentes antes de",
960
+ " crear uno nuevo.",
961
+ "- Respeta el routing y la privacidad de openteam: no fuerces modelos premium",
962
+ " y no envíes datos sensibles a frontier si la política fuerza local.",
963
+ "- Resume al usuario qué equipo has creado y por qué, y mantén el roster ligero.",
964
+ "",
965
+ "## Límites",
966
+ "",
967
+ "- Un único primario (tú); todo lo demás son subagentes creados a demanda.",
968
+ "- No borres ni sobrescribas subagentes creados previamente sin confirmarlo.",
969
+ "- Los subagentes nuevos quedan disponibles para `@mención` y para la",
970
+ " herramienta `task` una vez opencode recarga los agentes del proyecto.",
971
+ ""
972
+ ].join(`
973
+ `);
974
+ return `${frontmatter}
975
+
976
+ ${body}`;
977
+ }
978
+
979
+ // src/commands/init.ts
980
+ var OPENTEAM_PLUGIN_SPEC = "@jmanuelcorral/openteam";
981
+ var OPENCODE_CONFIG_PATH = "opencode.json";
982
+ var KNOWN_RUNTIMES = [
983
+ {
984
+ id: "ollama",
985
+ label: "Ollama",
986
+ defaultBaseURL: "http://localhost:11434/v1",
987
+ fallbackModelID: "qwen3:8b"
988
+ },
989
+ {
990
+ id: "lmstudio",
991
+ label: "LM Studio",
992
+ defaultBaseURL: "http://localhost:1234/v1",
993
+ fallbackModelID: "qwen2.5-coder"
994
+ },
995
+ {
996
+ id: "foundry-local",
997
+ label: "Foundry Local",
998
+ fallbackModelID: "Phi-4-mini-instruct",
999
+ dynamicPort: true
555
1000
  }
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");
1001
+ ];
1002
+ function weightedCost(inputUSD, outputUSD) {
1003
+ return 0.75 * inputUSD + 0.25 * outputUSD;
1004
+ }
1005
+ function frontierChoices() {
1006
+ return [...CURATED_FRONTIER_PROFILES].sort((a, b) => weightedCost(a.costPer1M.inputUSD, a.costPer1M.outputUSD) - weightedCost(b.costPer1M.inputUSD, b.costPer1M.outputUSD)).map((profile) => ({
1007
+ providerID: profile.ref.providerID,
1008
+ modelID: profile.ref.modelID
1009
+ }));
1010
+ }
1011
+ function knownRuntime(id) {
1012
+ const found = KNOWN_RUNTIMES.find((runtime) => runtime.id === id);
1013
+ if (found === undefined) {
1014
+ throw new Error(`unknown runtime id: ${id}`);
560
1015
  }
561
- return endpoint;
1016
+ return found;
562
1017
  }
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
- });
1018
+ function firstEnabled(runtimes) {
1019
+ return runtimes.find((runtime) => runtime.enabled);
1020
+ }
1021
+ function buildOpenTeamConfig(answers) {
1022
+ const runtimes = answers.runtimes.map((choice) => {
1023
+ const runtime = {
1024
+ id: choice.id,
1025
+ enabled: choice.enabled,
1026
+ defaultModel: { providerID: choice.id, modelID: choice.defaultModelID }
1027
+ };
1028
+ if (choice.baseURL !== undefined) {
1029
+ runtime.baseURL = choice.baseURL;
1030
+ } else if (knownRuntime(choice.id).dynamicPort === true) {
1031
+ runtime.discovery = "cli";
588
1032
  }
1033
+ return runtime;
1034
+ });
1035
+ const primaryLocal = firstEnabled(answers.runtimes);
1036
+ const localDefault = primaryLocal !== undefined ? { providerID: primaryLocal.id, modelID: primaryLocal.defaultModelID } : {
1037
+ providerID: runtimes[0]?.id ?? "ollama",
1038
+ modelID: runtimes[0]?.defaultModel.modelID ?? "qwen3:8b"
589
1039
  };
1040
+ return OpenTeamConfigSchema.parse({
1041
+ baseline: {
1042
+ mode: "auto",
1043
+ pinnedModel: null,
1044
+ hardDefault: answers.frontier
1045
+ },
1046
+ router: {
1047
+ mode: answers.routerMode,
1048
+ localDefault
1049
+ },
1050
+ local: { runtimes },
1051
+ privacyMode: answers.privacyMode
1052
+ });
590
1053
  }
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");
1054
+ function buildOpencodeConfig(answers) {
1055
+ const provider = {};
1056
+ for (const choice of answers.runtimes) {
1057
+ if (!choice.enabled) {
1058
+ continue;
1059
+ }
1060
+ const meta = knownRuntime(choice.id);
1061
+ const baseURL = choice.baseURL ?? meta.defaultBaseURL;
1062
+ if (baseURL === undefined) {
1063
+ continue;
1064
+ }
1065
+ const modelIDs = choice.models.length > 0 ? choice.models : [choice.defaultModelID];
1066
+ const models = {};
1067
+ for (const modelID of modelIDs) {
1068
+ models[modelID] = { name: modelID };
1069
+ }
1070
+ provider[choice.id] = {
1071
+ npm: "@ai-sdk/openai-compatible",
1072
+ name: `${meta.label} (local)`,
1073
+ options: { baseURL },
1074
+ models
1075
+ };
597
1076
  }
598
- return normalizeFoundryDiscovery(await options.discovery());
599
- }
600
- function normalizeFoundryDiscovery(discovery) {
601
- if (typeof discovery === "string") {
602
- return ensureFoundryV1BaseURL(discovery);
1077
+ const config = {
1078
+ $schema: "https://opencode.ai/config.json",
1079
+ plugin: [OPENTEAM_PLUGIN_SPEC],
1080
+ model: `${answers.frontier.providerID}/${answers.frontier.modelID}`
1081
+ };
1082
+ if (Object.keys(provider).length > 0) {
1083
+ config.provider = provider;
603
1084
  }
604
- if ("baseURL" in discovery) {
605
- return ensureFoundryV1BaseURL(discovery.baseURL);
1085
+ const primaryLocal = firstEnabled(answers.runtimes);
1086
+ if (primaryLocal !== undefined) {
1087
+ config.small_model = `${primaryLocal.id}/${primaryLocal.defaultModelID}`;
606
1088
  }
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 });
1089
+ return config;
613
1090
  }
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;
1091
+ function serializeOpencodeConfig(config) {
1092
+ return `${JSON.stringify(config, null, 2)}
1093
+ `;
625
1094
  }
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)}`);
1095
+ var CUSTOM_FRONTIER = "__custom__";
1096
+ async function chooseFrontier(prompt) {
1097
+ const options = frontierChoices();
1098
+ const selected = await prompt.select({
1099
+ message: "Modelo frontier baseline (cheapest-capable primero)",
1100
+ choices: [
1101
+ ...options.map((choice) => ({
1102
+ value: `${choice.providerID}/${choice.modelID}`,
1103
+ label: `${choice.providerID}/${choice.modelID}`
1104
+ })),
1105
+ { value: CUSTOM_FRONTIER, label: "Otro (introducir provider/model)" }
1106
+ ],
1107
+ initial: options[0] ? `${options[0].providerID}/${options[0].modelID}` : CUSTOM_FRONTIER
1108
+ });
1109
+ const ref = selected === CUSTOM_FRONTIER ? await prompt.text({
1110
+ message: "Modelo frontier en formato provider/model",
1111
+ placeholder: "anthropic/claude-sonnet-4-5"
1112
+ }) : selected;
1113
+ const slash = ref.indexOf("/");
1114
+ if (slash <= 0 || slash === ref.length - 1) {
1115
+ throw new Error(`modelo frontier inválido: "${ref}" (usa provider/model)`);
638
1116
  }
639
- }
640
-
641
- // src/local/lmstudio.ts
642
- var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
643
- function createLMStudioAdapter() {
644
1117
  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
- }
1118
+ providerID: ref.slice(0, slash),
1119
+ modelID: ref.slice(slash + 1)
658
1120
  };
659
1121
  }
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
- }
1122
+ async function chooseRuntimeModel(prompt, detected, meta) {
1123
+ if (detected.models.length === 0) {
1124
+ return prompt.text({
1125
+ message: `Modelo por defecto para ${meta.label} (no se detectaron modelos)`,
1126
+ placeholder: meta.fallbackModelID,
1127
+ initial: meta.fallbackModelID
1128
+ });
1129
+ }
1130
+ const first = detected.models[0];
1131
+ const opts = {
1132
+ message: `Modelo local por defecto para ${meta.label}`,
1133
+ choices: detected.models.map((modelID) => ({
1134
+ value: modelID,
1135
+ label: modelID
1136
+ }))
678
1137
  };
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;
1138
+ if (first !== undefined) {
1139
+ opts.initial = first;
697
1140
  }
698
- async probe(runtimes) {
699
- const snapshots = [];
700
- for (const runtime of runtimes) {
701
- if (!runtime.enabled) {
1141
+ return prompt.select(opts);
1142
+ }
1143
+ async function runInit(deps) {
1144
+ const { prompt } = deps;
1145
+ try {
1146
+ prompt.intro("openteam init — configura opencode con routing local-first");
1147
+ const detected = await deps.detect();
1148
+ const detectedById = new Map(detected.map((item) => [item.id, item]));
1149
+ const reachableIds = detected.filter((item) => item.reachable).map((item) => item.id);
1150
+ const enabledIds = await prompt.multiselect({
1151
+ message: "¿Qué runtimes locales quieres habilitar?",
1152
+ choices: KNOWN_RUNTIMES.map((meta) => {
1153
+ const info = detectedById.get(meta.id);
1154
+ return {
1155
+ value: meta.id,
1156
+ label: meta.label,
1157
+ hint: info?.reachable ? `detectado · ${info.models.length} modelo(s)` : "no detectado",
1158
+ selected: info?.reachable ?? false
1159
+ };
1160
+ })
1161
+ });
1162
+ const enabled = new Set(enabledIds);
1163
+ const runtimes = [];
1164
+ for (const meta of KNOWN_RUNTIMES) {
1165
+ const info = detectedById.get(meta.id);
1166
+ const isEnabled = enabled.has(meta.id);
1167
+ if (!isEnabled) {
1168
+ const choice2 = {
1169
+ id: meta.id,
1170
+ enabled: false,
1171
+ defaultModelID: meta.fallbackModelID,
1172
+ models: info?.models ?? []
1173
+ };
1174
+ const disabledBaseURL = info?.baseURL ?? meta.defaultBaseURL;
1175
+ if (disabledBaseURL !== undefined && disabledBaseURL.length > 0) {
1176
+ choice2.baseURL = disabledBaseURL;
1177
+ }
1178
+ runtimes.push(choice2);
702
1179
  continue;
703
1180
  }
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
- }));
1181
+ const detectedRuntime = info ?? {
1182
+ id: meta.id,
1183
+ baseURL: meta.defaultBaseURL ?? "",
1184
+ reachable: false,
1185
+ models: []
1186
+ };
1187
+ const defaultModelID = await chooseRuntimeModel(prompt, detectedRuntime, meta);
1188
+ const baseURL = info?.baseURL !== undefined && info.baseURL.length > 0 ? info.baseURL : meta.defaultBaseURL;
1189
+ const choice = {
1190
+ id: meta.id,
1191
+ enabled: true,
1192
+ defaultModelID,
1193
+ models: detectedRuntime.models
1194
+ };
1195
+ if (baseURL !== undefined) {
1196
+ choice.baseURL = baseURL;
716
1197
  }
1198
+ runtimes.push(choice);
717
1199
  }
718
- return snapshots;
719
- }
720
- createProbeOptions(runtime, probedAt) {
721
- const options = {
722
- fetch: this.fetch,
723
- probedAt
1200
+ const frontier = await chooseFrontier(prompt);
1201
+ const routerMode = await prompt.select({
1202
+ message: "Modo de routing",
1203
+ choices: [
1204
+ { value: "balanced", label: "balanced (recomendado)" },
1205
+ { value: "economy", label: "economy (máximo ahorro, prioriza local)" },
1206
+ { value: "quality", label: "quality (prioriza frontier)" }
1207
+ ],
1208
+ initial: "balanced"
1209
+ });
1210
+ const privacyMode = await prompt.select({
1211
+ message: "Privacidad para prompts sensibles",
1212
+ choices: [
1213
+ {
1214
+ value: "forceLocalOnSensitive",
1215
+ label: "forceLocalOnSensitive (nunca envía datos sensibles a frontier)"
1216
+ },
1217
+ {
1218
+ value: "consentBeforeFrontier",
1219
+ label: "consentBeforeFrontier (pide consentimiento)"
1220
+ },
1221
+ { value: "off", label: "off (sin protección)" }
1222
+ ],
1223
+ initial: "forceLocalOnSensitive"
1224
+ });
1225
+ const answers = {
1226
+ runtimes,
1227
+ frontier,
1228
+ routerMode,
1229
+ privacyMode
724
1230
  };
725
- if (runtime.baseURL !== undefined) {
726
- options.baseURL = runtime.baseURL;
1231
+ const openTeamConfig = buildOpenTeamConfig(answers);
1232
+ const opencodeConfig = buildOpencodeConfig(answers);
1233
+ const orchestratorAgent = buildOrchestratorAgent(frontier);
1234
+ const openTeamPath = join(deps.cwd, DEFAULT_CONFIG_PATH);
1235
+ const opencodePath = join(deps.cwd, OPENCODE_CONFIG_PATH);
1236
+ const agentPath = join(deps.cwd, ORCHESTRATOR_AGENT_PATH);
1237
+ const existing = [];
1238
+ if (await deps.fileExists(openTeamPath)) {
1239
+ existing.push(DEFAULT_CONFIG_PATH);
727
1240
  }
728
- if (runtime.id === "foundry-local" && runtime.discovery !== "manual" && this.foundryDiscovery !== undefined) {
729
- options.discovery = this.foundryDiscovery;
1241
+ if (await deps.fileExists(opencodePath)) {
1242
+ existing.push(OPENCODE_CONFIG_PATH);
730
1243
  }
731
- return options;
1244
+ if (await deps.fileExists(agentPath)) {
1245
+ existing.push(ORCHESTRATOR_AGENT_PATH);
1246
+ }
1247
+ if (existing.length > 0) {
1248
+ const overwrite = await prompt.confirm({
1249
+ message: `Ya existe ${existing.join(" y ")}. ¿Sobrescribir?`,
1250
+ initial: false
1251
+ });
1252
+ if (!overwrite) {
1253
+ prompt.outro("init cancelado: no se sobrescribió ninguna configuración.");
1254
+ return { exitCode: 1, stdout: "init cancelado por el usuario" };
1255
+ }
1256
+ }
1257
+ await deps.writeFile(openTeamPath, serializeOpenTeamConfig(openTeamConfig));
1258
+ await deps.writeFile(opencodePath, serializeOpencodeConfig(opencodeConfig));
1259
+ await deps.writeFile(agentPath, orchestratorAgent);
1260
+ const enabledSummary = runtimes.filter((runtime) => runtime.enabled).map((runtime) => `${runtime.id} (${runtime.defaultModelID})`).join(", ");
1261
+ prompt.note([
1262
+ `Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
1263
+ `Runtimes locales: ${enabledSummary || "ninguno habilitado"}`,
1264
+ `Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}`,
1265
+ "",
1266
+ "Siguientes pasos:",
1267
+ ` 1. Autentica el provider frontier: opencode auth login`,
1268
+ reachableIds.length === 0 ? " 2. Arranca tu runtime local (Ollama/LM Studio/Foundry) antes de usarlo" : " 2. Abre opencode en este repo; openteam enruta local-first automáticamente",
1269
+ " 3. Pulsa Tab y elige el agente 'openteam': creará el equipo a demanda"
1270
+ ].join(`
1271
+ `), "openteam configurado");
1272
+ prompt.outro("Listo. Abre opencode y selecciona el agente 'openteam'.");
1273
+ return {
1274
+ exitCode: 0,
1275
+ stdout: `openteam init completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}`
1276
+ };
1277
+ } catch (error) {
1278
+ const message = error instanceof Error ? error.message : String(error);
1279
+ return { exitCode: 1, stdout: `Error en init: ${message}` };
732
1280
  }
733
1281
  }
734
1282
 
1283
+ // src/config/load.ts
1284
+ function loadOpenTeamConfig(raw) {
1285
+ return OpenTeamConfigSchema.parse(raw ?? {});
1286
+ }
1287
+
735
1288
  // src/telemetry/read.ts
736
1289
  import { readFile } from "node:fs/promises";
737
1290
 
@@ -793,9 +1346,9 @@ var CostRecordSchema = z3.object({
793
1346
 
794
1347
  // src/telemetry/read.ts
795
1348
  var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
796
- function parseCostRecordsJsonl(text) {
1349
+ function parseCostRecordsJsonl(text2) {
797
1350
  const records = [];
798
- for (const line of text.split(`
1351
+ for (const line of text2.split(`
799
1352
  `)) {
800
1353
  const trimmed = line.trim();
801
1354
  if (trimmed.length === 0) {
@@ -874,8 +1427,37 @@ var deps = {
874
1427
  configPath: DEFAULT_CONFIG_PATH,
875
1428
  telemetryPath: DEFAULT_TELEMETRY_PATH
876
1429
  };
1430
+ function createInitDeps() {
1431
+ return {
1432
+ cwd: process.cwd(),
1433
+ detect: createDetect(nodeExec),
1434
+ prompt: clackPrompter,
1435
+ writeFile: async (path, contents) => {
1436
+ await mkdir2(dirname2(path), { recursive: true });
1437
+ await writeFile2(path, contents, "utf8");
1438
+ },
1439
+ fileExists: async (path) => {
1440
+ try {
1441
+ await access(path);
1442
+ return true;
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ };
1448
+ }
877
1449
  async function main() {
878
- const result = await runCli(process.argv.slice(2), deps);
1450
+ const argv = process.argv.slice(2);
1451
+ if (argv[0] === "init") {
1452
+ const result2 = await runInit(createInitDeps());
1453
+ if (result2.stdout.length > 0) {
1454
+ process.stdout.write(`${result2.stdout}
1455
+ `);
1456
+ }
1457
+ process.exitCode = result2.exitCode;
1458
+ return;
1459
+ }
1460
+ const result = await runCli(argv, deps);
879
1461
  process.stdout.write(`${result.stdout}
880
1462
  `);
881
1463
  process.exitCode = result.exitCode;