@mmmbuto/nexuscrew 0.9.30 → 0.9.31

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-BbcjoZS0.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-DaAL7P-m.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-DqCfZFGi.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.9.30"}
1
+ {"version":"0.9.31"}
package/lib/cli/doctor.js CHANGED
@@ -435,8 +435,37 @@ function checkTmuxServerTermuxPreload(runtimeEnv, execImpl, opts = {}) {
435
435
  let raw = '';
436
436
  try {
437
437
  raw = String(execImpl(opts.tmuxBin || 'tmux', ['show-environment', '-g', 'LD_PRELOAD'], { encoding: 'utf8' }) || '').trim();
438
- } catch (_) {
439
- return { name: 'tmux server termux-exec', ok: true, warn: true, detail: 'server tmux non attivo; probe rinviato al primo avvio' };
438
+ } catch (err) {
439
+ // Con il server ATTIVO `tmux show-environment -g <var>` esce 1 scrivendo
440
+ // "unknown variable" su stderr: execFileSync lo rigetta come errore, come
441
+ // fa per un server assente ("no server running"), ma i due esiti non sono
442
+ // la stessa cosa. L'assenza della variabile merita un WARN col rimedio,
443
+ // non l'esito «server non attivo».
444
+ const msg = String((err && (err.stderr || err.message)) || '');
445
+ if (/unknown variable/i.test(msg)) {
446
+ return {
447
+ name: 'tmux server termux-exec', ok: true, warn: true,
448
+ detail: 'server tmux attivo ma LD_PRELOAD non impostato (tmux: unknown variable); al primo avvio verra\' impostata, oppure: tmux set-environment -g LD_PRELOAD <libtermux-exec-ld-preload.so>',
449
+ };
450
+ }
451
+ // Classificazione dell'errore tmux: solo il «no server running» autentico
452
+ // dice che il server non c'è. Un «error connecting ... (Permission denied)»
453
+ // o qualunque altro fallimento lasciano la domanda aperta — il socket
454
+ // esiste ma questo utente non può leggerlo — e diventano «probe non
455
+ // verificabile» con lo stderr nel dettaglio, non un falso «server assente».
456
+ if (/permission denied/i.test(msg)) {
457
+ return {
458
+ name: 'tmux server termux-exec', ok: true, warn: true,
459
+ detail: `probe non verificabile: il socket tmux risponde "${msg}"; il server potrebbe essere attivo ma non leggibile con questo utente`,
460
+ };
461
+ }
462
+ if (/no server running|error connecting/i.test(msg)) {
463
+ return { name: 'tmux server termux-exec', ok: true, warn: true, detail: 'server tmux non attivo; probe rinviato al primo avvio' };
464
+ }
465
+ return {
466
+ name: 'tmux server termux-exec', ok: true, warn: true,
467
+ detail: `probe non verificabile: tmux ha risposto "${msg}"; stato del server non determinabile da qui`,
468
+ };
440
469
  }
441
470
  const match = raw.match(/^LD_PRELOAD=(.+)$/);
442
471
  const trusted = match && trustedTermuxPreload({ ...env, LD_PRELOAD: match[1] }, { platform, home: opts.home });
@@ -29,7 +29,17 @@ function decksRoutes({ cfg = {}, decksPath }) {
29
29
  };
30
30
  const find = (st, name) => st.decks.find((d) => d.name === name) || null;
31
31
  const checkRevision = (deck, rev) => {
32
- if (deck.revision !== rev) throw httpError(409, 'deck modificato da un’altra finestra', { current: deck });
32
+ if (deck.revision !== rev) {
33
+ // Traccia il conflitto di revisione con i campi che servono a
34
+ // capire chi lo ha causato (nessun merge lato server in questo ciclo).
35
+ console.log(JSON.stringify({
36
+ event: 'deck-revision-conflict',
37
+ name: deck.name,
38
+ expectedRevision: rev,
39
+ currentRevision: deck.revision,
40
+ }));
41
+ throw httpError(409, 'deck modificato da un’altra finestra', { current: deck });
42
+ }
33
43
  };
34
44
 
35
45
  r.get('/', (_req, res) => {
@@ -37,7 +37,7 @@ const {
37
37
  } = require('./definitions.js');
38
38
  const {
39
39
  publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine,
40
- CATALOG, credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames,
40
+ CATALOG, credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
41
41
  } = require('./managed.js');
42
42
  const { probeModel } = require('./model-probe.js');
43
43
  const { validEnvKey } = require('./env-key.js');
@@ -874,9 +874,18 @@ async function createBuiltinFleet(cfg = {}) {
874
874
  }
875
875
  // Se un engine sta USANDO questo modello, toglierlo lo renderebbe invalido
876
876
  // al prossimo avvio — e il messaggio arriverebbe a chi non sa cosa e'
877
- // cambiato. Meglio rifiutare adesso, dicendo chi lo usa.
877
+ // cambiato. Meglio rifiutare adesso, dicendo chi lo usa. Un engine risolve
878
+ // il modello dalla dichiarazione fatta sul SUO id o sul suo profilo
879
+ // (declaredFor cerca in [engineId, profileId]): il ritiro di
880
+ // {wantedId, wantedEngine} lo rompe solo se wantedEngine e' una di quelle
881
+ // chiavi — un altro engine con la propria dichiarazione non c'entra.
878
882
  const usato = (defs.engines || [])
879
- .filter((e) => e.managed && e.managed.model === wantedId)
883
+ .filter((e) => {
884
+ if (!e.managed || e.managed.model !== wantedId) return false;
885
+ if (e.id === wantedEngine) return true;
886
+ const profile = profileFor(e.managed.client, e.managed.provider, e.managed.credentialProfile || '');
887
+ return !!(profile && profile.id === wantedEngine);
888
+ })
880
889
  .map((e) => e.id);
881
890
  if (usato.length) {
882
891
  throw httpError(409, `modello in uso: ${usato.join(', ')}`);
@@ -1264,7 +1273,7 @@ async function createBuiltinFleet(cfg = {}) {
1264
1273
  engines: defs.engines.map((e) => {
1265
1274
  const out = { ...e, envKeys: Object.keys(e.env || {}).sort() };
1266
1275
  delete out.env;
1267
- if (e.managed) out.managedInfo = describeManaged(e.managed, { ...cfg, home, extraModels: extraModelsFrom(defs) });
1276
+ if (e.managed) out.managedInfo = describeManaged(e.managed, { ...cfg, home, extraModels: extraModelsFrom(defs), engineId: e.id });
1268
1277
  return out;
1269
1278
  }),
1270
1279
  cells: defs.cells.map((c) => {
@@ -170,7 +170,7 @@ function cellIdFromTmuxSession(session) {
170
170
  //
171
171
  // Un modello dichiarato NON aggira il controllo: lo estende in modo esplicito.
172
172
  // Un id mai dichiarato resta rifiutato.
173
- const MAX_MODELS = 64;
173
+ const MAX_MODELS = 128;
174
174
  const MODEL_ID_MAX = 128;
175
175
 
176
176
  function parseModel(m) {
@@ -245,7 +245,15 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null, o
245
245
  const models = [];
246
246
  const extraModels = new Map();
247
247
  if (d.models !== undefined) {
248
- if (!Array.isArray(d.models) || d.models.length > MAX_MODELS) return null;
248
+ if (!Array.isArray(d.models)) return null;
249
+ if (d.models.length > MAX_MODELS) {
250
+ // Come per gli engine: il rifiuto NOMINA il limite, altrimenti chi
251
+ // dichiara si ritrova un 400 generico senza sapere che il cap era
252
+ // quello. `onIssue` porta campo/valore/regola al messaggio del comando
253
+ // (400 di una scrittura), `reject` alla lettura del file.
254
+ if (typeof onIssue === 'function') onIssue({ field: 'models', value: d.models.length, rule: `troppi modelli dichiarati, cap ${MAX_MODELS}` });
255
+ return reject(`fleet.json rifiutato: ${d.models.length} modelli dichiarati, cap ${MAX_MODELS}; riduci i modelli a ${MAX_MODELS} o meno`);
256
+ }
249
257
  const seen = new Set();
250
258
  for (const m of d.models) {
251
259
  const model = parseModel(m);
@@ -1081,7 +1089,7 @@ function atomicWrite(p, data) {
1081
1089
  }
1082
1090
 
1083
1091
  const CAPS = {
1084
- SCHEMA_VERSION, MAX_ENGINES, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
1092
+ SCHEMA_VERSION, MAX_ENGINES, MAX_MODELS, MAX_CELLS, MAX_ARGS, MAX_ARG_LEN,
1085
1093
  MAX_ENV_KEYS, MAX_ENV_KEY_LEN, MAX_ENV_VAL_LEN, MAX_LABEL_LEN,
1086
1094
  MAX_COMMAND_LEN, MAX_CWD_LEN, MAX_MODEL_FLAG_LEN, MAX_MODEL_VAL_LEN,
1087
1095
  MAX_PROMPTFLAG_LEN, MAX_PROMPT_LEN, MAX_CELL_COMMAND_LEN, MAX_TMUXSESSION_LEN,
@@ -1267,7 +1267,17 @@ function describeManaged(spec, cfg = {}) {
1267
1267
  // (source 'unreadable', authConfigured false) non viene collassata in
1268
1268
  // 'missing' — il discriminante e' CHI ha fallito, non il esito grezzo.
1269
1269
  credentialSource: cred.source,
1270
- configured, models: [...(profile.models || [])], defaultModel: profile.model || '',
1270
+ configured,
1271
+ // Catalogo builtin del profilo PIU' i modelli dichiarati in
1272
+ // `definitions.models[]` per questo engine (o, legacy, per il profilo).
1273
+ // Senza l'unione un profilo custom — che catalogo non ha — elencava `[]`
1274
+ // anche con due modelli dichiarati: il boot li accettava (declaredFor) ma
1275
+ // la finestra, che legge da qui, non li mostrava.
1276
+ models: [...new Set([
1277
+ ...(profile.models || []),
1278
+ ...declaredModelsFor(extraModels, profile.id, cfg.engineId || null).map((m) => m.id),
1279
+ ])],
1280
+ defaultModel: profile.model || '',
1271
1281
  binary: binary || '', displayName: normalized.displayName || profile.label,
1272
1282
  reason,
1273
1283
  };
@@ -175,7 +175,7 @@ function createBuiltinRuntime(ctx) {
175
175
  needsPi ? discoverPiModels({ ...cfg, home }) : {},
176
176
  ]);
177
177
  const engines = cache.defs.engines.map((e) => {
178
- const managed = e.managed ? describeManaged(e.managed, { ...cfg, home, extraModels: extraModelsFrom(cache.defs) }) : null;
178
+ const managed = e.managed ? describeManaged(e.managed, { ...cfg, home, extraModels: extraModelsFrom(cache.defs), engineId: e.id }) : null;
179
179
  return {
180
180
  id: e.id, label: e.label, rc: !!e.rc,
181
181
  ...(managed ? {
@@ -184,7 +184,7 @@ function createBuiltinRuntime(ctx) {
184
184
  models: managed.provider === 'ollama-cloud' ? ollamaModels
185
185
  : (managed.client === 'pi'
186
186
  ? (e.managed.provider === 'custom'
187
- ? [e.managed.model]
187
+ ? [...new Set([e.managed.model, ...managed.models].filter(Boolean))]
188
188
  : (e.managed.provider === 'native'
189
189
  ? [...new Set(Object.values(piModels).flat())]
190
190
  : (piModels[e.managed.provider] || [])))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.30",
3
+ "version": "0.9.31",
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": {