@mmmbuto/nexuscrew 0.9.15 → 0.9.17

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +132 -3
  2. package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
  3. package/docs/AUDIO_SHARE.md +119 -0
  4. package/docs/CELL_PANEL.md +156 -0
  5. package/docs/CONFIGURATION.md +122 -0
  6. package/docs/FLEET.md +222 -0
  7. package/docs/INSTALLATION.md +128 -0
  8. package/docs/LIVE_PROMPT.md +110 -0
  9. package/docs/MCP.md +151 -0
  10. package/docs/NODES.md +214 -0
  11. package/docs/NOTIFICATIONS.md +101 -0
  12. package/docs/OPERATIONS.md +114 -0
  13. package/docs/README.md +55 -0
  14. package/docs/SECURITY.md +185 -0
  15. package/docs/VL_MICRO_NODES.md +107 -0
  16. package/docs/img/fleet-deck-desktop.png +0 -0
  17. package/docs/img/session-mobile.png +0 -0
  18. package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
  19. package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
  20. package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
  21. package/frontend/dist/assets/{index-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
  22. package/frontend/dist/index.html +1 -1
  23. package/frontend/dist/version.json +1 -1
  24. package/lib/audio/receipt.js +1 -1
  25. package/lib/auth/middleware.js +1 -1
  26. package/lib/cells/routes.js +26 -9
  27. package/lib/cells/scope-guard.js +2 -2
  28. package/lib/cli/commands.js +7 -7
  29. package/lib/cli/fleet-service.js +2 -2
  30. package/lib/cli/pidfile.js +15 -15
  31. package/lib/cli/service.js +2 -2
  32. package/lib/cli/stable-alias.js +1 -1
  33. package/lib/diagnostics/store.js +4 -1
  34. package/lib/files/routes.js +2 -2
  35. package/lib/fleet/builtin.js +59 -23
  36. package/lib/fleet/catalogs/opencode-go.json +3 -3
  37. package/lib/fleet/cell-exec.js +4 -4
  38. package/lib/fleet/cell-lease-server.js +17 -17
  39. package/lib/fleet/definitions.js +34 -15
  40. package/lib/fleet/launch-broker.js +3 -3
  41. package/lib/fleet/lease-client.js +1 -1
  42. package/lib/fleet/managed.js +97 -26
  43. package/lib/fleet/prompt-delivery.js +1 -1
  44. package/lib/fleet/runtime.js +3 -3
  45. package/lib/live-host/bridge.js +3 -3
  46. package/lib/live-host/routes.js +1 -1
  47. package/lib/mcp/server.js +7 -8
  48. package/lib/mcp/tools.js +1 -1
  49. package/lib/nodes/commands.js +2 -2
  50. package/lib/nodes/health.js +7 -11
  51. package/lib/nodes/identity.js +10 -13
  52. package/lib/nodes/store.js +2 -2
  53. package/lib/nodes/tunnel-supervisor.js +7 -7
  54. package/lib/nodes/tunnel.js +10 -11
  55. package/lib/notify/asks.js +4 -4
  56. package/lib/notify/persist.js +1 -1
  57. package/lib/notify/push.js +4 -4
  58. package/lib/notify/routes.js +12 -12
  59. package/lib/proxy/node-proxy.js +4 -4
  60. package/lib/proxy/panel-proxy.js +2 -2
  61. package/lib/pty/attach.js +1 -1
  62. package/lib/server.js +16 -4
  63. package/lib/settings/pairing-coordinator.js +1 -1
  64. package/lib/settings/routes.js +4 -4
  65. package/lib/tmux/lifecycle.js +4 -4
  66. package/lib/tmux/preview.js +1 -1
  67. package/lib/update/runner.js +9 -9
  68. package/lib/vl-nodes/broker.js +1 -1
  69. package/lib/ws/bridge.js +56 -3
  70. package/lib/ws/drop-counter.js +37 -0
  71. package/package.json +20 -2
  72. package/skills/nexuscrew/SKILL.md +1 -1
  73. package/frontend/index.html +0 -20
package/lib/ws/bridge.js CHANGED
@@ -118,10 +118,24 @@ function sendJson(ws, value) {
118
118
  }
119
119
 
120
120
  function bindWs(ws, deps) {
121
- const { openAttach, verifyToken, isValidSession = () => true, runAction = () => false, countClients = () => 0, defaults = {}, onAttach = () => {}, ptyGrace = null } = deps;
121
+ const { openAttach, verifyToken, isValidSession = () => true, runAction = () => false, countClients = () => 0, defaults = {}, onAttach = () => {}, ptyGrace = null, diagnostics = null, dropCounter = null } = deps;
122
122
  let record = null;
123
123
  let attached = false;
124
124
  let session = null;
125
+ let closeLogged = false;
126
+ // UNA sola riga diagnostica per socket: la prima chiusura vince. Le
127
+ // successive (close + error sullo stesso socket) non generano rumore.
128
+ const logClose = (level, event, message, meta = {}) => {
129
+ if (closeLogged) return;
130
+ closeLogged = true;
131
+ if (!diagnostics) return;
132
+ try { diagnostics.record(level, 'ws', event, message, { cell: session || undefined, ...meta }); } catch (_) {}
133
+ };
134
+ const countDrop = () => {
135
+ if (!dropCounter) return {};
136
+ const snap = dropCounter.recordDrop(session);
137
+ return { drops: snap.drops };
138
+ };
125
139
 
126
140
  const detach = () => {
127
141
  if (!record || record.ws !== ws) return;
@@ -144,6 +158,11 @@ function bindWs(ws, deps) {
144
158
  // fino al close event tiene in piedi un handle senza scopo.
145
159
  clearAttachTimer();
146
160
  sendJson(ws, { type: 'error', reason });
161
+ // Chiusura INITIATA DAL SERVER: il motivo e' qui, non nel close event.
162
+ // Level warn = sempre visibile (non richiede verbose).
163
+ logClose('warn', 'WS_SERVER_CLOSE', `Server closed socket: ${reason}`, {
164
+ reason: String(reason).slice(0, 48), closeCode: Number(code) || undefined, ...countDrop(),
165
+ });
147
166
  try { ws.close(code, reason); } catch (_) {}
148
167
  }
149
168
 
@@ -197,12 +216,14 @@ function bindWs(ws, deps) {
197
216
  attached = true;
198
217
  session = record.session;
199
218
  onAttach(session, ws);
219
+ logReattach();
200
220
  sendJson(ws, { type: 'attached', reconnectToken: record.resumeToken });
201
221
  return;
202
222
  }
203
223
  attached = true;
204
224
  clearAttachTimer();
205
225
  session = msg.session;
226
+ logReattach();
206
227
  // Resize default: when nobody else is attached, drive the session size so a
207
228
  // small phone gets a usable (non-clipped) view and clean line editing. When a
208
229
  // real terminal is already attached, default to ignore-size so we don't shrink
@@ -222,7 +243,7 @@ function bindWs(ws, deps) {
222
243
  // e' READONLY nessun client puo' declassarlo (msg.readonly:false non deve
223
244
  // vincere). Il client puo' solo AGGIUNGERE restrizione (attach read-only
224
245
  // su un server read-write). Contratto design §4b(6): READONLY blocca anche
225
- // le scritture PTY. (fix audit finale: prima il ?? faceva vincere il client.)
246
+ // le scritture PTY. (fix da revisione finale: prima il ?? faceva vincere il client.)
226
247
  readonly: defaults.readonlyDefault === true || !!msg.readonly,
227
248
  takeSize,
228
249
  cols: clamp(msg.cols, 20, 300, 80),
@@ -250,6 +271,9 @@ function bindWs(ws, deps) {
250
271
  record.pty.onExit((info) => {
251
272
  record.ended = true;
252
273
  record.exitCode = info && info.exitCode;
274
+ logClose('notice', 'PTY_EXIT', 'Sessione terminata dal PTY', {
275
+ exitCode: typeof record.exitCode === 'number' ? record.exitCode : undefined,
276
+ });
253
277
  if (!record.ws) return;
254
278
  sendJson(record.ws, { type: 'exit', code: record.exitCode });
255
279
  try { record.ws.close(1000, 'exit'); } catch (_) {}
@@ -272,8 +296,37 @@ function bindWs(ws, deps) {
272
296
  else if (msg.type === 'action') runAction(session, msg.name); // nav window/pane server-side
273
297
  }
274
298
 
299
+ // Alla riconnessione la DURATA della caduta e' il gap close->reopen della
300
+ // stessa sessione. Ritorni con gap = notice (sempre visibili); un primo
301
+ // attach senza storia e' debug (solo verbose).
302
+ function logReattach() {
303
+ if (!diagnostics) return;
304
+ if (!dropCounter) return;
305
+ try {
306
+ const reopen = dropCounter.recordReopen(session);
307
+ if (reopen.gapMs != null) {
308
+ diagnostics.record('notice', 'ws', 'WS_REATTACHED', 'Riconnesso dopo una caduta', {
309
+ cell: session, gapMs: reopen.gapMs, drops: reopen.drops,
310
+ });
311
+ } else {
312
+ diagnostics.record('debug', 'ws', 'WS_ATTACHED', 'Attach completato', { cell: session });
313
+ }
314
+ } catch (_) {}
315
+ }
316
+
275
317
  ws.on('message', onMessage);
276
- ws.on('close', () => { clearAttachTimer(); detach(); });
318
+ ws.on('close', (code) => {
319
+ clearAttachTimer(); detach();
320
+ if (closeLogged) return;
321
+ const closeCode = Number(code) || undefined;
322
+ if (ws.__ncCloseReason === 'heartbeat-timeout') {
323
+ logClose('warn', 'WS_HEARTBEAT_DROPPED', 'Heartbeat scaduto: connessione mezzo-aperta terminata', { closeCode, ...countDrop() });
324
+ } else if (closeCode === 1006) {
325
+ logClose('warn', 'WS_ABNORMAL_CLOSE', 'Chiusura senza handshake (drop TCP o terminate)', { closeCode, ...countDrop() });
326
+ } else {
327
+ logClose('notice', 'WS_CLIENT_CLOSE', 'Il client ha chiuso la connessione', { closeCode, ...countDrop() });
328
+ }
329
+ });
277
330
  ws.on('error', () => { clearAttachTimer(); detach(); });
278
331
  }
279
332
  module.exports = { bindWs, clamp, createPtyGraceStore, PTY_GRACE_MS };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+ // Contatore di cadute WebSocket: trasforma «celle che vanno e vengono»
3
+ // in numeri — quante cadute nella finestra (default 10 minuti) e, alla
4
+ // riconnessione, quanto è durata la caduta (gap tra close e reopen).
5
+ // In memoria, zero I/O: i valori viaggiano nei meta dei log di bridge.js.
6
+ function createDropCounter({ windowMs = 10 * 60 * 1000, now = Date.now } = {}) {
7
+ const drops = []; // { ts, sessionId }
8
+ const lastClose = new Map(); // sessionId -> ts
9
+ const key = (s) => String(s == null ? '' : s);
10
+ function prune(ts) {
11
+ const cutoff = ts - windowMs;
12
+ while (drops.length && drops[0].ts < cutoff) drops.shift();
13
+ }
14
+ return {
15
+ recordDrop(session, ts = now()) {
16
+ const k = key(session);
17
+ drops.push({ ts, sessionId: k });
18
+ if (k) lastClose.set(k, ts);
19
+ prune(ts);
20
+ return { drops: drops.length, windowSeconds: Math.round(windowMs / 1000) };
21
+ },
22
+ // Alla riconnessione: la durata della caduta e' il gap close->reopen.
23
+ // Consuma il last close: una connessione lunga e sana non e' «ritorno».
24
+ recordReopen(session, ts = now()) {
25
+ const k = key(session);
26
+ const last = lastClose.get(k);
27
+ lastClose.delete(k);
28
+ prune(ts);
29
+ return { gapMs: last == null ? null : Math.max(0, ts - last), drops: drops.length };
30
+ },
31
+ snapshot(ts = now()) {
32
+ prune(ts);
33
+ return { drops: drops.length, windowSeconds: Math.round(windowMs / 1000) };
34
+ },
35
+ };
36
+ }
37
+ module.exports = { createDropCounter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
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": {
@@ -11,8 +11,26 @@
11
11
  "lib/",
12
12
  "skills/",
13
13
  "frontend/dist/",
14
- "frontend/index.html",
15
14
  "CHANGELOG.md",
15
+ "docs/ALIBABA_TOKEN_PLAN.md",
16
+ "docs/AUDIO_SHARE.md",
17
+ "docs/CELL_PANEL.md",
18
+ "docs/CONFIGURATION.md",
19
+ "docs/FLEET.md",
20
+ "docs/INSTALLATION.md",
21
+ "docs/LIVE_PROMPT.md",
22
+ "docs/live-prompt-templates/LIVE_PROMPT.en.md",
23
+ "docs/live-prompt-templates/LIVE_PROMPT.es.md",
24
+ "docs/live-prompt-templates/LIVE_PROMPT.it.md",
25
+ "docs/img/fleet-deck-desktop.png",
26
+ "docs/img/session-mobile.png",
27
+ "docs/MCP.md",
28
+ "docs/NODES.md",
29
+ "docs/NOTIFICATIONS.md",
30
+ "docs/OPERATIONS.md",
31
+ "docs/README.md",
32
+ "docs/SECURITY.md",
33
+ "docs/VL_MICRO_NODES.md",
16
34
  "MCP_COMPANIONS.md",
17
35
  "mcp-companions.json",
18
36
  "LICENSE",
@@ -23,7 +23,7 @@ Five nouns carry almost everything:
23
23
  - **Node** — one installation on one machine. Its identity is an opaque
24
24
  `instanceId`; the human-readable name is a **label**, and two nodes may
25
25
  legitimately carry the same one. **Address things by id, never by name.**
26
- - **Cell** — one stable working identity (`Dev`, `Research`, …) bound to one
26
+ - **Cell** — one stable working identity (`Alpha`, `Beta`, …) bound to one
27
27
  tmux session and one engine. A cell is not a process: it survives restarts of
28
28
  the service, and stopping it does not end the work it was doing.
29
29
  - **Engine** — what a cell runs: an AI CLI, a plain shell, a command in a
@@ -1,20 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
6
- <meta name="theme-color" content="#0d1117" />
7
- <meta name="apple-mobile-web-app-capable" content="yes" />
8
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
- <meta name="mobile-web-app-capable" content="yes" />
10
- <meta name="application-name" content="NexusCrew" />
11
- <meta name="apple-mobile-web-app-title" content="NexusCrew" />
12
- <link rel="manifest" href="/manifest.json" />
13
- <title>NexusCrew</title>
14
- </head>
15
- <body>
16
- <div id="root"></div>
17
- <script>if('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');</script>
18
- <script type="module" src="/src/main.jsx"></script>
19
- </body>
20
- </html>