@mmmbuto/nexuscrew 0.9.73 → 0.9.75
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/CHANGELOG.md +18 -0
- package/frontend/dist/assets/{index-C_z6r985.css → index-B9GW2LHm.css} +1 -1
- package/frontend/dist/assets/index-DMfpKI3n.js +169 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +178 -8
- package/lib/fleet/managed.js +142 -12
- package/lib/fleet/model-discovery.js +815 -0
- package/lib/fleet/routes.js +18 -1
- package/package.json +2 -2
- package/frontend/dist/assets/index-EAHAmDmX.js +0 -169
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DMfpKI3n.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B9GW2LHm.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.75"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -38,10 +38,13 @@ const {
|
|
|
38
38
|
} = require('./definitions.js');
|
|
39
39
|
const {
|
|
40
40
|
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine,
|
|
41
|
-
CATALOG,
|
|
41
|
+
CATALOG, CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient,
|
|
42
|
+
credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
|
|
42
43
|
normalizeManagedSpec, codexSurfaceNames,
|
|
44
|
+
engineModelDiscoveryPlan, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
|
|
43
45
|
} = require('./managed.js');
|
|
44
46
|
const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
|
|
47
|
+
const { createModelDiscovery, DISCOVERY_PROPOSAL_CAP } = require('./model-discovery.js');
|
|
45
48
|
const { sharedProbe: defaultEndpointProbe } = require('./endpoint-probe.js');
|
|
46
49
|
const { validEnvKey } = require('./env-key.js');
|
|
47
50
|
const { isReservedLiveName } = require('../live-host/reserved.js');
|
|
@@ -696,10 +699,14 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
696
699
|
}
|
|
697
700
|
boot = persistito;
|
|
698
701
|
}
|
|
699
|
-
|
|
702
|
+
// L'ordine delle aggiunte segue CLIENT_ORDER (agy, grok, shell...) e poi i
|
|
703
|
+
// client fuori lista in ordine stabile (kimi, vl): un fleet.json backfillato
|
|
704
|
+
// nasce gia' nell'ordine che il payload mostra. Ogni backfill resta
|
|
705
|
+
// idempotente e indipendente: il riordino non cambia cosa viene aggiunto.
|
|
700
706
|
boot = backfillAgyEngine(defsPath, boot, cfg);
|
|
701
|
-
boot = backfillKimiEngine(defsPath, boot, cfg.log);
|
|
702
707
|
boot = backfillGrokEngine(defsPath, boot, cfg);
|
|
708
|
+
boot = backfillShellEngine(defsPath, boot, cfg.log);
|
|
709
|
+
boot = backfillKimiEngine(defsPath, boot, cfg.log);
|
|
703
710
|
boot = backfillVlEngine(defsPath, boot, cfg.log);
|
|
704
711
|
// Ripara (non aggiunge) un desktop.local gia' presente col comando relativo.
|
|
705
712
|
// Il logger va PASSATO: senza, il messaggio di fallimento si ferma a un
|
|
@@ -1204,7 +1211,10 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1204
1211
|
// rifiutato se la dichiarazione non c'e' ancora — fail-closed corretto, ma
|
|
1205
1212
|
// che al ripristino si legge come «il backup era rotto».
|
|
1206
1213
|
const models = Array.isArray(opts.models) ? opts.models : [];
|
|
1207
|
-
|
|
1214
|
+
// Il tetto dei modelli accettati e' lo stesso della validazione del
|
|
1215
|
+
// fleet.json (CAPS.MAX_MODELS): un hardcode separato qui era gia'
|
|
1216
|
+
// divergente (64 contro 128) e rifiutava backup validi.
|
|
1217
|
+
if (models.length > CAPS.MAX_MODELS) throw httpError(400, `troppi modelli nel backup (max ${CAPS.MAX_MODELS})`);
|
|
1208
1218
|
for (const m of models) {
|
|
1209
1219
|
if (!m || typeof m !== 'object' || Array.isArray(m)) throw httpError(400, 'definizione modello non valida');
|
|
1210
1220
|
if (typeof m.id !== 'string' || !m.id.trim() || typeof m.engine !== 'string' || !m.engine.trim()) {
|
|
@@ -1378,9 +1388,20 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1378
1388
|
const realHome = resolveCwd(home, home);
|
|
1379
1389
|
return {
|
|
1380
1390
|
schemaVersion: defs.schemaVersion,
|
|
1381
|
-
|
|
1391
|
+
// UNICO punto di ordinamento del payload: API, UI e tool MCP leggono
|
|
1392
|
+
// tutti da qui. ordinaEngine riordina in lettura: il fleet.json resta
|
|
1393
|
+
// com'e', mai riscritto.
|
|
1394
|
+
engines: ordinaEngine(defs.engines).map((e) => {
|
|
1382
1395
|
const out = { ...e, envKeys: Object.keys(e.env || {}).sort() };
|
|
1383
1396
|
delete out.env;
|
|
1397
|
+
// `standard`: calcolato dal client (CLIENT_ORDER), mai persistito nel
|
|
1398
|
+
// fleet.json. La UI ci divide i selettori in due gruppi senza
|
|
1399
|
+
// duplicare la lista dei client standard.
|
|
1400
|
+
out.standard = isStandardClient(engineClient(e));
|
|
1401
|
+
// Le etichette escono in minuscolo: vale per le label generate dai
|
|
1402
|
+
// client e anche per quelle scritte a mano nel fleet.json, che nel
|
|
1403
|
+
// file restano esattamente come le ha scritte l'operatore.
|
|
1404
|
+
if (typeof out.label === 'string' && out.label) out.label = out.label.toLowerCase();
|
|
1384
1405
|
if (e.managed) out.managedInfo = describeManaged(e.managed, {
|
|
1385
1406
|
...cfg, home, extraModels: extraModelsFrom(defs), engineId: e.id,
|
|
1386
1407
|
endpointProbe: cfg.endpointProbe || defaultEndpointProbe,
|
|
@@ -1541,7 +1562,9 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1541
1562
|
rc: { type: 'boolean', required: false, default: false },
|
|
1542
1563
|
managed: {
|
|
1543
1564
|
type: 'object', requiredFor: 'managed',
|
|
1544
|
-
|
|
1565
|
+
// Enum allineato a CLIENT_ORDER (standard prima, nell'ordine voluto)
|
|
1566
|
+
// e poi i client noti fuori lista, in ordine stabile.
|
|
1567
|
+
client: { type: 'enum', values: [...CLIENT_ORDER, ...Object.keys(CLIENT_LABELS).filter((c) => !CLIENT_ORDER.includes(c))] },
|
|
1545
1568
|
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
1546
1569
|
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
1547
1570
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
@@ -1627,7 +1650,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1627
1650
|
}
|
|
1628
1651
|
|
|
1629
1652
|
function capabilities() {
|
|
1630
|
-
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'restore', 'schema', 'definitions', 'credentials', 'model-test', 'edit-model'];
|
|
1653
|
+
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'restore', 'schema', 'definitions', 'credentials', 'model-test', 'model-discovery', 'edit-model'];
|
|
1631
1654
|
}
|
|
1632
1655
|
|
|
1633
1656
|
// Un engine del documento con un `baseUrl` dichiarato a mano: indirizzo e
|
|
@@ -1693,6 +1716,153 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1693
1716
|
return { engine: profile.id, model: wanted, ...out };
|
|
1694
1717
|
}
|
|
1695
1718
|
|
|
1719
|
+
// Model discovery for the engine editor: the «you can use these» list with
|
|
1720
|
+
// per-field provenance (user > api > public catalog > backup). The engine's
|
|
1721
|
+
// endpoint and credential are resolved on the SAME road the launcher and
|
|
1722
|
+
// testModel use, live in memory only, and never enter the response: the
|
|
1723
|
+
// return shape is curated field by field below and the discovery module's
|
|
1724
|
+
// errors are fixed strings by construction (no key, no URL, ever).
|
|
1725
|
+
const engineModelDiscovery = createModelDiscovery();
|
|
1726
|
+
function backupModelsFor(profile) {
|
|
1727
|
+
if (!profile) return [];
|
|
1728
|
+
// ollama-cloud carries real measured descriptors: context window and
|
|
1729
|
+
// capabilities from the wired catalog, not bare ids.
|
|
1730
|
+
if (profile.provider === 'ollama-cloud') {
|
|
1731
|
+
return ollamaCloudCatalogModels().map((m) => ({
|
|
1732
|
+
id: m.id,
|
|
1733
|
+
...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
|
|
1734
|
+
...(Array.isArray(m.input) && m.input.includes('image') ? { vision: true } : {}),
|
|
1735
|
+
...(m.reasoning === true ? { reasoning: true } : {}),
|
|
1736
|
+
}));
|
|
1737
|
+
}
|
|
1738
|
+
return (Array.isArray(profile.models) ? profile.models : [])
|
|
1739
|
+
.filter((id) => typeof id === 'string' && id).map((id) => ({ id }));
|
|
1740
|
+
}
|
|
1741
|
+
// Internal-catalog PROPOSALS for the missing-data dialog (lot 3b): what the
|
|
1742
|
+
// wired database knows about a model id, for fields the discovery layers
|
|
1743
|
+
// could NOT fill. These are PROPOSALS only — they never join the merge, so
|
|
1744
|
+
// precedence and discovery stay untouched; the dialog shows them pre-filled
|
|
1745
|
+
// and whatever the operator confirms becomes a user value in fleet.json.
|
|
1746
|
+
// Lookup mirrors the wired ollama rule: exact key first, then the base name
|
|
1747
|
+
// without the ':tag' suffix ('deepseek-v4-flash:0731' -> 'deepseek-v4-flash').
|
|
1748
|
+
function internalModelIndex(profile) {
|
|
1749
|
+
const index = new Map();
|
|
1750
|
+
const add = (id, values) => {
|
|
1751
|
+
if (!id || !values) return;
|
|
1752
|
+
const known = index.get(id) || {};
|
|
1753
|
+
for (const field of ['contextWindow', 'maxTokens']) {
|
|
1754
|
+
if (values[field] !== undefined) known[field] = values[field];
|
|
1755
|
+
}
|
|
1756
|
+
if (Object.keys(known).length) index.set(id, known);
|
|
1757
|
+
};
|
|
1758
|
+
if (profile?.provider === 'ollama-cloud') {
|
|
1759
|
+
for (const m of ollamaCloudCatalogModels()) add(m.id, { contextWindow: m.contextWindow });
|
|
1760
|
+
}
|
|
1761
|
+
if (profile?.provider === 'alibaba-token-plan') {
|
|
1762
|
+
// ALIBABA_PI_MODELS carries the full descriptors (context, output) of
|
|
1763
|
+
// the wired database; the profile's own `models` list is bare ids.
|
|
1764
|
+
for (const m of ALIBABA_PI_MODELS) add(m.id, { contextWindow: m.contextWindow, maxTokens: m.maxTokens });
|
|
1765
|
+
}
|
|
1766
|
+
return index;
|
|
1767
|
+
}
|
|
1768
|
+
function internalValueFor(index, id) {
|
|
1769
|
+
if (!index.size || typeof id !== 'string') return null;
|
|
1770
|
+
if (index.has(id)) return index.get(id);
|
|
1771
|
+
const base = id.replace(/:[^:/]+$/, '');
|
|
1772
|
+
return base !== id && index.has(base) ? index.get(base) : null;
|
|
1773
|
+
}
|
|
1774
|
+
async function discoverEngineModels(engineId, opts = {}) {
|
|
1775
|
+
const id = String(engineId || '');
|
|
1776
|
+
if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
1777
|
+
const e = new Error('engine non valido'); e.status = 400; throw e;
|
|
1778
|
+
}
|
|
1779
|
+
const defs = reloadDefs();
|
|
1780
|
+
const declared = findEngine(defs, id);
|
|
1781
|
+
let profile = null;
|
|
1782
|
+
let spec = null;
|
|
1783
|
+
if (declared && declared.managed) {
|
|
1784
|
+
spec = normalizeManagedSpec(declared.managed, { extraModels: extraModelsFrom(defs), engineId: declared.id });
|
|
1785
|
+
profile = spec ? profileFor(spec.client, spec.provider, spec.credentialProfile || '') : null;
|
|
1786
|
+
}
|
|
1787
|
+
if (!profile) {
|
|
1788
|
+
// Not declared (or not resolvable): the catalog profile alone still
|
|
1789
|
+
// knows client, provider, endpoint and credential key.
|
|
1790
|
+
profile = CATALOG.find((p) => p.id === id) || null;
|
|
1791
|
+
spec = profile ? { client: profile.client, provider: profile.provider, ...(profile.credentialProfile ? { credentialProfile: profile.credentialProfile } : {}) } : null;
|
|
1792
|
+
}
|
|
1793
|
+
if (!profile) { const e = new Error(`engine sconosciuto: ${id}`); e.status = 404; throw e; }
|
|
1794
|
+
const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
|
|
1795
|
+
if (!plan) {
|
|
1796
|
+
// No HTTP listing to interrogate (login/native flows, local providers
|
|
1797
|
+
// without a base URL): an honest empty answer, never an error, so the
|
|
1798
|
+
// editor stays usable. The UI hides the section on `supported: false`.
|
|
1799
|
+
return { engine: id, supported: false, source: 'unsupported', models: [] };
|
|
1800
|
+
}
|
|
1801
|
+
// User layer: models declared in fleet.json for this engine (or its
|
|
1802
|
+
// profile) — the values the operator chose, always on top and never
|
|
1803
|
+
// rewritten by discovery.
|
|
1804
|
+
const extraModels = extraModelsFrom(defs);
|
|
1805
|
+
const userModels = [];
|
|
1806
|
+
const seenUser = new Set();
|
|
1807
|
+
for (const key of (id !== profile.id ? [id, profile.id] : [id])) {
|
|
1808
|
+
for (const m of declaredModelsFor(extraModels, key)) {
|
|
1809
|
+
if (seenUser.has(m.id)) continue;
|
|
1810
|
+
seenUser.add(m.id);
|
|
1811
|
+
userModels.push({
|
|
1812
|
+
id: m.id,
|
|
1813
|
+
...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
|
|
1814
|
+
...(Number.isFinite(m.maxTokens) ? { maxTokens: m.maxTokens } : {}),
|
|
1815
|
+
// definitions.js parses `reasoning` as a boolean: `false` is an
|
|
1816
|
+
// explicit operator answer, not an absence. Keep it exactly like
|
|
1817
|
+
// that, or a provider's `true` would override the user layer here
|
|
1818
|
+
// (the merge itself already handles booleans correctly).
|
|
1819
|
+
...(typeof m.reasoning === 'boolean' ? { reasoning: m.reasoning } : {}),
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const backupModels = backupModelsFor(profile);
|
|
1824
|
+
const wanted = [...new Set([...userModels, ...backupModels].map((m) => m.id))].slice(0, 32);
|
|
1825
|
+
const result = await engineModelDiscovery.discoverEngine({
|
|
1826
|
+
...plan,
|
|
1827
|
+
home,
|
|
1828
|
+
userModels, backupModels,
|
|
1829
|
+
...(wanted.length ? { detailIds: wanted } : {}),
|
|
1830
|
+
publicCatalog: { provider: profile.provider, baseUrl: plan.baseUrl, ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}) },
|
|
1831
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
1832
|
+
...(opts.refresh === true ? { noCache: true } : {}),
|
|
1833
|
+
});
|
|
1834
|
+
// Curated response: `plan` (baseUrl, apiKey) stays out; apiModels is
|
|
1835
|
+
// already merged (with provenance) into `models`.
|
|
1836
|
+
// `backupValues` rides along only for fields NO layer could fill: the
|
|
1837
|
+
// internal catalog's proposal for the missing-data dialog. A field any
|
|
1838
|
+
// layer answered carries no proposal — the dialog asks for what nobody
|
|
1839
|
+
// knows, and the operator's confirmation is what turns a proposal into a
|
|
1840
|
+
// user value.
|
|
1841
|
+
const internalIndex = internalModelIndex(profile);
|
|
1842
|
+
const models = result.models.map((m) => {
|
|
1843
|
+
const backupValues = {};
|
|
1844
|
+
for (const field of ['contextWindow', 'maxTokens']) {
|
|
1845
|
+
if (m[field] === undefined) {
|
|
1846
|
+
const known = internalValueFor(internalIndex, m.id);
|
|
1847
|
+
if (known && known[field] !== undefined) backupValues[field] = known[field];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return Object.keys(backupValues).length ? { ...m, backupValues } : m;
|
|
1851
|
+
});
|
|
1852
|
+
return {
|
|
1853
|
+
engine: id,
|
|
1854
|
+
supported: true,
|
|
1855
|
+
source: result.source,
|
|
1856
|
+
status: result.status,
|
|
1857
|
+
stale: result.stale === true,
|
|
1858
|
+
error: result.error || '',
|
|
1859
|
+
...(result.fetchedAt ? { fetchedAt: result.fetchedAt } : {}),
|
|
1860
|
+
...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
|
|
1861
|
+
models,
|
|
1862
|
+
cap: DISCOVERY_PROPOSAL_CAP,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1696
1866
|
async function close() { await launchBroker.close(); if (leaseManager) leaseManager.close(); }
|
|
1697
1867
|
|
|
1698
1868
|
return {
|
|
@@ -1702,7 +1872,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1702
1872
|
defineEngine, editEngine, removeEngine,
|
|
1703
1873
|
defineCell, editCell, removeCell, importCell, restoreCells, restoreEngines,
|
|
1704
1874
|
schema, definitions, capabilities,
|
|
1705
|
-
credentialStatus, setLocalCredential, removeLocalCredential, testModel,
|
|
1875
|
+
credentialStatus, setLocalCredential, removeLocalCredential, testModel, discoverEngineModels,
|
|
1706
1876
|
defineModel, editModel, removeModel, close,
|
|
1707
1877
|
// Fetta 2b: il lease Live esposto alle route /api/lease, che il bridge
|
|
1708
1878
|
// MCP delle celle consuma per register/refresh/recovery. Assente se il
|
package/lib/fleet/managed.js
CHANGED
|
@@ -10,6 +10,7 @@ const { execFile } = require('node:child_process');
|
|
|
10
10
|
const { ENV_KEY_RE } = require('./env-key.js');
|
|
11
11
|
const { termuxRuntimePaths } = require('../runtime/env.js');
|
|
12
12
|
const { readCredentialStore, safePrivateDir } = require('./credentials.js');
|
|
13
|
+
const { createModelDiscovery } = require('./model-discovery.js');
|
|
13
14
|
const { argomentiHookCodex, gateVersione, gatePiattaforma, EVENTI_CODEX } = require('./codex-hooks.js');
|
|
14
15
|
|
|
15
16
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
@@ -446,7 +447,40 @@ const CREDENTIAL_SOURCE_VALUES = Object.freeze([
|
|
|
446
447
|
'providers-shell', 'keys-file', 'secure-file', 'legacy',
|
|
447
448
|
'compatibility', 'missing', 'unreadable',
|
|
448
449
|
]);
|
|
449
|
-
|
|
450
|
+
// Etichette dei client in minuscolo (decisione operatore 2026-10-17): il nome
|
|
451
|
+
// mostrato e' il client stesso. La normalizzazione vale in uscita (payload,
|
|
452
|
+
// UI, tool MCP); il testo scritto a mano nel fleet.json resta nel file cosi'
|
|
453
|
+
// com'e' e viene mostrato in minuscolo dal punto di uscita del payload.
|
|
454
|
+
const CLIENT_LABELS = Object.freeze({ claude: 'claude', codex: 'codex', 'codex-vl': 'codex-vl', grok: 'grok', vl: 'vl', pi: 'pi', opencode: 'opencode', agy: 'agy', kimi: 'kimi', shell: 'shell' });
|
|
455
|
+
// Client "standard" nell'ordine voluto dall'operatore (2026-10-17). E' LA
|
|
456
|
+
// lista: il campo calcolato `standard` del payload delle definizioni, il rango
|
|
457
|
+
// di ordinaEngine(), l'ordine del seed e l'enum client dello schema derivano
|
|
458
|
+
// tutti da qui. I client fuori lista (vl, kimi, custom...) sono supportati:
|
|
459
|
+
// chiudono l'elenco in ordine stabile, mai buttati via.
|
|
460
|
+
const CLIENT_ORDER = Object.freeze(['claude', 'codex-vl', 'codex', 'pi', 'opencode', 'agy', 'grok', 'shell']);
|
|
461
|
+
// Il client di un engine: managed.client quando c'e' (engine gestiti), primo
|
|
462
|
+
// segmento dell'id altrimenti (claude.hosted -> claude): cosi' i derivati
|
|
463
|
+
// restano attaccati al loro client anche quando nascono come id.
|
|
464
|
+
function engineClient(engine) {
|
|
465
|
+
if (engine && engine.managed && typeof engine.managed.client === 'string' && engine.managed.client) return engine.managed.client;
|
|
466
|
+
if (engine && typeof engine.id === 'string' && engine.id) return engine.id.split('.')[0];
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
// Ordine di SOLA LETTURA: nessun fleet.json viene riscritto. Rango del client
|
|
470
|
+
// secondo CLIENT_ORDER, a parita' di rango resta l'ordine di inserimento
|
|
471
|
+
// (Array#sort e' stabile da ES2019); i client fuori lista chiudono l'array.
|
|
472
|
+
function ordinaEngine(engines) {
|
|
473
|
+
if (!Array.isArray(engines)) return engines;
|
|
474
|
+
const rank = new Map(CLIENT_ORDER.map((client, index) => [client, index]));
|
|
475
|
+
return [...engines].sort((a, b) => {
|
|
476
|
+
const ra = rank.get(engineClient(a));
|
|
477
|
+
const rb = rank.get(engineClient(b));
|
|
478
|
+
return (ra === undefined ? CLIENT_ORDER.length : ra) - (rb === undefined ? CLIENT_ORDER.length : rb);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function isStandardClient(client) {
|
|
482
|
+
return typeof client === 'string' && CLIENT_ORDER.includes(client);
|
|
483
|
+
}
|
|
450
484
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
451
485
|
|
|
452
486
|
function validBaseUrl(value) {
|
|
@@ -588,6 +622,23 @@ const CATALOG = Object.freeze([
|
|
|
588
622
|
{ id: 'pi.ollama', client: 'pi', provider: 'ollama', label: 'Ollama local', auth: 'none', protocol: 'openai-completions', piProvider: 'ollama', requiresModel: true, core: true, piExtension: { baseUrl: 'http://127.0.0.1:11434/v1', apiKey: 'ollama' } },
|
|
589
623
|
{ id: 'pi.custom', client: 'pi', provider: 'custom', label: 'Custom provider', auth: 'dynamic', protocol: 'openai-responses', protocols: ['openai-responses', 'anthropic-messages', 'openai-completions', 'google-generative-ai'], custom: true, core: true },
|
|
590
624
|
|
|
625
|
+
// OpenCode (client nativo, 0.9.75): TUI headless-friendly con gateway proprio
|
|
626
|
+
// («provider» integrato `opencode`) e login locale (`opencode auth login`).
|
|
627
|
+
// MISURATO su opencode 1.18.33 (zero credenziali salvate):
|
|
628
|
+
// i modelli *-free del gateway rispondono ANONYMOUS, quindi la cella parte
|
|
629
|
+
// anche prima del login; il set completo arriva con l'account. Per questo
|
|
630
|
+
// auth e' 'login' (delegato, mai chiavi gestite da NexusCrew) e NON c'e'
|
|
631
|
+
// l'array `models`: l'elenco dei modelli del gateway ruota lato opencode e
|
|
632
|
+
// un elenco statico qui marcirebbe in silenzio; il modello resta libero
|
|
633
|
+
// (niente strictModels) e la forma e' `<model>` (prefisso gateway aggiunto
|
|
634
|
+
// dal launcher) oppure `provider/model` esplicito per chi sa cosa chiede.
|
|
635
|
+
// I provider A CHIAVE del catalogo (zai, ollama-cloud, openrouter...) NON
|
|
636
|
+
// entrano in questo client oggi: opencode.jsonc accetta `apiKey` SOLO come
|
|
637
|
+
// stringa letterale (il riferimento {env:...} e' rifiutato, misurato), e
|
|
638
|
+
// scrivere una chiave su disco viola la regola del repo (mai segreti nei
|
|
639
|
+
// file generati). Residuo dichiarato nel referto del lotto.
|
|
640
|
+
{ id: 'opencode.native', client: 'opencode', provider: 'native', label: 'OpenCode account (gateway + local login)', auth: 'login', protocol: 'opencode_native', core: true, notice: 'opencode-native' },
|
|
641
|
+
|
|
591
642
|
// Agy: client gestito primario (Linux/macOS non-Termux). Auth delegata al
|
|
592
643
|
// login locale del client (nessuno store letto/copiato). Nessun remote-control
|
|
593
644
|
// NexusCrew; permission default 'standard' (unsafe aggiunge --dangerously-skip-
|
|
@@ -744,10 +795,13 @@ function normalizeManagedSpec(value, { extraModels = null, engineId = null, onIs
|
|
|
744
795
|
function defaultDefinitions() {
|
|
745
796
|
return {
|
|
746
797
|
schemaVersion: 1,
|
|
747
|
-
|
|
798
|
+
// Il seed segue CLIENT_ORDER: una flotta nuova nasce gia' nell'ordine che
|
|
799
|
+
// il payload mostra (ordinaEngine), senza dover contare sull'ordine con
|
|
800
|
+
// cui le voci default sono scritte nel CATALOG.
|
|
801
|
+
engines: ordinaEngine(CATALOG.filter((p) => p.default).map((p) => ({
|
|
748
802
|
id: p.id, label: CLIENT_LABELS[p.client], rc: !!p.rc,
|
|
749
803
|
managed: { client: p.client, provider: p.provider, model: p.model || '', permissionPolicy: p.client === 'claude' ? 'unsafe' : 'standard' },
|
|
750
|
-
})),
|
|
804
|
+
}))),
|
|
751
805
|
cells: [],
|
|
752
806
|
};
|
|
753
807
|
}
|
|
@@ -1242,6 +1296,50 @@ function applyStoreNeutralization(env, spec, profile) {
|
|
|
1242
1296
|
}
|
|
1243
1297
|
|
|
1244
1298
|
let ollamaCache = { at: 0, models: [] };
|
|
1299
|
+
// Shared model-discovery instance: per-engine HTTP collectors + disk cache.
|
|
1300
|
+
// The engine's key is resolved HERE (credentialSources/credential, the same
|
|
1301
|
+
// path the launcher uses) and handed to the module in memory only.
|
|
1302
|
+
const sharedModelDiscovery = createModelDiscovery();
|
|
1303
|
+
// Map a managed profile (+ normalized spec) to its model-discovery plan:
|
|
1304
|
+
// collector kind, endpoint base URL and the engine's own credential. This is
|
|
1305
|
+
// the per-engine wiring point for the discovery lots: same endpoint and same
|
|
1306
|
+
// key the launcher would use, never a parallel configuration. Returns null
|
|
1307
|
+
// when the profile has no HTTP listing to interrogate (login/native/local
|
|
1308
|
+
// runtimes without an HTTP endpoint, or a custom base URL that is not set).
|
|
1309
|
+
// `engineId` is the DECLARED engine id and becomes the cache key: two custom
|
|
1310
|
+
// engines of the same client resolve to one shared profile, and keying their
|
|
1311
|
+
// model cache by profile id would hand the second engine the first one's
|
|
1312
|
+
// listing without ever interrogating its endpoint. Without a declared id
|
|
1313
|
+
// (catalog engines) the profile id IS the engine id.
|
|
1314
|
+
function engineModelDiscoveryPlan(profile, spec, cfg, home, engineId) {
|
|
1315
|
+
if (!profile) return null;
|
|
1316
|
+
const specBase = spec && typeof spec.baseUrl === 'string' && /^https?:\/\//i.test(spec.baseUrl) ? spec.baseUrl : '';
|
|
1317
|
+
const profileBase = typeof profile.endpoint === 'string' && /^https?:\/\//i.test(profile.endpoint) ? profile.endpoint : '';
|
|
1318
|
+
let collector = null;
|
|
1319
|
+
let baseUrl = '';
|
|
1320
|
+
if (profile.provider === 'ollama' || profile.provider === 'ollama-cloud') {
|
|
1321
|
+
collector = 'ollama';
|
|
1322
|
+
baseUrl = specBase || profileBase || 'http://127.0.0.1:11434';
|
|
1323
|
+
} else if (profile.provider === 'lmstudio') {
|
|
1324
|
+
collector = 'lmstudio';
|
|
1325
|
+
baseUrl = specBase || 'http://127.0.0.1:1234';
|
|
1326
|
+
} else if (profile.protocol === 'anthropic_messages') {
|
|
1327
|
+
collector = 'anthropic';
|
|
1328
|
+
baseUrl = specBase || profileBase;
|
|
1329
|
+
} else if (profile.protocol === 'openai_responses' || profile.protocol === 'openai_completions') {
|
|
1330
|
+
collector = 'openai-compat';
|
|
1331
|
+
baseUrl = specBase || profileBase;
|
|
1332
|
+
} else {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
if (!baseUrl) return null;
|
|
1336
|
+
const plan = { engine: engineId || profile.id, collector, baseUrl };
|
|
1337
|
+
try {
|
|
1338
|
+
const cred = credential(profile, spec || {}, cfg, home);
|
|
1339
|
+
if (cred && cred.value) plan.apiKey = cred.value;
|
|
1340
|
+
} catch (_) { /* discovery without a key: key-less local endpoints still list */ }
|
|
1341
|
+
return plan;
|
|
1342
|
+
}
|
|
1245
1343
|
async function discoverOllamaModels(opts = {}) {
|
|
1246
1344
|
const now = Date.now(); const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
|
|
1247
1345
|
if (!opts.noCache && ollamaCache.models.length && now - ollamaCache.at < ttl) return [...ollamaCache.models];
|
|
@@ -1254,13 +1352,22 @@ async function discoverOllamaModels(opts = {}) {
|
|
|
1254
1352
|
|| sources.local.OLLAMA_API_KEY || sources.shell.OLLAMA_API_KEY || sources.keys.OLLAMA_API_KEY
|
|
1255
1353
|
|| sources.legacy.OLLAMA_API_KEY;
|
|
1256
1354
|
if (!apiKey) throw new Error('OLLAMA_API_KEY missing');
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1355
|
+
// Generalized on the model-discovery module (ollama collector): the HTTP
|
|
1356
|
+
// call, the sanitized errors and the last-good-on-disk cache live there.
|
|
1357
|
+
// The EXTERNAL behavior stays: the wired list filtered by what the API
|
|
1358
|
+
// declares live, and the full wired list when nothing is known.
|
|
1359
|
+
const layer = await sharedModelDiscovery.fetchEngineModels({
|
|
1360
|
+
engine: 'ollama-cloud', collector: 'ollama', baseUrl: 'https://ollama.com',
|
|
1361
|
+
apiKey, home, fetchImpl, timeoutMs: 2500, ttlMs: ttl, ...(opts.noCache ? { noCache: true } : {}),
|
|
1362
|
+
});
|
|
1363
|
+
// IL CONTRATTO ESTERNO NON CAMBIA (mandato L1: generalizzare «senza
|
|
1364
|
+
// cambiarne il comportamento esterno»). Un valore buono di ieri servito al
|
|
1365
|
+
// posto dell'API non e' una risposta di oggi: vale come "non si sa niente",
|
|
1366
|
+
// e allora si torna alla shortlist cablata intera. Solo `api` (o un valore
|
|
1367
|
+
// fresco, non stantio) dice cosa e' vivo adesso.
|
|
1368
|
+
const conosciuto = layer.status === 'api' || (layer.status === 'cache' && layer.stale === false);
|
|
1369
|
+
if (!conosciuto) throw new Error(layer.error || 'ollama-cloud discovery unavailable');
|
|
1370
|
+
const available = new Set(layer.apiModels.map((m) => m.id));
|
|
1264
1371
|
const models = OLLAMA_CLOUD_MODELS.filter((name) => available.has(name));
|
|
1265
1372
|
if (!models.length) throw new Error('no cloud models');
|
|
1266
1373
|
ollamaCache = { at: now, models }; return [...models];
|
|
@@ -2422,6 +2529,9 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2422
2529
|
// autonomous, nessuna domanda) NON e' mappato: il contratto NexusCrew
|
|
2423
2530
|
// distingue solo standard/unsafe e il default resta interattivo.
|
|
2424
2531
|
if (spec.client === 'kimi') args.push('--yolo');
|
|
2532
|
+
// OpenCode: unsafe -> --auto (flag reale di `opencode --help` 1.18.33:
|
|
2533
|
+
// «auto-approve permissions that are not explicitly denied (dangerous!)»).
|
|
2534
|
+
if (spec.client === 'opencode') args.push('--auto');
|
|
2425
2535
|
}
|
|
2426
2536
|
let shellOneShot = false;
|
|
2427
2537
|
if (spec.client === 'shell') {
|
|
@@ -2772,6 +2882,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
2772
2882
|
// dal push generico qui sotto come ultimo argomento. Senza prompt parte il
|
|
2773
2883
|
// TUI interattivo nella cwd della cella.
|
|
2774
2884
|
if (model) args.push('--model', model);
|
|
2885
|
+
} else if (spec.client === 'opencode') {
|
|
2886
|
+
// OpenCode TUI (0.9.75, misurato su 1.18.33): auth delegata al login/locale
|
|
2887
|
+
// del client (gateway `opencode`, free tier anche senza credenziali) —
|
|
2888
|
+
// niente env provider, niente credenziali su argv, NE' config scritta da
|
|
2889
|
+
// NexusCrew: MCP e provider custom restano nell'opencode.jsonc dell'utente
|
|
2890
|
+
// (o nel file per-cella che l'operatore passa con OPENCODE_CONFIG, che
|
|
2891
|
+
// qui non si compone). `--standalone` NON esiste in 1.18.33: ogni
|
|
2892
|
+
// invocazione gira col proprio server e muore con essa (misurato con ps
|
|
2893
|
+
// dopo la chiusura), quindi il TUI parte nudo — quel flag sarebbe servito
|
|
2894
|
+
// per un server di background condiviso per utente che questa versione non
|
|
2895
|
+
// avvia. Il modello viaggia come `-m provider/model`: un id senza `/`
|
|
2896
|
+
// e' del gateway integrato (`opencode/<model>`), uno gia' prefissato
|
|
2897
|
+
// passa tale e quale (l'operatore che ha config sue sa cosa chiede).
|
|
2898
|
+
// Il prompt di cella va su `--prompt` (precompila la casella di input,
|
|
2899
|
+
// pattern di agy --prompt-interactive): il posizionale e' il PATH di
|
|
2900
|
+
// progetto, mai testo. Niente send-keys: il classifier di readiness
|
|
2901
|
+
// (prompt-delivery) copre kimi/claude/vl e un marker nuovo va misurato
|
|
2902
|
+
// sul TUI vivo prima di essere fidato — residuo dichiarato.
|
|
2903
|
+
if (model) args.push('-m', model.includes('/') ? model : `opencode/${model}`);
|
|
2904
|
+
if (cell?.prompt) args.push('--prompt');
|
|
2775
2905
|
} else if (spec.client === 'vl') {
|
|
2776
2906
|
// VL/Vivling runtime TUI: auth is handled by the runtime in every variant
|
|
2777
2907
|
// (config.toml / VL_API_KEY by name via envPassthrough), never
|
|
@@ -3014,8 +3144,8 @@ module.exports = {
|
|
|
3014
3144
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
3015
3145
|
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
|
|
3016
3146
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|
|
3017
|
-
CLIENT_LABELS, normalizeManagedSpec, profileFor,
|
|
3018
|
-
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
3147
|
+
CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient, normalizeManagedSpec, profileFor,
|
|
3148
|
+
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, resolveManagedEngine, needsExplicitNode,
|
|
3019
3149
|
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
|
|
3020
3150
|
codexSurfaceNames, writeCellCodexProfile,
|
|
3021
3151
|
writeVlCellPrompt, vlSystemAppendGate,
|