@mmmbuto/nexuscrew 0.8.11 → 0.8.13
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/README.md +82 -36
- package/frontend/dist/assets/index-4rNd0SwZ.css +32 -0
- package/frontend/dist/assets/index-DuIR61l-.js +91 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cells/routes.js +137 -0
- package/lib/cli/doctor.js +26 -2
- package/lib/cli/service.js +19 -6
- package/lib/config.js +4 -0
- package/lib/fleet/builtin.js +146 -22
- package/lib/fleet/managed.js +72 -15
- package/lib/fleet/routes.js +6 -1
- package/lib/mcp/server.js +131 -3
- package/lib/nodes/tunnel-supervisor.js +26 -4
- package/lib/nodes/tunnel.js +22 -6
- package/lib/proxy/federation.js +44 -5
- package/lib/pty/attach.js +3 -2
- package/lib/runtime/env.js +50 -0
- package/lib/server.js +10 -1
- package/lib/settings/routes.js +4 -3
- package/lib/tmux/actions.js +96 -1
- package/lib/tmux/list.js +2 -1
- package/lib/update/core.js +35 -8
- package/lib/update/manager.js +6 -3
- package/lib/update/runner.js +7 -3
- package/package.json +2 -2
- package/skills/nexuscrew-agent/SKILL.md +14 -3
- package/frontend/dist/assets/index-DoUIJ4GM.js +0 -91
- package/frontend/dist/assets/index-DrrRAIg6.css +0 -32
package/lib/server.js
CHANGED
|
@@ -9,7 +9,7 @@ const { WebSocketServer } = require('ws');
|
|
|
9
9
|
const { defaults, loadConfig, assertLoopback, configJsonPath } = require('./config.js');
|
|
10
10
|
const { writeConfigAtomic } = require('./cli/init.js');
|
|
11
11
|
const { listSessions, attachedClients } = require('./tmux/list.js');
|
|
12
|
-
const { runAction, pasteToSession } = require('./tmux/actions.js');
|
|
12
|
+
const { runAction, pasteToSession, submitToSession } = require('./tmux/actions.js');
|
|
13
13
|
const { createSession, killSession, isProtectedSession } = require('./tmux/lifecycle.js');
|
|
14
14
|
const { createPreviewSampler } = require('./tmux/preview.js');
|
|
15
15
|
const { openAttach } = require('./pty/attach.js');
|
|
@@ -22,6 +22,7 @@ const VERSION = require('../package.json').version;
|
|
|
22
22
|
const { transcribe } = require('./voice/transcribe.js');
|
|
23
23
|
const { selectProvider } = require('./fleet/provider.js');
|
|
24
24
|
const { fleetRoutes } = require('./fleet/routes.js');
|
|
25
|
+
const { cellsRoutes } = require('./cells/routes.js');
|
|
25
26
|
const { fsRoutes } = require('./fs/routes.js');
|
|
26
27
|
const nodesStore = require('./nodes/store.js');
|
|
27
28
|
const nodesTunnel = require('./nodes/tunnel.js');
|
|
@@ -220,6 +221,14 @@ function createServer(opts = {}) {
|
|
|
220
221
|
sessionExists: (name) => sessionExists(cfg.tmuxBin, name),
|
|
221
222
|
}));
|
|
222
223
|
api.use('/fleet', fleetRoutes(fleetP, cfg));
|
|
224
|
+
api.use('/cells', cellsRoutes({
|
|
225
|
+
fleetP,
|
|
226
|
+
instanceId: () => (nodesStore.loadStore(nodesPath) || {}).nodeId || null,
|
|
227
|
+
submit: opts.cellSubmit || ((session, text, meta) => submitToSession(cfg.tmuxBin, session, text, {
|
|
228
|
+
engine: meta && meta.engine,
|
|
229
|
+
})),
|
|
230
|
+
readonly: proxyReadonly,
|
|
231
|
+
}));
|
|
223
232
|
api.use('/decks', decksRoutes({ cfg, decksPath }));
|
|
224
233
|
api.use('/fs', fsRoutes({ home: os.homedir() })); // folder-picker del dialog new session
|
|
225
234
|
// /nodes (read-only, per la settings UI B2): stesso formato di `nodes list --json`
|
package/lib/settings/routes.js
CHANGED
|
@@ -47,7 +47,7 @@ const nodesStore = require('../nodes/store.js');
|
|
|
47
47
|
const nodesCmds = require('../nodes/commands.js');
|
|
48
48
|
const nodesTunnel = require('../nodes/tunnel.js');
|
|
49
49
|
const peering = require('../nodes/peering.js');
|
|
50
|
-
const { probeHealth } = require('../proxy/federation.js');
|
|
50
|
+
const { probeHealth, waitForHealthyPeer } = require('../proxy/federation.js');
|
|
51
51
|
const { rotateToken } = require('../auth/token.js');
|
|
52
52
|
const { generateService, installService, installPath: svcInstallPath } = require('../cli/service.js');
|
|
53
53
|
const { detectPlatform, nodeBin, repoRoot, uid } = require('../cli/platform.js');
|
|
@@ -822,9 +822,10 @@ function settingsRoutes(deps = {}) {
|
|
|
822
822
|
if (!started.started && started.reason !== 'already running') {
|
|
823
823
|
throw new Error(started.reason || 'avvio SSH fallito');
|
|
824
824
|
}
|
|
825
|
-
const ready = await
|
|
825
|
+
const ready = await waitForHealthyPeer({
|
|
826
826
|
port: node.localPort, token: node.token, expectedInstanceId: node.nodeId,
|
|
827
|
-
fetchImpl,
|
|
827
|
+
fetchImpl, attempts: 8, delayMs: 200,
|
|
828
|
+
...(typeof seams.pairDelay === 'function' ? { delay: seams.pairDelay } : {}),
|
|
828
829
|
});
|
|
829
830
|
if (!ready || ready.status !== 'healthy') {
|
|
830
831
|
const diagnosis = (seams.readTunnelDiagnostic || nodesTunnel.readTunnelDiagnostic)(home, name, node.remotePort);
|
package/lib/tmux/actions.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { execFile } = require('node:child_process');
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { isValidSession } = require('../files/store.js');
|
|
3
8
|
|
|
4
9
|
// Window/pane navigation must NOT be emulated with client-side prefix keys
|
|
5
10
|
// (it depends on each host's bindings, which may be remapped or broken).
|
|
@@ -53,6 +58,7 @@ function runAction(tmuxBin, session, name) {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
const MAX_PASTE = 4096;
|
|
61
|
+
const MAX_SUBMIT = 8192;
|
|
56
62
|
|
|
57
63
|
// true se text contiene control char (codici 0x00-0x1f e 0x7f). Espresso via
|
|
58
64
|
// charCode invece di regex perche' il write-layer corrompe gli escape backslash
|
|
@@ -86,4 +92,93 @@ function pasteToSession(tmuxBin, session, text) {
|
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
function submitTextOk(text) {
|
|
96
|
+
if (typeof text !== 'string' || !text || text.length > MAX_SUBMIT) return false;
|
|
97
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
98
|
+
const c = text.charCodeAt(i);
|
|
99
|
+
if (c === 9 || c === 10 || c === 13) continue;
|
|
100
|
+
if (c < 32 || c === 127) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function execTmux(execFileImpl, tmuxBin, args, timeout = 5000) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
try {
|
|
108
|
+
execFileImpl(tmuxBin, args, { timeout }, (err, stdout) => {
|
|
109
|
+
resolve({ ok: !err, stdout: String(stdout || '') });
|
|
110
|
+
});
|
|
111
|
+
} catch (_) { resolve({ ok: false, stdout: '' }); }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Consegna un messaggio a un pane esatto e lo sottopone al TUI. Il testo non
|
|
116
|
+
// passa mai in argv: file temporaneo 0600 -> tmux load-buffer -> paste-buffer -p
|
|
117
|
+
// (bracketed paste), poi Enter con una chiamata separata. Il pane id viene
|
|
118
|
+
// risolto prima del paste e verificato di nuovo prima dell'Enter, evitando
|
|
119
|
+
// prefix match e il riuso del solo nome sessione.
|
|
120
|
+
async function submitToSession(tmuxBin, session, text, opts = {}) {
|
|
121
|
+
if (!isValidSession(session) || !submitTextOk(text)) {
|
|
122
|
+
return { submitted: false, reason: 'input non valido' };
|
|
123
|
+
}
|
|
124
|
+
const execFileImpl = opts.execFileImpl || execFile;
|
|
125
|
+
const fsImpl = opts.fsImpl || fs;
|
|
126
|
+
const tmpRoot = opts.tmpdir || os.tmpdir();
|
|
127
|
+
const delay = typeof opts.delay === 'function'
|
|
128
|
+
? opts.delay : (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
const codexComposer = /^(?:codex|codex-vl)(?:\.|$)/.test(String(opts.engine || ''));
|
|
130
|
+
const nonce = (opts.nonce || crypto.randomBytes(8).toString('hex')).replace(/[^a-f0-9]/g, '').slice(0, 32);
|
|
131
|
+
if (!nonce) return { submitted: false, reason: 'invio non disponibile' };
|
|
132
|
+
const buffer = `ncmsg-${nonce}`;
|
|
133
|
+
const tmp = path.join(tmpRoot, `.nexuscrew-message-${nonce}.txt`);
|
|
134
|
+
let loaded = false;
|
|
135
|
+
try {
|
|
136
|
+
fsImpl.writeFileSync(tmp, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
137
|
+
fsImpl.chmodSync(tmp, 0o600);
|
|
138
|
+
const pane = await execTmux(execFileImpl, tmuxBin,
|
|
139
|
+
['display-message', '-p', '-t', `=${session}:`, '#{pane_id}']);
|
|
140
|
+
const paneId = pane.stdout.trim();
|
|
141
|
+
if (!pane.ok || !/^%[0-9]+$/.test(paneId)) {
|
|
142
|
+
return { submitted: false, reason: 'sessione non raggiungibile' };
|
|
143
|
+
}
|
|
144
|
+
const load = await execTmux(execFileImpl, tmuxBin, ['load-buffer', '-b', buffer, tmp]);
|
|
145
|
+
if (!load.ok) return { submitted: false, reason: 'buffer non disponibile' };
|
|
146
|
+
loaded = true;
|
|
147
|
+
const paste = await execTmux(execFileImpl, tmuxBin,
|
|
148
|
+
['paste-buffer', '-p', '-t', paneId, '-b', buffer]);
|
|
149
|
+
if (!paste.ok) return { submitted: false, reason: 'consegna non riuscita' };
|
|
150
|
+
const paneAlive = async () => {
|
|
151
|
+
const verify = await execTmux(execFileImpl, tmuxBin,
|
|
152
|
+
['display-message', '-p', '-t', paneId, '#{session_name}\t#{pane_dead}\t#{pane_id}']);
|
|
153
|
+
const [verifiedSession, dead, verifiedPane] = verify.stdout.trim().split('\t');
|
|
154
|
+
return verify.ok && verifiedSession === session && dead === '0' && verifiedPane === paneId;
|
|
155
|
+
};
|
|
156
|
+
// Codex/Codex-VL have a paste-burst window: a too-early Enter can remain
|
|
157
|
+
// inside the composer. C-e is sent as its own tmux command after the paste,
|
|
158
|
+
// then the pane is revalidated before the separate Enter. Other clients
|
|
159
|
+
// still receive a short separation between paste and submit.
|
|
160
|
+
await delay(codexComposer ? 400 : 150);
|
|
161
|
+
if (!(await paneAlive())) return { submitted: false, reason: 'sessione terminata durante la consegna' };
|
|
162
|
+
if (codexComposer) {
|
|
163
|
+
const flush = await execTmux(execFileImpl, tmuxBin, ['send-keys', '-t', paneId, 'C-e']);
|
|
164
|
+
if (!flush.ok) return { submitted: false, reason: 'testo consegnato ma flush composer non riuscito' };
|
|
165
|
+
await delay(300);
|
|
166
|
+
if (!(await paneAlive())) return { submitted: false, reason: 'sessione terminata durante la consegna' };
|
|
167
|
+
}
|
|
168
|
+
const enter = await execTmux(execFileImpl, tmuxBin, ['send-keys', '-t', paneId, 'Enter']);
|
|
169
|
+
if (!enter.ok) return { submitted: false, reason: 'testo consegnato ma invio non riuscito' };
|
|
170
|
+
return { submitted: true, reason: codexComposer
|
|
171
|
+
? 'bracketed paste + burst flush + Enter separati'
|
|
172
|
+
: 'bracketed paste + Enter separato' };
|
|
173
|
+
} catch (_) {
|
|
174
|
+
return { submitted: false, reason: 'consegna non riuscita' };
|
|
175
|
+
} finally {
|
|
176
|
+
if (loaded) await execTmux(execFileImpl, tmuxBin, ['delete-buffer', '-b', buffer]);
|
|
177
|
+
try { fsImpl.unlinkSync(tmp); } catch (_) { /* cleanup best-effort */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
actionArgs, runAction, ACTIONS, pasteArgs, pasteToSession, scrollArgs,
|
|
183
|
+
submitTextOk, submitToSession, MAX_SUBMIT,
|
|
184
|
+
};
|
package/lib/tmux/list.js
CHANGED
package/lib/update/core.js
CHANGED
|
@@ -48,15 +48,42 @@ function compareVersions(a, b) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function registryVersion(stdout) {
|
|
51
|
-
const raw = String(stdout || '')
|
|
52
|
-
|
|
51
|
+
const raw = String(stdout || '')
|
|
52
|
+
.replace(/^\uFEFF/, '')
|
|
53
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
54
|
+
.trim();
|
|
55
|
+
const candidates = [];
|
|
56
|
+
const add = (value) => {
|
|
57
|
+
if (typeof value !== 'string') return;
|
|
58
|
+
const parsed = parseVersion(value);
|
|
59
|
+
if (parsed) candidates.push(parsed.raw);
|
|
60
|
+
};
|
|
53
61
|
try {
|
|
54
62
|
const decoded = JSON.parse(raw);
|
|
55
|
-
if (typeof decoded === 'string')
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
if (typeof decoded === 'string') add(decoded);
|
|
64
|
+
else if (Array.isArray(decoded)) decoded.forEach(add);
|
|
65
|
+
else if (decoded && typeof decoded === 'object') add(decoded.version);
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// npm versions differ in whether --json returns a JSON scalar or a plain
|
|
68
|
+
// line. Notices may also precede the value, so inspect complete lines but
|
|
69
|
+
// never extract a semver from the middle of arbitrary text.
|
|
70
|
+
for (const line of raw.split(/\r?\n/)) add(line.trim().replace(/^['"]|['"]$/g, ''));
|
|
71
|
+
}
|
|
72
|
+
const unique = [...new Set(candidates)];
|
|
73
|
+
if (unique.length !== 1) throw new Error('npm ha restituito una versione non valida');
|
|
74
|
+
return unique[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stableRuntimeDir(home) {
|
|
78
|
+
const dir = path.join(path.resolve(String(home || '')), '.nexuscrew');
|
|
79
|
+
try {
|
|
80
|
+
const st = fs.lstatSync(dir);
|
|
81
|
+
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error('directory runtime NexusCrew non sicura');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code !== 'ENOENT') throw error;
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
85
|
+
}
|
|
86
|
+
return dir;
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
function scrubError(error) {
|
|
@@ -153,5 +180,5 @@ function writeState(file, value) {
|
|
|
153
180
|
module.exports = {
|
|
154
181
|
PACKAGE_NAME, VERSION_RE, parseVersion, compareVersions, registryVersion,
|
|
155
182
|
scrubError, pidAlive, readLock, acquireUpdateLock, adoptUpdateLock, releaseUpdateLock,
|
|
156
|
-
readState, writeState,
|
|
183
|
+
readState, writeState, stableRuntimeDir,
|
|
157
184
|
};
|
package/lib/update/manager.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const { execFile, spawn } = require('node:child_process');
|
|
7
7
|
const {
|
|
8
8
|
PACKAGE_NAME, compareVersions, parseVersion, registryVersion, scrubError, pidAlive, readLock,
|
|
9
|
-
acquireUpdateLock, releaseUpdateLock, readState, writeState,
|
|
9
|
+
acquireUpdateLock, releaseUpdateLock, readState, writeState, stableRuntimeDir,
|
|
10
10
|
} = require('./core.js');
|
|
11
11
|
|
|
12
12
|
const DEFAULT_INITIAL_DELAY_MS = 60 * 1000;
|
|
@@ -24,10 +24,11 @@ function isNewer(candidate, current) {
|
|
|
24
24
|
try { return compareVersions(candidate, current) > 0; } catch (_) { return false; }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function lookupLatestNpm({ execFileImpl = execFile, timeoutMs = 20_000 } = {}) {
|
|
27
|
+
function lookupLatestNpm({ execFileImpl = execFile, timeoutMs = 20_000, home = os.homedir(), cwd } = {}) {
|
|
28
28
|
return new Promise((resolve, reject) => {
|
|
29
29
|
execFileImpl('npm', ['view', `${PACKAGE_NAME}@latest`, 'version', '--json'], {
|
|
30
30
|
encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024,
|
|
31
|
+
cwd: cwd || stableRuntimeDir(home),
|
|
31
32
|
}, (error, stdout) => {
|
|
32
33
|
if (error) return reject(error);
|
|
33
34
|
try { resolve(registryVersion(stdout)); } catch (e) { reject(e); }
|
|
@@ -42,6 +43,7 @@ function createNpmUpdater(opts = {}) {
|
|
|
42
43
|
const logPath = opts.logPath || path.join(home, '.nexuscrew', 'npm-update.log');
|
|
43
44
|
const lockPath = opts.lockPath || path.join(home, '.nexuscrew', 'npm-update.lock');
|
|
44
45
|
const runnerPath = opts.runnerPath || path.join(__dirname, 'runner.js');
|
|
46
|
+
const workDir = opts.cwd || stableRuntimeDir(home);
|
|
45
47
|
const supported = opts.supported === undefined ? isGlobalInstall(opts.packageRoot) : !!opts.supported;
|
|
46
48
|
const readonly = opts.readonly === true;
|
|
47
49
|
const lookupLatest = opts.lookupLatest || (() => lookupLatestNpm(opts));
|
|
@@ -135,7 +137,8 @@ function createNpmUpdater(opts = {}) {
|
|
|
135
137
|
process.execPath, ...runnerArgs]
|
|
136
138
|
: runnerArgs;
|
|
137
139
|
child = spawnImpl(bin, argv, {
|
|
138
|
-
detached: true, stdio: ['ignore', fd, fd],
|
|
140
|
+
detached: true, stdio: ['ignore', fd, fd], cwd: workDir,
|
|
141
|
+
env: { ...process.env, NEXUSCREW_UPDATE_RUNNER: '1' },
|
|
139
142
|
});
|
|
140
143
|
} catch (e) {
|
|
141
144
|
releaseUpdateLock(lockPath, reservation.token);
|
package/lib/update/runner.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('node:path');
|
|
|
7
7
|
const { execFileSync } = require('node:child_process');
|
|
8
8
|
const {
|
|
9
9
|
PACKAGE_NAME, parseVersion, scrubError, pidAlive, adoptUpdateLock, releaseUpdateLock, readState, writeState,
|
|
10
|
+
stableRuntimeDir,
|
|
10
11
|
} = require('./core.js');
|
|
11
12
|
|
|
12
13
|
function args(argv) {
|
|
@@ -69,6 +70,7 @@ async function runUpdate(opts = {}) {
|
|
|
69
70
|
if (!parseVersion(version)) throw new Error('versione update non valida');
|
|
70
71
|
const home = opts.home || os.homedir();
|
|
71
72
|
const statusPath = opts.statusPath || path.join(home, '.nexuscrew', 'npm-update.json');
|
|
73
|
+
const workDir = opts.cwd || stableRuntimeDir(home);
|
|
72
74
|
const execImpl = opts.execImpl || execFileSync;
|
|
73
75
|
const readInstalledVersion = opts.readInstalledVersion || (() => {
|
|
74
76
|
const p = path.resolve(__dirname, '..', '..', 'package.json');
|
|
@@ -76,7 +78,9 @@ async function runUpdate(opts = {}) {
|
|
|
76
78
|
});
|
|
77
79
|
const preflightImpl = opts.preflightImpl || (({ version: expectedVersion = version } = {}) => {
|
|
78
80
|
const bin = path.resolve(__dirname, '..', '..', 'bin', 'nexuscrew.js');
|
|
79
|
-
const output = execFileSync(process.execPath, [bin, 'version'], {
|
|
81
|
+
const output = execFileSync(process.execPath, [bin, 'version'], {
|
|
82
|
+
encoding: 'utf8', timeout: 20_000, stdio: ['ignore', 'pipe', 'pipe'], cwd: workDir,
|
|
83
|
+
});
|
|
80
84
|
if (String(output || '').trim() !== expectedVersion) throw new Error(`preflight CLI fallito: attesa ${expectedVersion}`);
|
|
81
85
|
return true;
|
|
82
86
|
});
|
|
@@ -96,7 +100,7 @@ async function runUpdate(opts = {}) {
|
|
|
96
100
|
try {
|
|
97
101
|
writeState(statusPath, { ...previous, phase: 'installing', targetVersion: version, updaterPid: process.pid, lastError: '' });
|
|
98
102
|
execImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${version}`, '--no-audit', '--no-fund'], {
|
|
99
|
-
stdio: 'inherit', timeout: 5 * 60 * 1000,
|
|
103
|
+
stdio: 'inherit', timeout: 5 * 60 * 1000, cwd: workDir,
|
|
100
104
|
});
|
|
101
105
|
const installed = String(readInstalledVersion() || '');
|
|
102
106
|
if (installed !== version) throw new Error(`verifica installazione fallita: attesa ${version}, trovata ${installed || 'sconosciuta'}`);
|
|
@@ -114,7 +118,7 @@ async function runUpdate(opts = {}) {
|
|
|
114
118
|
if (installedNew && parseVersion(previousVersion) && previousVersion !== version) {
|
|
115
119
|
try {
|
|
116
120
|
execImpl('npm', ['install', '--global', `${PACKAGE_NAME}@${previousVersion}`, '--no-audit', '--no-fund'], {
|
|
117
|
-
stdio: 'inherit', timeout: 5 * 60 * 1000,
|
|
121
|
+
stdio: 'inherit', timeout: 5 * 60 * 1000, cwd: workDir,
|
|
118
122
|
});
|
|
119
123
|
if (String(readInstalledVersion() || '') !== previousVersion) throw new Error(`rollback verify: attesa ${previousVersion}`);
|
|
120
124
|
await preflightImpl({ version: previousVersion, home, rollback: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/nexuscrew",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"start": "node bin/nexuscrew.js serve",
|
|
21
21
|
"dev": "node bin/nexuscrew.js serve",
|
|
22
22
|
"build": "cd frontend && npm install && npm run build && cd ..",
|
|
23
|
-
"test": "node tests/run-isolated.js"
|
|
23
|
+
"test": "node tests/run-isolated.js && npm --prefix frontend test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"express": "^4.21.0",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nexuscrew-agent
|
|
3
|
-
description: Use when an AI agent connected to NexusCrew must notify or ask the human, inspect
|
|
3
|
+
description: Use when an AI agent connected to NexusCrew must notify or ask the human, inspect runtime or deck context, list authorized Fleet cells, message an exact active cell, read its inbox, deliver a file, or recover from local tmux messages that remain unsubmitted or garbled. Prefer nc_notify, nc_ask, nc_status, nc_deck, nc_cells, nc_send_cell, nc_inbox, and nc_send_file; use bundled tmux/file helpers only as a declared compatibility fallback.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# NexusCrew Agent I/O
|
|
7
7
|
|
|
8
|
-
Use the [NexusCrew](https://github.com/DioNanos/nexuscrew) MCP bridge for communication with the human
|
|
8
|
+
Use the [NexusCrew](https://github.com/DioNanos/nexuscrew) MCP bridge for communication with the human, read-only runtime discovery, and authenticated delivery to active Fleet cells. Use the bundled helpers only for same-host tmux compatibility fallbacks.
|
|
9
9
|
|
|
10
10
|
## MCP bridge (preferred)
|
|
11
11
|
|
|
@@ -17,6 +17,8 @@ When the client exposes the NexusCrew MCP server, use these tools directly:
|
|
|
17
17
|
| Ask for a decision without blocking the agent | `nc_ask` |
|
|
18
18
|
| Inspect live tmux sessions and fleet cells | `nc_status` |
|
|
19
19
|
| Read the caller's deck name(s) and member cells/tmux sessions | `nc_deck` |
|
|
20
|
+
| List every authorized owner-qualified Fleet cell | `nc_cells` |
|
|
21
|
+
| Submit a bounded message to an exact active Fleet cell | `nc_send_cell` |
|
|
20
22
|
| List files received for the current session | `nc_inbox` |
|
|
21
23
|
| Deliver an absolute file path under the user's home | `nc_send_file` |
|
|
22
24
|
|
|
@@ -26,7 +28,8 @@ Apply these rules:
|
|
|
26
28
|
- Never include access tokens, credentials, private keys, push subscriptions, or other secrets in a notification, ask, file caption, or tool result.
|
|
27
29
|
- Treat `nc_ask` as non-blocking: it returns an ask ID immediately. Continue safe independent work or wait normally; the human response arrives in the originating tmux session with a `[human reply · ask#<id>]` prefix by default.
|
|
28
30
|
- Use `nc_status` instead of scraping NexusCrew state files. Use `nc_deck` instead of reading `decks.json`: it returns every local or authorized shared-owner deck containing the caller, preserves visual member order, identifies each deck and member by stable owner ID, includes viewer-valid Hydra routes, and reports `cell: null` when no managed Fleet match is available.
|
|
29
|
-
-
|
|
31
|
+
- Use `nc_cells` immediately before cross-cell delivery. Select its exact owner-qualified `id`; require `canReceive: true`; never guess a duplicate name or stale route.
|
|
32
|
+
- Use `nc_send_cell {target, message}` for actual Fleet-cell delivery. `submitted` confirms bracketed paste plus Enter only, never task acceptance or completion. Inactive targets are not queued.
|
|
30
33
|
- Use `nc_inbox` instead of guessing an inbox path when the tool is available.
|
|
31
34
|
- Pass `nc_send_file` an existing absolute regular-file path below the user's home. Let NexusCrew choose and sanitize the outbox name.
|
|
32
35
|
- Do not treat an MCP notification as a substitute for the final response required by the active client.
|
|
@@ -50,6 +53,10 @@ Don't hand-craft the path from a guessed session name — use `nc-deliver`, or d
|
|
|
50
53
|
|
|
51
54
|
## Sending text to a tmux session
|
|
52
55
|
|
|
56
|
+
For an active managed Fleet cell, prefer `nc_cells` followed by `nc_send_cell`.
|
|
57
|
+
The helper below is a same-host fallback for older NexusCrew runtimes or
|
|
58
|
+
non-Fleet sessions; it must not bypass federation visibility or routing ACLs.
|
|
59
|
+
|
|
53
60
|
`tmux send-keys 'msg' Enter` is **not** reliable: a TUI's paste-burst detector swallows the Enter and the message just sits in the composer, while exit code is still 0. Use the helper:
|
|
54
61
|
|
|
55
62
|
```bash
|
|
@@ -72,6 +79,8 @@ tmux capture-pane -t <session> -p | tail -8 # see the text / a running state
|
|
|
72
79
|
| Ask the human without blocking | `nc_ask` |
|
|
73
80
|
| Inspect NexusCrew runtime state | `nc_status` |
|
|
74
81
|
| Discover this session's deck neighbours | `nc_deck` |
|
|
82
|
+
| Discover authorized cells across nodes | `nc_cells` |
|
|
83
|
+
| Submit to an exact active Fleet cell | `nc_send_cell` |
|
|
75
84
|
| Give the human a file | `nc_send_file` or fallback `nc-deliver <file>...` |
|
|
76
85
|
| Read a file the human sent | `nc_inbox` or fallback to the path in the prompt |
|
|
77
86
|
| Send a prompt/command to a session | `nc-send <session> "text"` |
|
|
@@ -85,3 +94,5 @@ tmux capture-pane -t <session> -p | tail -8 # see the text / a running state
|
|
|
85
94
|
- **`tmux` aliased/wrapped by the shell** (e.g. an oh-my-zsh plugin) → the nudge silently fails. Helpers resolve the real binary; in ad-hoc commands use `/usr/bin/tmux` or `command tmux`.
|
|
86
95
|
- **Pasting onto a dirty composer** → text concatenates with whatever was there. Clear it first, or the previous line will merge with yours.
|
|
87
96
|
- **Delivering to a guessed session name** → file lands in an orphan folder with no badge. Use `nc-deliver` (it reads the real session).
|
|
97
|
+
- **Sending to an ambiguous cell name** → call `nc_cells` and use the full owner-qualified ID.
|
|
98
|
+
- **Calling `submitted` a completed task** → it is only a transport receipt; require an explicit result callback.
|