@mmmbuto/nexuscrew 0.9.76 → 0.9.77

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.
@@ -11,7 +11,7 @@
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-BURtPwUi.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-Cgd3OQws.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-B9GW2LHm.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.9.76"}
1
+ {"version":"0.9.77"}
@@ -41,7 +41,7 @@ const {
41
41
  CATALOG, CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient,
42
42
  credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
43
43
  normalizeManagedSpec, codexSurfaceNames,
44
- engineModelDiscoveryPlan, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
44
+ engineModelDiscoveryPlan, modelListingCredential, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
45
45
  } = require('./managed.js');
46
46
  const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
47
47
  const { createModelDiscovery, DISCOVERY_PROPOSAL_CAP } = require('./model-discovery.js');
@@ -1823,6 +1823,23 @@ async function createBuiltinFleet(cfg = {}) {
1823
1823
  ...(cause ? { cause } : {}),
1824
1824
  };
1825
1825
  }
1826
+ // Backup proposals for fields no layer could fill: the missing-data dialog
1827
+ // offers what the internal catalog knows, and the operator's confirmation
1828
+ // is what turns a proposal into a user value.
1829
+ function withBackupDefaults(records, profile) {
1830
+ const internalIndex = internalModelIndex(profile);
1831
+ return (Array.isArray(records) ? records : []).map((m) => {
1832
+ const backupValues = {};
1833
+ for (const field of ['contextWindow', 'maxTokens']) {
1834
+ if (m[field] === undefined) {
1835
+ const known = internalValueFor(internalIndex, m.id);
1836
+ if (known && known[field] !== undefined) backupValues[field] = known[field];
1837
+ }
1838
+ }
1839
+ return Object.keys(backupValues).length ? { ...m, backupValues } : m;
1840
+ });
1841
+ }
1842
+
1826
1843
  async function discoverEngineModels(engineId, opts = {}) {
1827
1844
  const id = String(engineId || '');
1828
1845
  if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
@@ -1843,16 +1860,10 @@ async function createBuiltinFleet(cfg = {}) {
1843
1860
  spec = profile ? { client: profile.client, provider: profile.provider, ...(profile.credentialProfile ? { credentialProfile: profile.credentialProfile } : {}) } : null;
1844
1861
  }
1845
1862
  if (!profile) { const e = new Error(`engine sconosciuto: ${id}`); e.status = 404; throw e; }
1846
- const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
1847
- if (!plan) {
1848
- // No HTTP listing to interrogate (login/native flows, local providers
1849
- // without a base URL): an honest empty answer, never an error, so the
1850
- // editor stays usable. The UI hides the section on `supported: false`.
1851
- return { engine: id, supported: false, source: 'unsupported', models: [] };
1852
- }
1853
1863
  // User layer: models declared in fleet.json for this engine (or its
1854
1864
  // profile) — the values the operator chose, always on top and never
1855
- // rewritten by discovery.
1865
+ // rewritten by discovery. Built BEFORE the plan: an engine without a
1866
+ // listing still has ids worth enriching.
1856
1867
  const extraModels = extraModelsFrom(defs);
1857
1868
  const userModels = [];
1858
1869
  const seenUser = new Set();
@@ -1876,33 +1887,57 @@ async function createBuiltinFleet(cfg = {}) {
1876
1887
  }
1877
1888
  const backupModels = backupModelsFor(profile);
1878
1889
  const wanted = [...new Set([...userModels, ...backupModels].map((m) => m.id))].slice(0, 32);
1890
+ // The plan is the HTTP listing, when the profile has one. When it does
1891
+ // not, the same composition still runs with the API layer explicitly
1892
+ // unavailable: known ids are enriched from the public snapshot, with no
1893
+ // invented collector, request or auth attempt.
1894
+ const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
1895
+ // The active model is configuration the operator already chose. It counts
1896
+ // as a known id only when it is the ONLY one: with a declared model or a
1897
+ // profile backup the proposal already has its ids, and adding it would
1898
+ // duplicate a row or change the order.
1899
+ const activeModel = spec && typeof spec.model === 'string' ? spec.model.trim() : '';
1900
+ const knownIds = !userModels.length && !backupModels.length && activeModel ? [activeModel] : [];
1879
1901
  const result = await engineModelDiscovery.discoverEngine({
1880
- ...plan,
1902
+ ...(plan || {}),
1903
+ engine: id,
1881
1904
  home,
1882
1905
  userModels, backupModels,
1906
+ knownIds,
1883
1907
  ...(wanted.length ? { detailIds: wanted } : {}),
1884
- publicCatalog: { provider: profile.provider, baseUrl: plan.baseUrl, ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}) },
1908
+ publicCatalog: {
1909
+ provider: profile.provider,
1910
+ ...(plan && plan.baseUrl ? { baseUrl: plan.baseUrl } : {}),
1911
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
1912
+ },
1885
1913
  ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
1886
1914
  ...(opts.refresh === true ? { noCache: true } : {}),
1915
+ ...(plan ? {} : { apiAvailable: false }),
1887
1916
  });
1888
1917
  // Curated response: `plan` (baseUrl, apiKey) stays out; apiModels is
1889
- // already merged (with provenance) into `models`.
1890
- // `backupValues` rides along only for fields NO layer could fill: the
1891
- // internal catalog's proposal for the missing-data dialog. A field any
1892
- // layer answered carries no proposal — the dialog asks for what nobody
1893
- // knows, and the operator's confirmation is what turns a proposal into a
1894
- // user value.
1895
- const internalIndex = internalModelIndex(profile);
1896
- const models = result.models.map((m) => {
1897
- const backupValues = {};
1898
- for (const field of ['contextWindow', 'maxTokens']) {
1899
- if (m[field] === undefined) {
1900
- const known = internalValueFor(internalIndex, m.id);
1901
- if (known && known[field] !== undefined) backupValues[field] = known[field];
1902
- }
1903
- }
1904
- return Object.keys(backupValues).length ? { ...m, backupValues } : m;
1905
- });
1918
+ // already merged (with provenance) into `models`. `backupValues` rides
1919
+ // along only for fields NO layer could fill.
1920
+ const models = withBackupDefaults(result.models, profile);
1921
+ if (!plan) {
1922
+ const cred = modelListingCredential(profile, spec, cfg, home);
1923
+ return {
1924
+ engine: id,
1925
+ supported: false,
1926
+ source: 'unsupported',
1927
+ status: result.status,
1928
+ stale: false,
1929
+ error: '',
1930
+ auth: {
1931
+ credentialState: cred.credentialState,
1932
+ ...(cred.envKey ? { envKey: cred.envKey } : {}),
1933
+ attempted: false,
1934
+ credentialSent: false,
1935
+ },
1936
+ ...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
1937
+ models,
1938
+ cap: DISCOVERY_PROPOSAL_CAP,
1939
+ };
1940
+ }
1906
1941
  return {
1907
1942
  engine: id,
1908
1943
  supported: true,
@@ -1313,52 +1313,72 @@ const sharedModelDiscovery = createModelDiscovery();
1313
1313
  // (catalog engines) the profile id IS the engine id.
1314
1314
  function engineModelDiscoveryPlan(profile, spec, cfg, home, engineId) {
1315
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 : '';
1316
+ const httpUrl = (value) => (typeof value === 'string' && /^https?:\/\//i.test(value) ? value : '');
1317
+ // Endpoint precedence follows the launcher: the normalized spec, the
1318
+ // generated provider extension, then the profile's own endpoint. A native
1319
+ // provider has no explicit endpoint here, and is never pointed at another
1320
+ // client's profile.
1321
+ const specBase = httpUrl(spec && spec.baseUrl);
1322
+ const extensionBase = httpUrl(profile.piExtension && profile.piExtension.baseUrl);
1323
+ const profileBase = httpUrl(profile.endpoint);
1324
+ // The wire name may come hyphenated (`openai-completions`, the Pi form) or
1325
+ // underscored (the normalized form). It is the same transport.
1326
+ const rawWire = spec && typeof spec.protocol === 'string' && spec.protocol
1327
+ ? spec.protocol : (typeof profile.protocol === 'string' ? profile.protocol : '');
1328
+ const wire = rawWire.replace(/-/g, '_');
1318
1329
  let collector = null;
1319
1330
  let baseUrl = '';
1320
1331
  if (profile.provider === 'ollama' || profile.provider === 'ollama-cloud') {
1332
+ // Ollama speaks its own listing API, whatever wire a client uses for chat.
1321
1333
  collector = 'ollama';
1322
- baseUrl = specBase || profileBase || 'http://127.0.0.1:11434';
1334
+ baseUrl = specBase || extensionBase || profileBase || 'http://127.0.0.1:11434';
1323
1335
  } else if (profile.provider === 'lmstudio') {
1324
1336
  collector = 'lmstudio';
1325
- baseUrl = specBase || 'http://127.0.0.1:1234';
1326
- } else if (profile.protocol === 'anthropic_messages') {
1337
+ baseUrl = specBase || extensionBase || 'http://127.0.0.1:1234';
1338
+ } else if (wire === 'anthropic_messages') {
1327
1339
  collector = 'anthropic';
1328
- baseUrl = specBase || profileBase;
1329
- } else if (profile.protocol === 'openai_responses' || profile.protocol === 'openai_completions') {
1340
+ baseUrl = specBase || extensionBase || profileBase;
1341
+ } else if (wire === 'openai_responses' || wire === 'openai_completions') {
1330
1342
  collector = 'openai-compat';
1331
- baseUrl = specBase || profileBase;
1343
+ baseUrl = specBase || extensionBase || profileBase;
1332
1344
  } else {
1345
+ // `pi_native`, login flows and any other client-owned wire: no explicit
1346
+ // listing contract, so no collector. An explicit endpoint alone is not
1347
+ // proof of a listing API.
1333
1348
  return null;
1334
1349
  }
1335
1350
  if (!baseUrl) return null;
1336
- const plan = { engine: engineId || profile.id, collector, baseUrl };
1337
- // The credential STATE travels with the plan, from the same resolution the
1338
- // launcher uses (the caller never re-reads the credential sources):
1339
- // none / configured / missing / unavailable, plus the env NAME when one
1340
- // exists and the editor already shows it. A file that exists but cannot be
1341
- // read is "unavailable", not "missing" — the same distinction describeManaged
1342
- // makes for launching. The value, its provenance paths/hashes and any
1343
- // exception text stay here: the plan hands the key to the collector in
1344
- // memory and nothing else leaves.
1351
+ return { engine: engineId || profile.id, collector, baseUrl, ...modelListingCredential(profile, spec, cfg, home) };
1352
+ }
1353
+
1354
+ // Credential resolution for a model listing, from the same path the launcher
1355
+ // uses (the caller never re-reads the credential sources): none / configured /
1356
+ // missing / unavailable, plus the env NAME when one exists and the editor
1357
+ // already shows it. A file that exists but cannot be read is "unavailable",
1358
+ // not "missing" — the same distinction describeManaged makes for launching.
1359
+ // The value, its provenance paths/hashes and any exception text stay here:
1360
+ // the value is handed to a collector in memory and nothing else leaves. An
1361
+ // engine with no listing still gets the STATE: the diagnosis refers to the
1362
+ // listing only, never to the client's own login.
1363
+ function modelListingCredential(profile, spec, cfg, home) {
1364
+ const out = { credentialState: 'unavailable' };
1345
1365
  try {
1346
1366
  const cred = credential(profile, spec || {}, cfg, home, { blocked: [] });
1347
- plan.credentialState = !cred ? 'unavailable'
1367
+ out.credentialState = !cred ? 'unavailable'
1348
1368
  : cred.value ? 'configured'
1349
1369
  : cred.source === 'unreadable' ? 'unavailable'
1350
1370
  : cred.source === 'missing' ? 'missing'
1351
1371
  : 'none';
1352
- if (cred && cred.value) plan.apiKey = cred.value;
1372
+ if (cred && cred.value) out.apiKey = cred.value;
1353
1373
  const envKey = cred && typeof cred.envKey === 'string' ? cred.envKey : '';
1354
1374
  // 'login'/'none' resolve to a pseudo key name: never invent one for them.
1355
- if (plan.credentialState !== 'none' && ENV_KEY_RE.test(envKey || '')) plan.envKey = envKey;
1375
+ if (out.credentialState !== 'none' && ENV_KEY_RE.test(envKey || '')) out.envKey = envKey;
1356
1376
  } catch (_) {
1357
1377
  // Resolution itself failed: the key is not known to be missing — it is
1358
1378
  // unavailable, and an anonymous rejection will say exactly that.
1359
- plan.credentialState = 'unavailable';
1379
+ out.credentialState = 'unavailable';
1360
1380
  }
1361
- return plan;
1381
+ return out;
1362
1382
  }
1363
1383
  async function discoverOllamaModels(opts = {}) {
1364
1384
  const now = Date.now(); const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
@@ -3165,7 +3185,7 @@ module.exports = {
3165
3185
  OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
3166
3186
  OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
3167
3187
  CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient, normalizeManagedSpec, profileFor,
3168
- defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, resolveManagedEngine, needsExplicitNode,
3188
+ defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, modelListingCredential, resolveManagedEngine, needsExplicitNode,
3169
3189
  discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
3170
3190
  codexSurfaceNames, writeCellCodexProfile,
3171
3191
  writeVlCellPrompt, vlSystemAppendGate,
@@ -44,7 +44,9 @@ const CACHE_VERSION = 1;
44
44
  const MAX_SHOW_CALLS = 32; // Ollama /api/show: only listed ids the caller asked about
45
45
  const ANTHROPIC_MAX_PAGES = 5; // pagination safety bound
46
46
  const ANTHROPIC_VERSION = '2023-06-01';
47
- const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
47
+ // Bracketed markers such as `[1m]` are part of the model id the fleet file
48
+ // accepts; they are kept intact and never stripped for a basename match.
49
+ const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,127}$/;
48
50
  const ENGINE_KEY_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
49
51
  const MERGE_FIELDS = Object.freeze(['contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning', 'deprecated']);
50
52
 
@@ -986,24 +988,37 @@ function createModelDiscovery({ fetchImpl: defaultFetchImpl = globalThis.fetch,
986
988
  async function discoverEngine(opts = {}) {
987
989
  const {
988
990
  userModels = [], backupModels = [], publicCatalogModels, publicCatalog,
989
- cap = DISCOVERY_PROPOSAL_CAP,
991
+ cap = DISCOVERY_PROPOSAL_CAP, apiAvailable = true, knownIds = [],
990
992
  } = opts;
991
- const layer = await fetchEngineModels(opts);
993
+ // An engine with no listing endpoint still has ids worth enriching: the
994
+ // declared models and the profile backup. There is no API layer at all —
995
+ // not a fake collector and not a fake fetch — so the merge runs with the
996
+ // API explicitly unavailable and never claims an API answer.
997
+ const layer = apiAvailable
998
+ ? await fetchEngineModels(opts)
999
+ : { status: 'unsupported', fetchedAt: 0, apiModels: [], stale: false, error: '', disappeared: [] };
992
1000
  let publicModels = Array.isArray(publicCatalogModels) ? publicCatalogModels : [];
993
1001
  let publicLayer = null;
1002
+ // Ids that are known from configuration but are not a user declaration
1003
+ // (for example the engine's own active model). They join the public layer
1004
+ // as bare ids: they can be enriched, never invented with values, and they
1005
+ // stay after the declared ids in the proposal order.
1006
+ const configuredIds = (Array.isArray(knownIds) ? knownIds : [])
1007
+ .filter((id) => typeof id === 'string' && id);
1008
+ const knownLayerIds = [...(layer.disappeared || []), ...configuredIds];
994
1009
  if (!Array.isArray(publicCatalogModels) && publicCatalog) {
995
1010
  publicLayer = await resolvePublicCatalogLayer(
996
- opts, layer.apiModels, userModels, backupModels, layer.disappeared || []
1011
+ opts, layer.apiModels, userModels, backupModels, knownLayerIds
997
1012
  );
998
1013
  publicModels = publicLayer.records;
999
1014
  }
1000
- const apiKnown = layer.status !== 'backup';
1015
+ const apiKnown = apiAvailable && layer.status !== 'backup';
1001
1016
  const models = mergeModelRecords({
1002
1017
  userModels,
1003
1018
  apiModels: layer.apiModels,
1004
1019
  publicCatalogModels: publicModels,
1005
1020
  backupModels,
1006
- knownModels: (layer.disappeared || []).map((id) => ({ id })),
1021
+ knownModels: knownLayerIds.map((id) => ({ id })),
1007
1022
  apiKnown,
1008
1023
  fetchedAt: layer.fetchedAt || now(),
1009
1024
  cap,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.76",
3
+ "version": "0.9.77",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {