@jmanuelcorral/openteam 0.1.3 → 0.1.4
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/README.md +25 -0
- package/dist/cli/initAdapters.d.ts +7 -0
- package/dist/cli/initAdapters.d.ts.map +1 -0
- package/dist/cli.js +851 -347
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/commands/init.d.ts +121 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -2
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)",
|
|
@@ -356,382 +823,390 @@ async function runCli(argv, deps) {
|
|
|
356
823
|
return { exitCode: 0, stdout: HELP };
|
|
357
824
|
}
|
|
358
825
|
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
|
-
}
|
|
826
|
+
exitCode: 1,
|
|
827
|
+
stdout: `Comando desconocido: ${command}
|
|
828
|
+
|
|
829
|
+
${HELP}`
|
|
830
|
+
};
|
|
831
|
+
} catch (error) {
|
|
832
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
833
|
+
return { exitCode: 1, stdout: `Error: ${message}` };
|
|
532
834
|
}
|
|
533
|
-
return;
|
|
534
835
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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
|
|
545
|
-
|
|
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/
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
908
|
+
// src/commands/init.ts
|
|
909
|
+
var OPENTEAM_PLUGIN_SPEC = "@jmanuelcorral/openteam";
|
|
910
|
+
var OPENCODE_CONFIG_PATH = "opencode.json";
|
|
911
|
+
var KNOWN_RUNTIMES = [
|
|
912
|
+
{
|
|
913
|
+
id: "ollama",
|
|
914
|
+
label: "Ollama",
|
|
915
|
+
defaultBaseURL: "http://localhost:11434/v1",
|
|
916
|
+
fallbackModelID: "qwen3:8b"
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
id: "lmstudio",
|
|
920
|
+
label: "LM Studio",
|
|
921
|
+
defaultBaseURL: "http://localhost:1234/v1",
|
|
922
|
+
fallbackModelID: "qwen2.5-coder"
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
id: "foundry-local",
|
|
926
|
+
label: "Foundry Local",
|
|
927
|
+
fallbackModelID: "Phi-4-mini-instruct",
|
|
928
|
+
dynamicPort: true
|
|
555
929
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
930
|
+
];
|
|
931
|
+
function weightedCost(inputUSD, outputUSD) {
|
|
932
|
+
return 0.75 * inputUSD + 0.25 * outputUSD;
|
|
933
|
+
}
|
|
934
|
+
function frontierChoices() {
|
|
935
|
+
return [...CURATED_FRONTIER_PROFILES].sort((a, b) => weightedCost(a.costPer1M.inputUSD, a.costPer1M.outputUSD) - weightedCost(b.costPer1M.inputUSD, b.costPer1M.outputUSD)).map((profile) => ({
|
|
936
|
+
providerID: profile.ref.providerID,
|
|
937
|
+
modelID: profile.ref.modelID
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
function knownRuntime(id) {
|
|
941
|
+
const found = KNOWN_RUNTIMES.find((runtime) => runtime.id === id);
|
|
942
|
+
if (found === undefined) {
|
|
943
|
+
throw new Error(`unknown runtime id: ${id}`);
|
|
560
944
|
}
|
|
561
|
-
return
|
|
945
|
+
return found;
|
|
562
946
|
}
|
|
563
|
-
function
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
});
|
|
947
|
+
function firstEnabled(runtimes) {
|
|
948
|
+
return runtimes.find((runtime) => runtime.enabled);
|
|
949
|
+
}
|
|
950
|
+
function buildOpenTeamConfig(answers) {
|
|
951
|
+
const runtimes = answers.runtimes.map((choice) => {
|
|
952
|
+
const runtime = {
|
|
953
|
+
id: choice.id,
|
|
954
|
+
enabled: choice.enabled,
|
|
955
|
+
defaultModel: { providerID: choice.id, modelID: choice.defaultModelID }
|
|
956
|
+
};
|
|
957
|
+
if (choice.baseURL !== undefined) {
|
|
958
|
+
runtime.baseURL = choice.baseURL;
|
|
959
|
+
} else if (knownRuntime(choice.id).dynamicPort === true) {
|
|
960
|
+
runtime.discovery = "cli";
|
|
588
961
|
}
|
|
962
|
+
return runtime;
|
|
963
|
+
});
|
|
964
|
+
const primaryLocal = firstEnabled(answers.runtimes);
|
|
965
|
+
const localDefault = primaryLocal !== undefined ? { providerID: primaryLocal.id, modelID: primaryLocal.defaultModelID } : {
|
|
966
|
+
providerID: runtimes[0]?.id ?? "ollama",
|
|
967
|
+
modelID: runtimes[0]?.defaultModel.modelID ?? "qwen3:8b"
|
|
589
968
|
};
|
|
969
|
+
return OpenTeamConfigSchema.parse({
|
|
970
|
+
baseline: {
|
|
971
|
+
mode: "auto",
|
|
972
|
+
pinnedModel: null,
|
|
973
|
+
hardDefault: answers.frontier
|
|
974
|
+
},
|
|
975
|
+
router: {
|
|
976
|
+
mode: answers.routerMode,
|
|
977
|
+
localDefault
|
|
978
|
+
},
|
|
979
|
+
local: { runtimes },
|
|
980
|
+
privacyMode: answers.privacyMode
|
|
981
|
+
});
|
|
590
982
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
983
|
+
function buildOpencodeConfig(answers) {
|
|
984
|
+
const provider = {};
|
|
985
|
+
for (const choice of answers.runtimes) {
|
|
986
|
+
if (!choice.enabled) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const meta = knownRuntime(choice.id);
|
|
990
|
+
const baseURL = choice.baseURL ?? meta.defaultBaseURL;
|
|
991
|
+
if (baseURL === undefined) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const modelIDs = choice.models.length > 0 ? choice.models : [choice.defaultModelID];
|
|
995
|
+
const models = {};
|
|
996
|
+
for (const modelID of modelIDs) {
|
|
997
|
+
models[modelID] = { name: modelID };
|
|
998
|
+
}
|
|
999
|
+
provider[choice.id] = {
|
|
1000
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1001
|
+
name: `${meta.label} (local)`,
|
|
1002
|
+
options: { baseURL },
|
|
1003
|
+
models
|
|
1004
|
+
};
|
|
597
1005
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1006
|
+
const config = {
|
|
1007
|
+
$schema: "https://opencode.ai/config.json",
|
|
1008
|
+
plugin: [OPENTEAM_PLUGIN_SPEC],
|
|
1009
|
+
model: `${answers.frontier.providerID}/${answers.frontier.modelID}`
|
|
1010
|
+
};
|
|
1011
|
+
if (Object.keys(provider).length > 0) {
|
|
1012
|
+
config.provider = provider;
|
|
603
1013
|
}
|
|
604
|
-
|
|
605
|
-
|
|
1014
|
+
const primaryLocal = firstEnabled(answers.runtimes);
|
|
1015
|
+
if (primaryLocal !== undefined) {
|
|
1016
|
+
config.small_model = `${primaryLocal.id}/${primaryLocal.defaultModelID}`;
|
|
606
1017
|
}
|
|
607
|
-
|
|
608
|
-
const protocol = discovery.protocol ?? "http";
|
|
609
|
-
return `${protocol}://${host}:${discovery.port}/v1`;
|
|
610
|
-
}
|
|
611
|
-
function foundryDiscoveryFromExec(exec) {
|
|
612
|
-
return () => discoverFoundryLocalBaseURL({ exec });
|
|
1018
|
+
return config;
|
|
613
1019
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
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;
|
|
1020
|
+
function serializeOpencodeConfig(config) {
|
|
1021
|
+
return `${JSON.stringify(config, null, 2)}
|
|
1022
|
+
`;
|
|
625
1023
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1024
|
+
var CUSTOM_FRONTIER = "__custom__";
|
|
1025
|
+
async function chooseFrontier(prompt) {
|
|
1026
|
+
const options = frontierChoices();
|
|
1027
|
+
const selected = await prompt.select({
|
|
1028
|
+
message: "Modelo frontier baseline (cheapest-capable primero)",
|
|
1029
|
+
choices: [
|
|
1030
|
+
...options.map((choice) => ({
|
|
1031
|
+
value: `${choice.providerID}/${choice.modelID}`,
|
|
1032
|
+
label: `${choice.providerID}/${choice.modelID}`
|
|
1033
|
+
})),
|
|
1034
|
+
{ value: CUSTOM_FRONTIER, label: "Otro (introducir provider/model)" }
|
|
1035
|
+
],
|
|
1036
|
+
initial: options[0] ? `${options[0].providerID}/${options[0].modelID}` : CUSTOM_FRONTIER
|
|
1037
|
+
});
|
|
1038
|
+
const ref = selected === CUSTOM_FRONTIER ? await prompt.text({
|
|
1039
|
+
message: "Modelo frontier en formato provider/model",
|
|
1040
|
+
placeholder: "anthropic/claude-sonnet-4-5"
|
|
1041
|
+
}) : selected;
|
|
1042
|
+
const slash = ref.indexOf("/");
|
|
1043
|
+
if (slash <= 0 || slash === ref.length - 1) {
|
|
1044
|
+
throw new Error(`modelo frontier inválido: "${ref}" (usa provider/model)`);
|
|
638
1045
|
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// src/local/lmstudio.ts
|
|
642
|
-
var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
|
|
643
|
-
function createLMStudioAdapter() {
|
|
644
1046
|
return {
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
}
|
|
1047
|
+
providerID: ref.slice(0, slash),
|
|
1048
|
+
modelID: ref.slice(slash + 1)
|
|
658
1049
|
};
|
|
659
1050
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
probedAt: options.probedAt
|
|
676
|
-
});
|
|
677
|
-
}
|
|
1051
|
+
async function chooseRuntimeModel(prompt, detected, meta) {
|
|
1052
|
+
if (detected.models.length === 0) {
|
|
1053
|
+
return prompt.text({
|
|
1054
|
+
message: `Modelo por defecto para ${meta.label} (no se detectaron modelos)`,
|
|
1055
|
+
placeholder: meta.fallbackModelID,
|
|
1056
|
+
initial: meta.fallbackModelID
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const first = detected.models[0];
|
|
1060
|
+
const opts = {
|
|
1061
|
+
message: `Modelo local por defecto para ${meta.label}`,
|
|
1062
|
+
choices: detected.models.map((modelID) => ({
|
|
1063
|
+
value: modelID,
|
|
1064
|
+
label: modelID
|
|
1065
|
+
}))
|
|
678
1066
|
};
|
|
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;
|
|
1067
|
+
if (first !== undefined) {
|
|
1068
|
+
opts.initial = first;
|
|
697
1069
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1070
|
+
return prompt.select(opts);
|
|
1071
|
+
}
|
|
1072
|
+
async function runInit(deps) {
|
|
1073
|
+
const { prompt } = deps;
|
|
1074
|
+
try {
|
|
1075
|
+
prompt.intro("openteam init — configura opencode con routing local-first");
|
|
1076
|
+
const detected = await deps.detect();
|
|
1077
|
+
const detectedById = new Map(detected.map((item) => [item.id, item]));
|
|
1078
|
+
const reachableIds = detected.filter((item) => item.reachable).map((item) => item.id);
|
|
1079
|
+
const enabledIds = await prompt.multiselect({
|
|
1080
|
+
message: "¿Qué runtimes locales quieres habilitar?",
|
|
1081
|
+
choices: KNOWN_RUNTIMES.map((meta) => {
|
|
1082
|
+
const info = detectedById.get(meta.id);
|
|
1083
|
+
return {
|
|
1084
|
+
value: meta.id,
|
|
1085
|
+
label: meta.label,
|
|
1086
|
+
hint: info?.reachable ? `detectado · ${info.models.length} modelo(s)` : "no detectado",
|
|
1087
|
+
selected: info?.reachable ?? false
|
|
1088
|
+
};
|
|
1089
|
+
})
|
|
1090
|
+
});
|
|
1091
|
+
const enabled = new Set(enabledIds);
|
|
1092
|
+
const runtimes = [];
|
|
1093
|
+
for (const meta of KNOWN_RUNTIMES) {
|
|
1094
|
+
const info = detectedById.get(meta.id);
|
|
1095
|
+
const isEnabled = enabled.has(meta.id);
|
|
1096
|
+
if (!isEnabled) {
|
|
1097
|
+
const choice2 = {
|
|
1098
|
+
id: meta.id,
|
|
1099
|
+
enabled: false,
|
|
1100
|
+
defaultModelID: meta.fallbackModelID,
|
|
1101
|
+
models: info?.models ?? []
|
|
1102
|
+
};
|
|
1103
|
+
const disabledBaseURL = info?.baseURL ?? meta.defaultBaseURL;
|
|
1104
|
+
if (disabledBaseURL !== undefined && disabledBaseURL.length > 0) {
|
|
1105
|
+
choice2.baseURL = disabledBaseURL;
|
|
1106
|
+
}
|
|
1107
|
+
runtimes.push(choice2);
|
|
702
1108
|
continue;
|
|
703
1109
|
}
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1110
|
+
const detectedRuntime = info ?? {
|
|
1111
|
+
id: meta.id,
|
|
1112
|
+
baseURL: meta.defaultBaseURL ?? "",
|
|
1113
|
+
reachable: false,
|
|
1114
|
+
models: []
|
|
1115
|
+
};
|
|
1116
|
+
const defaultModelID = await chooseRuntimeModel(prompt, detectedRuntime, meta);
|
|
1117
|
+
const baseURL = info?.baseURL !== undefined && info.baseURL.length > 0 ? info.baseURL : meta.defaultBaseURL;
|
|
1118
|
+
const choice = {
|
|
1119
|
+
id: meta.id,
|
|
1120
|
+
enabled: true,
|
|
1121
|
+
defaultModelID,
|
|
1122
|
+
models: detectedRuntime.models
|
|
1123
|
+
};
|
|
1124
|
+
if (baseURL !== undefined) {
|
|
1125
|
+
choice.baseURL = baseURL;
|
|
716
1126
|
}
|
|
1127
|
+
runtimes.push(choice);
|
|
717
1128
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1129
|
+
const frontier = await chooseFrontier(prompt);
|
|
1130
|
+
const routerMode = await prompt.select({
|
|
1131
|
+
message: "Modo de routing",
|
|
1132
|
+
choices: [
|
|
1133
|
+
{ value: "balanced", label: "balanced (recomendado)" },
|
|
1134
|
+
{ value: "economy", label: "economy (máximo ahorro, prioriza local)" },
|
|
1135
|
+
{ value: "quality", label: "quality (prioriza frontier)" }
|
|
1136
|
+
],
|
|
1137
|
+
initial: "balanced"
|
|
1138
|
+
});
|
|
1139
|
+
const privacyMode = await prompt.select({
|
|
1140
|
+
message: "Privacidad para prompts sensibles",
|
|
1141
|
+
choices: [
|
|
1142
|
+
{
|
|
1143
|
+
value: "forceLocalOnSensitive",
|
|
1144
|
+
label: "forceLocalOnSensitive (nunca envía datos sensibles a frontier)"
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
value: "consentBeforeFrontier",
|
|
1148
|
+
label: "consentBeforeFrontier (pide consentimiento)"
|
|
1149
|
+
},
|
|
1150
|
+
{ value: "off", label: "off (sin protección)" }
|
|
1151
|
+
],
|
|
1152
|
+
initial: "forceLocalOnSensitive"
|
|
1153
|
+
});
|
|
1154
|
+
const answers = {
|
|
1155
|
+
runtimes,
|
|
1156
|
+
frontier,
|
|
1157
|
+
routerMode,
|
|
1158
|
+
privacyMode
|
|
724
1159
|
};
|
|
725
|
-
|
|
726
|
-
|
|
1160
|
+
const openTeamConfig = buildOpenTeamConfig(answers);
|
|
1161
|
+
const opencodeConfig = buildOpencodeConfig(answers);
|
|
1162
|
+
const openTeamPath = join(deps.cwd, DEFAULT_CONFIG_PATH);
|
|
1163
|
+
const opencodePath = join(deps.cwd, OPENCODE_CONFIG_PATH);
|
|
1164
|
+
const existing = [];
|
|
1165
|
+
if (await deps.fileExists(openTeamPath)) {
|
|
1166
|
+
existing.push(DEFAULT_CONFIG_PATH);
|
|
727
1167
|
}
|
|
728
|
-
if (
|
|
729
|
-
|
|
1168
|
+
if (await deps.fileExists(opencodePath)) {
|
|
1169
|
+
existing.push(OPENCODE_CONFIG_PATH);
|
|
730
1170
|
}
|
|
731
|
-
|
|
1171
|
+
if (existing.length > 0) {
|
|
1172
|
+
const overwrite = await prompt.confirm({
|
|
1173
|
+
message: `Ya existe ${existing.join(" y ")}. ¿Sobrescribir?`,
|
|
1174
|
+
initial: false
|
|
1175
|
+
});
|
|
1176
|
+
if (!overwrite) {
|
|
1177
|
+
prompt.outro("init cancelado: no se sobrescribió ninguna configuración.");
|
|
1178
|
+
return { exitCode: 1, stdout: "init cancelado por el usuario" };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
await deps.writeFile(openTeamPath, serializeOpenTeamConfig(openTeamConfig));
|
|
1182
|
+
await deps.writeFile(opencodePath, serializeOpencodeConfig(opencodeConfig));
|
|
1183
|
+
const enabledSummary = runtimes.filter((runtime) => runtime.enabled).map((runtime) => `${runtime.id} (${runtime.defaultModelID})`).join(", ");
|
|
1184
|
+
prompt.note([
|
|
1185
|
+
`Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
|
|
1186
|
+
`Runtimes locales: ${enabledSummary || "ninguno habilitado"}`,
|
|
1187
|
+
`Escrito: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}`,
|
|
1188
|
+
"",
|
|
1189
|
+
"Siguientes pasos:",
|
|
1190
|
+
` 1. Autentica el provider frontier: opencode auth login`,
|
|
1191
|
+
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"
|
|
1192
|
+
].join(`
|
|
1193
|
+
`), "openteam configurado");
|
|
1194
|
+
prompt.outro("Listo. Abre opencode para empezar.");
|
|
1195
|
+
return {
|
|
1196
|
+
exitCode: 0,
|
|
1197
|
+
stdout: `openteam init completado: ${OPENCODE_CONFIG_PATH}, ${DEFAULT_CONFIG_PATH}`
|
|
1198
|
+
};
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1201
|
+
return { exitCode: 1, stdout: `Error en init: ${message}` };
|
|
732
1202
|
}
|
|
733
1203
|
}
|
|
734
1204
|
|
|
1205
|
+
// src/config/load.ts
|
|
1206
|
+
function loadOpenTeamConfig(raw) {
|
|
1207
|
+
return OpenTeamConfigSchema.parse(raw ?? {});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
735
1210
|
// src/telemetry/read.ts
|
|
736
1211
|
import { readFile } from "node:fs/promises";
|
|
737
1212
|
|
|
@@ -793,9 +1268,9 @@ var CostRecordSchema = z3.object({
|
|
|
793
1268
|
|
|
794
1269
|
// src/telemetry/read.ts
|
|
795
1270
|
var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
|
|
796
|
-
function parseCostRecordsJsonl(
|
|
1271
|
+
function parseCostRecordsJsonl(text2) {
|
|
797
1272
|
const records = [];
|
|
798
|
-
for (const line of
|
|
1273
|
+
for (const line of text2.split(`
|
|
799
1274
|
`)) {
|
|
800
1275
|
const trimmed = line.trim();
|
|
801
1276
|
if (trimmed.length === 0) {
|
|
@@ -874,8 +1349,37 @@ var deps = {
|
|
|
874
1349
|
configPath: DEFAULT_CONFIG_PATH,
|
|
875
1350
|
telemetryPath: DEFAULT_TELEMETRY_PATH
|
|
876
1351
|
};
|
|
1352
|
+
function createInitDeps() {
|
|
1353
|
+
return {
|
|
1354
|
+
cwd: process.cwd(),
|
|
1355
|
+
detect: createDetect(nodeExec),
|
|
1356
|
+
prompt: clackPrompter,
|
|
1357
|
+
writeFile: async (path, contents) => {
|
|
1358
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
1359
|
+
await writeFile2(path, contents, "utf8");
|
|
1360
|
+
},
|
|
1361
|
+
fileExists: async (path) => {
|
|
1362
|
+
try {
|
|
1363
|
+
await access(path);
|
|
1364
|
+
return true;
|
|
1365
|
+
} catch {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
877
1371
|
async function main() {
|
|
878
|
-
const
|
|
1372
|
+
const argv = process.argv.slice(2);
|
|
1373
|
+
if (argv[0] === "init") {
|
|
1374
|
+
const result2 = await runInit(createInitDeps());
|
|
1375
|
+
if (result2.stdout.length > 0) {
|
|
1376
|
+
process.stdout.write(`${result2.stdout}
|
|
1377
|
+
`);
|
|
1378
|
+
}
|
|
1379
|
+
process.exitCode = result2.exitCode;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const result = await runCli(argv, deps);
|
|
879
1383
|
process.stdout.write(`${result.stdout}
|
|
880
1384
|
`);
|
|
881
1385
|
process.exitCode = result.exitCode;
|