@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/README.md +32 -0
- package/dist/cli/initAdapters.d.ts +7 -0
- package/dist/cli/initAdapters.d.ts.map +1 -0
- package/dist/cli.js +923 -341
- 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/commands/orchestratorAgent.d.ts +18 -0
- package/dist/commands/orchestratorAgent.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)",
|
|
@@ -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
|
-
|
|
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/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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
1016
|
+
return found;
|
|
562
1017
|
}
|
|
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
|
-
});
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
605
|
-
|
|
1085
|
+
const primaryLocal = firstEnabled(answers.runtimes);
|
|
1086
|
+
if (primaryLocal !== undefined) {
|
|
1087
|
+
config.small_model = `${primaryLocal.id}/${primaryLocal.defaultModelID}`;
|
|
606
1088
|
}
|
|
607
|
-
|
|
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
|
|
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;
|
|
1091
|
+
function serializeOpencodeConfig(config) {
|
|
1092
|
+
return `${JSON.stringify(config, null, 2)}
|
|
1093
|
+
`;
|
|
625
1094
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1118
|
+
providerID: ref.slice(0, slash),
|
|
1119
|
+
modelID: ref.slice(slash + 1)
|
|
658
1120
|
};
|
|
659
1121
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
726
|
-
|
|
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 (
|
|
729
|
-
|
|
1241
|
+
if (await deps.fileExists(opencodePath)) {
|
|
1242
|
+
existing.push(OPENCODE_CONFIG_PATH);
|
|
730
1243
|
}
|
|
731
|
-
|
|
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(
|
|
1349
|
+
function parseCostRecordsJsonl(text2) {
|
|
797
1350
|
const records = [];
|
|
798
|
-
for (const line of
|
|
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
|
|
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;
|