@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.
- package/CHANGELOG.md +132 -3
- package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
- package/docs/AUDIO_SHARE.md +119 -0
- package/docs/CELL_PANEL.md +156 -0
- package/docs/CONFIGURATION.md +122 -0
- package/docs/FLEET.md +222 -0
- package/docs/INSTALLATION.md +128 -0
- package/docs/LIVE_PROMPT.md +110 -0
- package/docs/MCP.md +151 -0
- package/docs/NODES.md +214 -0
- package/docs/NOTIFICATIONS.md +101 -0
- package/docs/OPERATIONS.md +114 -0
- package/docs/README.md +55 -0
- package/docs/SECURITY.md +185 -0
- package/docs/VL_MICRO_NODES.md +107 -0
- package/docs/img/fleet-deck-desktop.png +0 -0
- package/docs/img/session-mobile.png +0 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
- package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
- package/frontend/dist/assets/{index-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/audio/receipt.js +1 -1
- package/lib/auth/middleware.js +1 -1
- package/lib/cells/routes.js +26 -9
- package/lib/cells/scope-guard.js +2 -2
- package/lib/cli/commands.js +7 -7
- package/lib/cli/fleet-service.js +2 -2
- package/lib/cli/pidfile.js +15 -15
- package/lib/cli/service.js +2 -2
- package/lib/cli/stable-alias.js +1 -1
- package/lib/diagnostics/store.js +4 -1
- package/lib/files/routes.js +2 -2
- package/lib/fleet/builtin.js +59 -23
- package/lib/fleet/catalogs/opencode-go.json +3 -3
- package/lib/fleet/cell-exec.js +4 -4
- package/lib/fleet/cell-lease-server.js +17 -17
- package/lib/fleet/definitions.js +34 -15
- package/lib/fleet/launch-broker.js +3 -3
- package/lib/fleet/lease-client.js +1 -1
- package/lib/fleet/managed.js +97 -26
- package/lib/fleet/prompt-delivery.js +1 -1
- package/lib/fleet/runtime.js +3 -3
- package/lib/live-host/bridge.js +3 -3
- package/lib/live-host/routes.js +1 -1
- package/lib/mcp/server.js +7 -8
- package/lib/mcp/tools.js +1 -1
- package/lib/nodes/commands.js +2 -2
- package/lib/nodes/health.js +7 -11
- package/lib/nodes/identity.js +10 -13
- package/lib/nodes/store.js +2 -2
- package/lib/nodes/tunnel-supervisor.js +7 -7
- package/lib/nodes/tunnel.js +10 -11
- package/lib/notify/asks.js +4 -4
- package/lib/notify/persist.js +1 -1
- package/lib/notify/push.js +4 -4
- package/lib/notify/routes.js +12 -12
- package/lib/proxy/node-proxy.js +4 -4
- package/lib/proxy/panel-proxy.js +2 -2
- package/lib/pty/attach.js +1 -1
- package/lib/server.js +16 -4
- package/lib/settings/pairing-coordinator.js +1 -1
- package/lib/settings/routes.js +4 -4
- package/lib/tmux/lifecycle.js +4 -4
- package/lib/tmux/preview.js +1 -1
- package/lib/update/runner.js +9 -9
- package/lib/vl-nodes/broker.js +1 -1
- package/lib/ws/bridge.js +56 -3
- package/lib/ws/drop-counter.js +37 -0
- package/package.json +20 -2
- package/skills/nexuscrew/SKILL.md +1 -1
- 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
|
|
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', () => {
|
|
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.
|
|
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 (`
|
|
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
|
package/frontend/index.html
DELETED
|
@@ -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>
|