@mmmbuto/nexuscrew 0.8.27 → 0.8.29

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.
@@ -61,7 +61,7 @@ const ALIBABA_PI_MODELS = Object.freeze([
61
61
 
62
62
  const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
63
63
  const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', ...CUSTOM_KEYS]);
64
- const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi' });
64
+ const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', shell: 'Shell' });
65
65
  const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
66
66
 
67
67
  function validBaseUrl(value) {
@@ -109,6 +109,9 @@ const CATALOG = Object.freeze([
109
109
  // Pi uses its real provider IDs directly. OAuth providers do not need env keys.
110
110
  { id: 'pi.native', client: 'pi', provider: 'native', label: 'Pi configured default', auth: 'login', protocol: 'pi_native', default: true, core: true },
111
111
  { id: 'pi.alibaba-token-plan', client: 'pi', provider: 'alibaba-token-plan', label: 'Alibaba Token Plan Personal', auth: 'ALIBABA_CODE_API_KEY', endpoint: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', protocol: 'openai-completions', model: 'qwen3.8-max-preview', models: ALIBABA_TOKEN_PLAN_MODELS, strictModels: true, piProvider: 'alibaba-token-plan', piExtension: { baseUrl: 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1', models: ALIBABA_PI_MODELS }, delegatePiAuth: false, core: true, notice: 'alibaba-token-plan' },
112
+ // Device-local shell. Kept after the established default clients so adding
113
+ // it does not change the preselected engine for a newly-created cell.
114
+ { id: 'shell.local', client: 'shell', provider: 'local', label: 'Shell', auth: 'none', protocol: 'shell', default: true, core: true },
112
115
  { id: 'pi.anthropic', client: 'pi', provider: 'anthropic', label: 'Anthropic', auth: 'ANTHROPIC_API_KEY', protocol: 'pi_native', piProvider: 'anthropic', core: true },
113
116
  { id: 'pi.openai', client: 'pi', provider: 'openai', label: 'OpenAI API', auth: 'OPENAI_API_KEY', protocol: 'pi_native', piProvider: 'openai', core: true },
114
117
  { id: 'pi.openai-codex', client: 'pi', provider: 'openai-codex', label: 'OpenAI Codex OAuth', auth: 'login', protocol: 'pi_native', piProvider: 'openai-codex', core: true },
@@ -148,11 +151,12 @@ function normalizeManagedSpec(value) {
148
151
  if (!profile) return null;
149
152
  const model = value.model === undefined ? (profile.model || '') : value.model;
150
153
  if (typeof model !== 'string' || model.length > 128 || /[\x00-\x1f\x7f]/.test(model)) return null;
154
+ if (value.client === 'shell' && model) return null;
151
155
  if (profile.requiresModel && !model) return null;
152
156
  if (profile.strictModels && !(profile.models || []).includes(model)) return null;
153
157
  const permissionPolicy = value.permissionPolicy === undefined ? (profile.client === 'claude' ? 'unsafe' : 'standard') : value.permissionPolicy;
154
158
  if (permissionPolicy !== 'standard' && permissionPolicy !== 'unsafe') return null;
155
- if (value.client === 'pi' && permissionPolicy !== 'standard') return null;
159
+ if ((value.client === 'pi' || value.client === 'shell') && permissionPolicy !== 'standard') return null;
156
160
  const out = { client: profile.client, provider: profile.provider, model, permissionPolicy };
157
161
  if (profile.credentialProfile) out.credentialProfile = profile.credentialProfile;
158
162
  if (profile.credentialEnv) {
@@ -188,6 +192,16 @@ function defaultDefinitions() {
188
192
  };
189
193
  }
190
194
 
195
+ function defaultShellEngine() {
196
+ const profile = CATALOG.find((entry) => entry.id === 'shell.local');
197
+ return {
198
+ id: profile.id,
199
+ label: CLIENT_LABELS.shell,
200
+ rc: false,
201
+ managed: { client: 'shell', provider: 'local', model: '', permissionPolicy: 'standard' },
202
+ };
203
+ }
204
+
191
205
  function parseAssignments(raw) {
192
206
  const out = {};
193
207
  for (const line of raw.split(/\r?\n/)) {
@@ -269,6 +283,35 @@ function findBinary(client, home) {
269
283
  return null;
270
284
  }
271
285
 
286
+ // Resolve a device-local interactive shell without persisting a path in
287
+ // fleet.json. Candidates are ordered and fail closed. Symlinks are resolved
288
+ // first, then the existing command trust policy is applied to the real file.
289
+ function resolveInteractiveShell(cfg = {}) {
290
+ const env = cfg.env || process.env;
291
+ const platform = cfg.platform || process.platform;
292
+ const termux = termuxRuntimePaths(env, { platform, home: cfg.home });
293
+ const candidates = [];
294
+ if (typeof env.SHELL === 'string' && path.isAbsolute(env.SHELL)) candidates.push(env.SHELL);
295
+ if (termux?.prefix) {
296
+ candidates.push(path.join(termux.prefix, 'bin', 'bash'));
297
+ candidates.push(path.join(termux.prefix, 'bin', 'sh'));
298
+ }
299
+ candidates.push('/bin/bash', '/bin/sh');
300
+ const validate = cfg.validateCommandTrust
301
+ || ((command) => require('./definitions.js').validateCommandTrust(command));
302
+ for (const candidate of [...new Set(candidates)]) {
303
+ try {
304
+ const real = fs.realpathSync(candidate);
305
+ if (validate(real).ok) return real;
306
+ } catch (_) { /* next candidate */ }
307
+ }
308
+ return null;
309
+ }
310
+
311
+ function shellLoginArgs(command) {
312
+ return ['bash', 'zsh', 'sh', 'dash'].includes(path.basename(String(command || ''))) ? ['-l'] : [];
313
+ }
314
+
272
315
  // Termux reports process.platform === 'android' and deliberately has no
273
316
  // /usr/bin/env. npm CLI shims commonly resolve to a JavaScript file with
274
317
  // `#!/usr/bin/env node`; direct tmux exec then fails in the kernel before the
@@ -422,7 +465,9 @@ function describeManaged(spec, cfg = {}) {
422
465
  if (!normalized) return { configured: false, reason: 'invalid managed profile' };
423
466
  const home = cfg.home || require('node:os').homedir();
424
467
  const profile = profileFor(normalized.client, normalized.provider, normalized.credentialProfile || '');
425
- const binary = findBinary(normalized.client, home);
468
+ const binary = normalized.client === 'shell'
469
+ ? resolveInteractiveShell({ ...cfg, home })
470
+ : findBinary(normalized.client, home);
426
471
  const cred = credential(profile, normalized, cfg, home);
427
472
  // Pi can resolve credentials from its own documented /login auth store. Do
428
473
  // not inspect or copy that store; delegate native-provider auth to Pi.
@@ -585,13 +630,20 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
585
630
  let effectivePolicy = (override === 'standard' || override === 'unsafe') ? override : spec.permissionPolicy;
586
631
  // Pi resta sempre 'standard': normalizeManagedSpec rifiuta gia' unsafe nello spec
587
632
  // dell'engine, ma l'override PER-CELL bypasserebbe quel check -> clamp esplicito.
588
- if (spec.client === 'pi') effectivePolicy = 'standard';
633
+ if (spec.client === 'pi' || spec.client === 'shell') effectivePolicy = 'standard';
589
634
  info.permissionPolicy = effectivePolicy;
590
635
  if (effectivePolicy === 'unsafe') {
591
636
  if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
592
637
  if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
593
638
  }
594
- if (spec.client === 'claude') {
639
+ let shellOneShot = false;
640
+ if (spec.client === 'shell') {
641
+ const raw = cell?.commands && typeof cell.commands[engineId] === 'string'
642
+ ? cell.commands[engineId] : '';
643
+ shellOneShot = raw.trim().length > 0;
644
+ if (shellOneShot) args.push('-lc', raw);
645
+ else args.push(...shellLoginArgs(info.binary));
646
+ } else if (spec.client === 'claude') {
595
647
  if (spec.provider === 'native') {
596
648
  if (engine.rc !== false) args.push('--remote-control', `Cloud_${cell.id}`);
597
649
  } else if (profile.providerEnv) {
@@ -708,7 +760,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
708
760
  if (model) args.push('--model', model);
709
761
  if (spec.provider === 'alibaba-token-plan' && model === 'qwen3.8-max-preview') args.push('--thinking', 'xhigh');
710
762
  }
711
- if (cell?.prompt) args.push(cell.prompt);
763
+ if (spec.client !== 'shell' && cell?.prompt) args.push(cell.prompt);
712
764
  let command = info.binary;
713
765
  if (needsExplicitNode(info.binary, cfg.platform || process.platform, cfg.env || process.env)) {
714
766
  command = cfg.nodeExecPath || process.execPath;
@@ -716,6 +768,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
716
768
  }
717
769
  return { ok: true, info, engine: {
718
770
  ...engine, command, args, env, promptMode: 'managed-argv', clientBinary: info.binary,
771
+ ...(spec.client === 'shell' ? { shellOneShot } : {}),
719
772
  } };
720
773
  }
721
774
 
@@ -724,7 +777,7 @@ function publicCatalog() {
724
777
  id: p.id, client: p.client, clientLabel: CLIENT_LABELS[p.client], provider: p.provider,
725
778
  credentialProfile: p.credentialProfile || '', label: p.label, protocol: p.protocol,
726
779
  auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
727
- protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: p.client !== 'pi', requiresModel: !!p.requiresModel || !!p.custom,
780
+ protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
728
781
  permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
729
782
  rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
730
783
  credentialEnv: p.auth === 'dynamic' ? !!p.credentialEnv : (ENV_KEY_RE.test(p.auth || '') ? p.auth : false),
@@ -736,8 +789,8 @@ module.exports = {
736
789
  CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
737
790
  ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
738
791
  CLIENT_LABELS, normalizeManagedSpec,
739
- defaultDefinitions, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
792
+ defaultDefinitions, defaultShellEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
740
793
  discoverPiModels, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
741
794
  providerKeyPaths, parseProviderKeyFiles, credentialSources, credential,
742
- ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, ENV_KEY_RE,
795
+ ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell, shellLoginArgs, ENV_KEY_RE,
743
796
  };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  const express = require('express');
3
+ const { codeOf, phaseOf } = require('./causes.js');
3
4
 
4
5
  // Fallback difensivo per un adapter builtin incompleto nei test. Il provider di
5
6
  // prodotto dichiara sempre la propria capability list.
@@ -60,10 +61,23 @@ function fleetRoutes(fleetP, cfg = {}) {
60
61
  action: opts.action, cell: safeCell(req.body), errno: match[1], client: match[2], status: e.status || 500,
61
62
  });
62
63
  else emit('warn', 'FLEET_ACTION_FAILED', 'Fleet action failed', {
63
- action: opts.action, cell: safeCell(req.body), state: 'failed', status: e.status || 500,
64
+ action: opts.action, cell: safeCell(req.body), state: 'failed',
65
+ status: e.status || 500,
66
+ // Cause-preserving (T4): bounded enum only (lib/fleet/causes.js). The
67
+ // cause triple is {status, code, phase}; no free message, cwd/path,
68
+ // argv, env, prompt, token or credential ever enters here. Untagged /
69
+ // legacy errors degrade to the bounded UNKNOWN fallback.
70
+ code: codeOf(e.fleetCode), phase: phaseOf(e.fleetPhase),
64
71
  });
65
72
  }
66
- res.status(e.status || 500).json({ error: String(e.message || e), ...(e.data || {}) });
73
+ res.status(e.status || 500).json({
74
+ error: String(e.message || e),
75
+ ...(e.data || {}),
76
+ // Structured, bounded cause on the HTTP error too: stable enum only,
77
+ // never cwd/path, argv, env, prompt, token or credentials. Untagged
78
+ // errors (no fleetCode) keep the historical response shape.
79
+ ...(e.fleetCode ? { code: codeOf(e.fleetCode), phase: phaseOf(e.fleetPhase) } : {}),
80
+ });
67
81
  }
68
82
  };
69
83
 
@@ -85,7 +85,7 @@ function createBuiltinRuntime(ctx) {
85
85
  const remembered = c.permissionPolicies
86
86
  && (c.permissionPolicies[c.engine] === 'standard' || c.permissionPolicies[c.engine] === 'unsafe')
87
87
  ? c.permissionPolicies[c.engine] : null;
88
- const effectivePolicy = engineDef?.managed?.client === 'pi'
88
+ const effectivePolicy = ['pi', 'shell'].includes(engineDef?.managed?.client)
89
89
  ? 'standard'
90
90
  : (remembered || engineDefault || '');
91
91
  return {
@@ -149,16 +149,19 @@ function createBuiltinRuntime(ctx) {
149
149
  let launchEngine = engine;
150
150
  if (engine.managed) {
151
151
  const resolved = resolveManagedEngine(engine, cell, { ...cfg, home });
152
- if (!resolved.ok) throw httpError(400, `engine managed non configurato (${engine.id}): ${resolved.reason}`);
152
+ if (!resolved.ok) {
153
+ const code = engine.managed.client === 'shell' ? 'SHELL_NOT_AVAILABLE' : 'ENGINE_UNCONFIGURED';
154
+ throw httpError(400, `engine managed non configurato (${engine.id}): ${resolved.reason}`, null, { phase: 'preflight', code });
155
+ }
153
156
  launchEngine = resolved.engine;
154
157
  }
155
158
 
156
159
  // (2) trust del command PRIMA di lanciare
157
160
  const trust = validateCommandTrust(launchEngine.command);
158
- if (!trust.ok) throw httpError(400, `command non trusted (${launchEngine.command}): ${trust.reason}`);
161
+ if (!trust.ok) throw httpError(400, `command non trusted (${launchEngine.command}): ${trust.reason}`, null, { phase: 'preflight', code: 'COMMAND_UNTRUSTED' });
159
162
  // (3) cwd reale sotto la home
160
163
  const realCwd = resolveCwd(cell.cwd, home);
161
- if (!realCwd) throw httpError(400, `cwd non valida (deve esistere sotto la home): ${cell.cwd}`);
164
+ if (!realCwd) throw httpError(400, `cwd non valida (deve esistere sotto la home): ${cell.cwd}`, null, { phase: 'preflight', code: 'CWD_INVALID' });
162
165
 
163
166
  // (4)+(5) argv diretto (no shell). Every cell goes through the private
164
167
  // broker-backed supervisor: credentials never enter tmux state/argv and a
@@ -168,32 +171,46 @@ function createBuiltinRuntime(ctx) {
168
171
  // #5: elimina la race di riuso del nome sessione tra waitAlive e paste).
169
172
  const readyMs = cfg.launchReadyMs != null ? cfg.launchReadyMs : 500;
170
173
  const child = composeClientInvocation(launchEngine, cell);
171
- const ticket = await launchBroker.issue({
172
- command: child.command,
173
- args: child.args,
174
- env: {
175
- ...minimalEnv(),
176
- ...launchEngine.env,
177
- NEXUSCREW_MCP_SESSION: cell.tmuxSession,
178
- },
179
- supervise: {
180
- enabled: true,
181
- initialReadyMs: Math.max(50, Math.min(30000, Number(readyMs) || 500)),
182
- restartDelayMs: Math.max(50, Math.min(60000, Number(cfg.cellRestartDelayMs) || 1000)),
183
- maxRestartDelayMs: Math.max(100, Math.min(300000, Number(cfg.cellMaxRestartDelayMs) || 60000)),
184
- resetAfterMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRestartResetMs) || 30000)),
185
- rapidWindowMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRapidWindowMs) || 60000)),
186
- maxRapidRestarts: Math.max(1, Math.min(100, Number(cfg.cellMaxRapidRestarts) || 8)),
187
- },
188
- ...(launchEngine.promptMode === 'send-keys' && cell.prompt ? {
189
- restartPrompt: {
190
- tmuxBin,
191
- tmuxSession: cell.tmuxSession,
192
- prompt: cell.prompt,
193
- readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
174
+ let ticket;
175
+ try {
176
+ ticket = await launchBroker.issue({
177
+ command: child.command,
178
+ args: child.args,
179
+ env: {
180
+ ...minimalEnv(),
181
+ ...launchEngine.env,
182
+ NEXUSCREW_MCP_SESSION: cell.tmuxSession,
194
183
  },
195
- } : {}),
196
- });
184
+ supervise: {
185
+ enabled: !launchEngine.shellOneShot,
186
+ initialReadyMs: Math.max(50, Math.min(30000, Number(readyMs) || 500)),
187
+ restartDelayMs: Math.max(50, Math.min(60000, Number(cfg.cellRestartDelayMs) || 1000)),
188
+ maxRestartDelayMs: Math.max(100, Math.min(300000, Number(cfg.cellMaxRestartDelayMs) || 60000)),
189
+ resetAfterMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRestartResetMs) || 30000)),
190
+ rapidWindowMs: Math.max(1000, Math.min(3600000, Number(cfg.cellRapidWindowMs) || 60000)),
191
+ maxRapidRestarts: Math.max(1, Math.min(100, Number(cfg.cellMaxRapidRestarts) || 8)),
192
+ },
193
+ ...(launchEngine.promptMode === 'send-keys' && cell.prompt ? {
194
+ restartPrompt: {
195
+ tmuxBin,
196
+ tmuxSession: cell.tmuxSession,
197
+ prompt: cell.prompt,
198
+ readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
199
+ },
200
+ } : {}),
201
+ });
202
+ } catch (brokerErr) {
203
+ const bmsg = String((brokerErr && brokerErr.message) || brokerErr);
204
+ const bcode = /too large/i.test(bmsg) ? 'LAUNCH_BROKER_PAYLOAD'
205
+ : /closed/i.test(bmsg) ? 'LAUNCH_BROKER_CLOSED'
206
+ : /unsafe launch broker/i.test(bmsg) ? 'LAUNCH_BROKER_UNSAFE'
207
+ : 'LAUNCH_BROKER_FAILED';
208
+ const bpublic = bcode === 'LAUNCH_BROKER_PAYLOAD' ? 'launch broker payload rifiutato'
209
+ : bcode === 'LAUNCH_BROKER_CLOSED' ? 'launch broker non disponibile'
210
+ : bcode === 'LAUNCH_BROKER_UNSAFE' ? 'launch broker non sicuro'
211
+ : 'launch broker failed';
212
+ throw httpError(500, bpublic, null, { phase: 'launch-broker', code: bcode });
213
+ }
197
214
  const tmuxLaunchEngine = {
198
215
  command: process.execPath,
199
216
  args: [path.join(__dirname, 'cell-exec.js'), '--socket', ticket.socketPath, '--nonce', ticket.nonce],
@@ -204,14 +221,27 @@ function createBuiltinRuntime(ctx) {
204
221
  // Mantieni il pane morto solo durante la finestra di readiness: permette di
205
222
  // catturare un errore reale del client senza lasciare una sessione fantasma.
206
223
  // Il separatore e' interpretato da tmux (execFile argv diretto), non da shell.
207
- argv.push(';', 'set-option', '-w', '-t', `=${cell.tmuxSession}:`, 'remain-on-exit', 'on');
224
+ if (!launchEngine.shellOneShot) {
225
+ argv.push(';', 'set-option', '-w', '-t', `=${cell.tmuxSession}:`, 'remain-on-exit', 'on');
226
+ }
208
227
  const launch = await tmuxExec(tmuxBin, argv, { env: minimalEnv() });
209
228
  if (launch.err) {
210
229
  // Redazione (§9h): lo stderr di tmux puo' ecoare argv/env del comando lanciato.
211
- const why = /duplicate session/i.test(launch.stderr)
230
+ const dup = /duplicate session/i.test(launch.stderr);
231
+ const why = dup
212
232
  ? 'sessione già in esecuzione'
213
233
  : `tmux new-session failed: ${redactSecrets(launch.stderr.trim() || launch.err.message, launchEngine, cell)}`;
214
- throw httpError(/duplicate/i.test(launch.stderr) ? 409 : 500, why);
234
+ throw httpError(dup ? 409 : 500, why, null,
235
+ { phase: 'new-session', code: dup ? 'SESSION_DUPLICATE' : 'NEW_SESSION_FAILED' });
236
+ }
237
+
238
+ // Un comando Shell valorizzato e' intenzionalmente one-shot: l'uscita del
239
+ // processo, anche immediata o non-zero, e' il risultato del comando e non
240
+ // un crash del client Fleet. Non si mantiene il pane e non si abilita il
241
+ // supervisore; la cella torna inattiva quando la shell termina.
242
+ if (launchEngine.shellOneShot) {
243
+ cache = { ...cache, at: 0 };
244
+ return { ok: true, cell: cellId, session: cell.tmuxSession, prompt: null, oneShot: true };
215
245
  }
216
246
 
217
247
  // `tmux new-session -d` can return 0 even when the launched CLI exits a
@@ -235,7 +265,13 @@ function createBuiltinRuntime(ctx) {
235
265
  cache = { ...cache, at: 0 };
236
266
  const client = path.basename(launchEngine.clientBinary || launchEngine.command || 'client');
237
267
  const status = Number.isInteger(readiness.status) ? ` (exit ${readiness.status})` : '';
238
- throw httpError(500, `client ${client} terminato subito${status}: ${diagnostic || 'verifica login, provider, modello e argomenti dell\'engine'}`);
268
+ // Cause-preserving (T4): distinguish a cell-client spawn failure (the
269
+ // captured pane carries the stable 'cell spawn failed:' marker produced by
270
+ // cell-exec.js) from a generic early exit. Both stay on the readiness
271
+ // surface; the spawn branch keeps CELL_SPAWN_FAILED sanitized downstream.
272
+ const isSpawn = /cell spawn failed:/.test(diagnostic);
273
+ throw httpError(500, `client ${client} terminato subito${status}: ${diagnostic || 'verifica login, provider, modello e argomenti dell\'engine'}`, null,
274
+ isSpawn ? { phase: 'spawn-client', code: 'SPAWN_CLIENT_FAILED' } : { phase: 'readiness', code: 'CLIENT_EARLY_EXIT' });
239
275
  }
240
276
  if (readiness.target) {
241
277
  await tmuxExec(tmuxBin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.27",
3
+ "version": "0.8.29",
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": {