@mmmbuto/nexuscrew 0.8.32 → 0.8.34
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 +49 -0
- package/MCP_COMPANIONS.md +67 -0
- package/README.md +42 -5
- package/frontend/dist/assets/{index-Dw90VJRY.js → index-cNTOIj7e.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +109 -4
- package/lib/cli/pidfile.js +39 -14
- package/lib/fleet/launch.js +16 -2
- package/lib/mcp/server.js +13 -0
- package/lib/nodes/tunnel.js +6 -1
- package/mcp-companions.json +103 -0
- package/package.json +3 -1
- package/skills/crew/SKILL.md +89 -0
- package/skills/crew/agents/openai.yaml +4 -0
- package/skills/fill-forms/SKILL.md +154 -0
- package/skills/fill-forms/agents/openai.yaml +4 -0
- package/skills/fill-forms/references/overlay-technique.md +99 -0
- package/skills/fill-forms/requirements.txt +4 -0
- package/skills/fill-forms/scripts/dump_docx.py +70 -0
- package/skills/fill-forms/scripts/fill_docx.py +207 -0
- package/skills/fill-forms/scripts/fill_pdf.py +424 -0
- package/skills/fill-forms/scripts/inspect_pdf.py +188 -0
- package/skills/fill-forms/scripts/prepare_signature.py +171 -0
- package/skills/mail-assistant/SKILL.md +71 -0
- package/skills/mail-assistant/agents/openai.yaml +4 -0
- package/skills/memory/SKILL.md +81 -0
- package/skills/memory/agents/openai.yaml +4 -0
- package/skills/nexuscrew-agent/SKILL.md +20 -0
- package/skills/vl-msa/SKILL.md +68 -0
- package/skills/vl-msa/agents/openai.yaml +4 -0
package/lib/cli/commands.js
CHANGED
|
@@ -112,8 +112,22 @@ function parseFlags(argv, valueFlags) {
|
|
|
112
112
|
|
|
113
113
|
function serve(opts = {}) {
|
|
114
114
|
const serverStart = opts.serverStart || require('../server.js').start;
|
|
115
|
+
const platform = opts.platform || detectPlatform();
|
|
116
|
+
// A legacy Termux:Boot script can start the new package from a replaceable
|
|
117
|
+
// npm directory. Correct the live process before it can create the shared
|
|
118
|
+
// tmux server, then atomically refresh both boot definitions for next boot.
|
|
119
|
+
if (platform === 'termux') {
|
|
120
|
+
stabilizeTermuxWorkingDirectory({ ...opts, platform });
|
|
121
|
+
try {
|
|
122
|
+
repairTermuxBootDefinitions({ ...opts, platform, clearMarker: true });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const warn = opts.lifecycleWarn || ((message) => process.stderr.write(`${message}\n`));
|
|
125
|
+
warn(`nexuscrew: Termux boot auto-repair pending — ${String(error && error.message || error)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
115
128
|
// Service manager e Termux:Boot entrano direttamente da `serve`, senza
|
|
116
|
-
// passare per smartUp.
|
|
129
|
+
// passare per smartUp. Su Termux ripara anche gli script di boot legacy.
|
|
130
|
+
// Per Fleet, ripara solo fleet.json MANCANTE; un file invalido
|
|
117
131
|
// resta intatto e il provider continua a fallire chiuso.
|
|
118
132
|
(opts.ensureFleetDefaultsImpl || ensureFleetDefaults)(opts);
|
|
119
133
|
if (opts.pidfile) {
|
|
@@ -279,6 +293,37 @@ function restart(opts = {}) {
|
|
|
279
293
|
const execImpl = opts.execImpl || execFileSync;
|
|
280
294
|
const log = opts.log || console.log;
|
|
281
295
|
if (!['linux', 'mac', 'termux'].includes(platform)) throw new Error(`restart: platform ${platform} non supportata`);
|
|
296
|
+
let termuxRepair = null;
|
|
297
|
+
if (platform === 'termux') {
|
|
298
|
+
try {
|
|
299
|
+
stabilizeTermuxWorkingDirectory({ ...opts, platform });
|
|
300
|
+
termuxRepair = repairTermuxBootDefinitions({
|
|
301
|
+
...opts, platform, clearMarker: false,
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
platform,
|
|
306
|
+
runtimeOwner: 'portable',
|
|
307
|
+
restarted: false,
|
|
308
|
+
reason: `Termux boot auto-repair failed: ${String(error && error.message || error)}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const complete = (result) => {
|
|
313
|
+
if (platform !== 'termux' || !result || result.restarted !== true || !termuxRepair?.markerPending) {
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
clearServiceMigrationMarker(opts.home || require('node:os').homedir(), opts.serviceMigrationMarkerPath);
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
...result,
|
|
322
|
+
serviceRepairPending: true,
|
|
323
|
+
warning: `Termux boot repair marker cleanup pending: ${String(error && error.message || error)}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
282
327
|
const before = resolveRuntimeOwner({ ...opts, platform, execImpl });
|
|
283
328
|
|
|
284
329
|
// A managed owner can use the service manager's atomic restart. In a
|
|
@@ -307,7 +352,7 @@ function restart(opts = {}) {
|
|
|
307
352
|
execImpl('launchctl', ['kickstart', '-k', label], { stdio: 'ignore' });
|
|
308
353
|
log(`restart: launchctl kickstart -k ${label}`);
|
|
309
354
|
}
|
|
310
|
-
return { platform, owner: before.owner, runtimeOwner: 'managed', restarted: true };
|
|
355
|
+
return complete({ platform, owner: before.owner, runtimeOwner: 'managed', restarted: true });
|
|
311
356
|
} catch (error) {
|
|
312
357
|
return { platform, owner: before.owner, runtimeOwner: 'managed', restarted: false, reason: String(error.message || error) };
|
|
313
358
|
}
|
|
@@ -319,7 +364,7 @@ function restart(opts = {}) {
|
|
|
319
364
|
const started = (opts.startPortableImpl || startPortable)({ ...opts, platform, spawnImpl: opts.spawnImpl });
|
|
320
365
|
const ok = !!(started && started.started !== false);
|
|
321
366
|
if (ok) log('restart: portable runtime started');
|
|
322
|
-
return { platform, owner: before.owner, runtimeOwner: 'portable', restarted: ok, reason: ok ? undefined : started && started.reason };
|
|
367
|
+
return complete({ platform, owner: before.owner, runtimeOwner: 'portable', restarted: ok, reason: ok ? undefined : started && started.reason });
|
|
323
368
|
}
|
|
324
369
|
|
|
325
370
|
// `restart` on a stopped runtime follows the explicit boot owner; with boot
|
|
@@ -330,7 +375,7 @@ function restart(opts = {}) {
|
|
|
330
375
|
: (opts.startPortableImpl || startPortable)({ ...opts, platform, spawnImpl: opts.spawnImpl });
|
|
331
376
|
const ok = !!(started && started.started !== false);
|
|
332
377
|
if (ok) log(`restart: ${useManaged ? 'managed' : 'portable'} runtime started`);
|
|
333
|
-
return { platform, owner: before.owner, runtimeOwner: useManaged ? 'managed' : 'portable', restarted: ok, reason: ok ? undefined : started && started.reason };
|
|
378
|
+
return complete({ platform, owner: before.owner, runtimeOwner: useManaged ? 'managed' : 'portable', restarted: ok, reason: ok ? undefined : started && started.reason });
|
|
334
379
|
}
|
|
335
380
|
|
|
336
381
|
function wizardComplete(configPath) {
|
|
@@ -406,6 +451,63 @@ function clearServiceMigrationMarker(home, override) {
|
|
|
406
451
|
}
|
|
407
452
|
}
|
|
408
453
|
|
|
454
|
+
// Termux app updates replace the global npm package while existing boot
|
|
455
|
+
// scripts remain on disk. Always enter the stable HOME before serving or
|
|
456
|
+
// booting Fleet so a legacy script cannot make tmux inherit the package cwd.
|
|
457
|
+
function stabilizeTermuxWorkingDirectory(opts = {}) {
|
|
458
|
+
const platform = opts.platform || detectPlatform();
|
|
459
|
+
if (platform !== 'termux') return { platform, changed: false };
|
|
460
|
+
const home = opts.home || require('node:os').homedir();
|
|
461
|
+
(opts.chdirImpl || process.chdir)(home);
|
|
462
|
+
return { platform, changed: true, cwd: home };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Refresh only an already-enabled Termux:Boot installation. Boot remains
|
|
466
|
+
// opt-in: missing scripts are never created by this repair path. A durable
|
|
467
|
+
// marker makes an interrupted refresh visible and retryable.
|
|
468
|
+
function repairTermuxBootDefinitions(opts = {}) {
|
|
469
|
+
const platform = opts.platform || detectPlatform();
|
|
470
|
+
const home = opts.home || require('node:os').homedir();
|
|
471
|
+
if (platform !== 'termux') return { platform, persistent: false, refreshed: false, markerPending: false };
|
|
472
|
+
const persistent = bootState({ ...opts, platform }).enabled;
|
|
473
|
+
if (!persistent) return { platform, persistent: false, refreshed: false, markerPending: false };
|
|
474
|
+
|
|
475
|
+
const markerPending = serviceMigrationPending(home, opts.serviceMigrationMarkerPath);
|
|
476
|
+
const refreshNeeded = serviceDefinitionNeedsRefresh(
|
|
477
|
+
platform, home, opts.installPath, opts.fleetInstallPath,
|
|
478
|
+
);
|
|
479
|
+
if (!markerPending && !refreshNeeded) {
|
|
480
|
+
return { platform, persistent: true, refreshed: false, markerPending: false };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (refreshNeeded) {
|
|
484
|
+
ensureServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
|
|
485
|
+
const result = (opts.runInitImpl || runInit)({
|
|
486
|
+
...opts,
|
|
487
|
+
home,
|
|
488
|
+
platform,
|
|
489
|
+
installBoot: true,
|
|
490
|
+
printUrl: false,
|
|
491
|
+
log: opts.lifecycleLog || (() => {}),
|
|
492
|
+
});
|
|
493
|
+
if (serviceDefinitionNeedsRefresh(platform, home, opts.installPath, opts.fleetInstallPath)) {
|
|
494
|
+
throw new Error('generated Termux boot definitions failed stable-HOME verification');
|
|
495
|
+
}
|
|
496
|
+
const failures = Array.isArray(result?.installFailures) ? result.installFailures : [];
|
|
497
|
+
if (failures.length) {
|
|
498
|
+
const components = [...new Set(failures.map((failure) => failure?.component)
|
|
499
|
+
.filter((component) => component === 'service' || component === 'fleet-companion'))];
|
|
500
|
+
throw new Error(`Termux boot definition install failed${components.length ? ` (${components.join(', ')})` : ''}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const pending = markerPending || refreshNeeded;
|
|
505
|
+
if (pending && opts.clearMarker !== false) {
|
|
506
|
+
clearServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
|
|
507
|
+
}
|
|
508
|
+
return { platform, persistent: true, refreshed: refreshNeeded, markerPending: pending };
|
|
509
|
+
}
|
|
510
|
+
|
|
409
511
|
function portAvailable(port, host = '127.0.0.1') {
|
|
410
512
|
return new Promise((resolve) => {
|
|
411
513
|
const s = net.createServer();
|
|
@@ -742,6 +844,7 @@ function update(opts = {}) {
|
|
|
742
844
|
// / cfg / exit (per test, no process.exit che uccide il runner).
|
|
743
845
|
async function runFleetBoot(opts = {}) {
|
|
744
846
|
const log = opts.log || console.log;
|
|
847
|
+
stabilizeTermuxWorkingDirectory(opts);
|
|
745
848
|
const loadConfig = opts.loadConfig || require('../config.js').loadConfig;
|
|
746
849
|
const selectProvider = opts.selectProvider || require('../fleet/provider.js').selectProvider;
|
|
747
850
|
const { bootCells } = require('../fleet/boot.js');
|
|
@@ -1065,6 +1168,8 @@ module.exports = {
|
|
|
1065
1168
|
serviceDefinitionNeedsRefresh,
|
|
1066
1169
|
fleetServiceDefinitionNeedsRefresh,
|
|
1067
1170
|
serviceMigrationPending,
|
|
1171
|
+
stabilizeTermuxWorkingDirectory,
|
|
1172
|
+
repairTermuxBootDefinitions,
|
|
1068
1173
|
portAvailable, findAvailablePort, probeNexusCrew, waitForNexusCrew, openPwa, startPortable, wizardComplete,
|
|
1069
1174
|
servicePinsLegacyPort,
|
|
1070
1175
|
isServiceRunning, readRoles,
|
package/lib/cli/pidfile.js
CHANGED
|
@@ -30,9 +30,24 @@ function removePidfile(p) {
|
|
|
30
30
|
try { fs.unlinkSync(p); } catch (_) {}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// A PID can exist without belonging to this UID. Android commonly reuses PIDs
|
|
34
|
+
// across app sandboxes; kill(pid, 0) then returns EPERM and /proc is hidden.
|
|
35
|
+
// Keep generic existence separate from NexusCrew ownership so a foreign PID
|
|
36
|
+
// can never keep one of our pidfiles "alive" forever.
|
|
37
|
+
function pidOwnership(pid, killImpl = process.kill) {
|
|
38
|
+
try {
|
|
39
|
+
killImpl(pid, 0);
|
|
40
|
+
return 'owned';
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e && e.code === 'EPERM') return 'foreign';
|
|
43
|
+
if (e && e.code === 'ESRCH') return 'missing';
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pidExists(pid, killImpl = process.kill) {
|
|
49
|
+
const ownership = pidOwnership(pid, killImpl);
|
|
50
|
+
return ownership === 'owned' || ownership === 'foreign';
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
function readCmdline(pid) {
|
|
@@ -50,36 +65,46 @@ function cmdMatches(savedCmd, liveCmd) {
|
|
|
50
65
|
return liveCmd.includes(savedCmd) || savedCmd.includes(liveCmd);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
// true se il pid
|
|
54
|
-
|
|
68
|
+
// true se il pid appartiene a questo UID E il cmd matcha (o non verificabile).
|
|
69
|
+
// EPERM is deliberately false: NexusCrew must neither adopt nor signal a
|
|
70
|
+
// process owned by another Android/Linux user.
|
|
71
|
+
function isAlive(meta, impl = {}) {
|
|
55
72
|
if (!meta || !Number.isFinite(meta.pid)) return false;
|
|
56
|
-
if (
|
|
73
|
+
if (pidOwnership(meta.pid, impl.killImpl || process.kill) !== 'owned') return false;
|
|
57
74
|
if (meta.cmd) {
|
|
58
|
-
const live = readCmdline(meta.pid);
|
|
75
|
+
const live = (impl.readCmdlineImpl || readCmdline)(meta.pid);
|
|
59
76
|
if (live) return cmdMatches(meta.cmd, live);
|
|
60
77
|
}
|
|
61
78
|
return true;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
// Rimuove pidfile stale (pid morto o non verificabile). Ritorna true se rimosso.
|
|
65
|
-
function cleanStale(p) {
|
|
82
|
+
function cleanStale(p, impl = {}) {
|
|
66
83
|
const meta = readPidfile(p);
|
|
67
84
|
if (!meta) return false;
|
|
68
|
-
if (!isAlive(meta)) { removePidfile(p); return true; }
|
|
85
|
+
if (!isAlive(meta, impl)) { removePidfile(p); return true; }
|
|
69
86
|
return false;
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
// Kill verificato: legge pidfile, verifica pid+cmd, signal. MAI broad match by name.
|
|
73
90
|
// Ritorna { killed, pid?, reason? }.
|
|
74
|
-
function killPidfile(p, signal = 'SIGTERM') {
|
|
91
|
+
function killPidfile(p, signal = 'SIGTERM', impl = {}) {
|
|
75
92
|
const meta = readPidfile(p);
|
|
76
93
|
if (!meta) return { killed: false, reason: 'no pidfile' };
|
|
77
|
-
|
|
94
|
+
const killImpl = impl.killImpl || process.kill;
|
|
95
|
+
const ownership = pidOwnership(meta.pid, killImpl);
|
|
96
|
+
if (ownership === 'missing' || ownership === 'unknown') {
|
|
78
97
|
removePidfile(p);
|
|
79
98
|
return { killed: false, reason: 'stale (pid dead)' };
|
|
80
99
|
}
|
|
100
|
+
if (ownership === 'foreign') {
|
|
101
|
+
// Never send a real signal after an EPERM ownership probe. The pidfile is
|
|
102
|
+
// ours; the process is not.
|
|
103
|
+
removePidfile(p);
|
|
104
|
+
return { killed: false, reason: 'stale (pid not owned)' };
|
|
105
|
+
}
|
|
81
106
|
if (meta.cmd) {
|
|
82
|
-
const live = readCmdline(meta.pid);
|
|
107
|
+
const live = (impl.readCmdlineImpl || readCmdline)(meta.pid);
|
|
83
108
|
if (live && !cmdMatches(meta.cmd, live)) {
|
|
84
109
|
// PID reuse: processo diverso. Non killare. Rimuovi pidfile stale.
|
|
85
110
|
removePidfile(p);
|
|
@@ -87,7 +112,7 @@ function killPidfile(p, signal = 'SIGTERM') {
|
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
114
|
try {
|
|
90
|
-
|
|
115
|
+
killImpl(meta.pid, signal);
|
|
91
116
|
removePidfile(p);
|
|
92
117
|
return { killed: true, pid: meta.pid };
|
|
93
118
|
} catch (e) {
|
|
@@ -97,5 +122,5 @@ function killPidfile(p, signal = 'SIGTERM') {
|
|
|
97
122
|
|
|
98
123
|
module.exports = {
|
|
99
124
|
defaultPidfilePath, readPidfile, writePidfile, removePidfile,
|
|
100
|
-
pidExists, readCmdline, isAlive, cleanStale, killPidfile,
|
|
125
|
+
pidOwnership, pidExists, readCmdline, isAlive, cleanStale, killPidfile,
|
|
101
126
|
};
|
package/lib/fleet/launch.js
CHANGED
|
@@ -36,6 +36,20 @@ function minimalEnv() {
|
|
|
36
36
|
return minimalRuntimeEnv(process.env, { home: os.homedir() });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// tmux reports client-side connection failures through localized stderr. The
|
|
40
|
+
// migration classifier must see the stable POSIX wording on every platform,
|
|
41
|
+
// otherwise a missing first-boot socket could disable Fleet under a non-English
|
|
42
|
+
// locale. Restrict only the message locale: LC_ALL=C would also force ASCII
|
|
43
|
+
// character handling and tmux would sanitize the tab in our format string.
|
|
44
|
+
// Panes, the shared server and the inventory protocol keep a UTF-8 LC_CTYPE.
|
|
45
|
+
function tmuxInventoryEnv() {
|
|
46
|
+
const env = minimalEnv();
|
|
47
|
+
delete env.LC_ALL; // LC_ALL would override LC_MESSAGES and LC_CTYPE.
|
|
48
|
+
env.LANGUAGE = 'C';
|
|
49
|
+
env.LC_MESSAGES = 'C';
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
// httpError(status, msg, data?, cause?) — structured HTTP error. `data` carries
|
|
40
54
|
// arbitrary API detail for the response body; `cause` (T4) is the OPTIONAL
|
|
41
55
|
// bounded failure triple {phase, code} of the up() boundary that failed. The
|
|
@@ -170,10 +184,10 @@ async function migrateLegacyTmuxSessions(tmuxBin, defs, { readonly = false } = {
|
|
|
170
184
|
};
|
|
171
185
|
}
|
|
172
186
|
const listing = await tmuxExec(tmuxBin,
|
|
173
|
-
['list-sessions', '-F', '#{session_id}\t#{session_name}'], { env:
|
|
187
|
+
['list-sessions', '-F', '#{session_id}\t#{session_name}'], { env: tmuxInventoryEnv() });
|
|
174
188
|
if (listing.err) {
|
|
175
189
|
const detail = boundedTmuxFailure(listing);
|
|
176
|
-
if (/no server running|failed to connect|connection refused|no such file.*tmux/i.test(detail)) {
|
|
190
|
+
if (/no server running|failed to connect|connection refused|no such file.*tmux|error connecting to .*\(no such file or directory\)/i.test(detail)) {
|
|
177
191
|
return { migrated: [], reason: 'no-tmux-server', needsPersistence: legacyMap.size > 0 };
|
|
178
192
|
}
|
|
179
193
|
throw tmuxMigrationError('TMUX_MIGRATION_LIST_FAILED',
|
package/lib/mcp/server.js
CHANGED
|
@@ -25,6 +25,7 @@ const { loadConfig } = require('../config.js');
|
|
|
25
25
|
const { readTokenSafe } = require('../auth/token.js');
|
|
26
26
|
const { isValidSession } = require('../files/store.js');
|
|
27
27
|
const VERSION = require('../../package.json').version;
|
|
28
|
+
const MCP_COMPANIONS = require('../../mcp-companions.json');
|
|
28
29
|
const { TOOLS, IDENTITY_CODE, IDENTITY_REMEDIATION } = require('./tools.js');
|
|
29
30
|
const cells = require('./cells.js');
|
|
30
31
|
|
|
@@ -38,6 +39,17 @@ const INVALID_REQUEST = -32600;
|
|
|
38
39
|
const METHOD_NOT_FOUND = -32601;
|
|
39
40
|
const INVALID_PARAMS = -32602;
|
|
40
41
|
|
|
42
|
+
function companionInstructions() {
|
|
43
|
+
const catalog = MCP_COMPANIONS.companions
|
|
44
|
+
.map((item) => `${item.id}: ${item.name} (${item.repository})`)
|
|
45
|
+
.join('; ');
|
|
46
|
+
return 'Discover the current client tools before recommending another MCP server. '
|
|
47
|
+
+ 'If a requested capability is missing, these optional NexusCrew companions may cover it: '
|
|
48
|
+
+ `${catalog}. Recommend only the capability actually needed and ask before installing `
|
|
49
|
+
+ 'software, changing MCP configuration, starting services or requesting credentials. '
|
|
50
|
+
+ 'NexusCrew does not install or configure companions automatically.';
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
// --- identita' cella mittente ------------------------------------------------
|
|
42
54
|
// Ordine (design §1, INVARIATO): $TMUX presente -> `tmux display-message -p '#S'`
|
|
43
55
|
// (nome sessione reale); se fallisce/invalida -> fallback env NEXUSCREW_MCP_SESSION;
|
|
@@ -246,6 +258,7 @@ function createMcpServer(opts = {}) {
|
|
|
246
258
|
protocolVersion: pv,
|
|
247
259
|
capabilities: { tools: {} },
|
|
248
260
|
serverInfo: { name: 'nexuscrew', version: VERSION },
|
|
261
|
+
instructions: companionInstructions(),
|
|
249
262
|
});
|
|
250
263
|
}
|
|
251
264
|
if (method === 'ping') return reply(id, {});
|
package/lib/nodes/tunnel.js
CHANGED
|
@@ -130,7 +130,12 @@ function reconcileTunnelSupervisors({ home = os.homedir(), configuredNames = [],
|
|
|
130
130
|
.filter((name) => store.NODE_NAME_RE.test(String(name || ''))));
|
|
131
131
|
const stopOne = typeof stopImpl === 'function' ? stopImpl : stopTunnel;
|
|
132
132
|
const result = { kept: [], stopped: [], cleaned: [], failed: [] };
|
|
133
|
-
const safeAbsent = new Set([
|
|
133
|
+
const safeAbsent = new Set([
|
|
134
|
+
'no pidfile',
|
|
135
|
+
'stale (pid dead)',
|
|
136
|
+
'stale (pid not owned)',
|
|
137
|
+
'pid reuse (cmd mismatch)',
|
|
138
|
+
]);
|
|
134
139
|
for (const name of tunnelPidNames(home)) {
|
|
135
140
|
if (keep.has(name)) { result.kept.push(name); continue; }
|
|
136
141
|
try {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "nexuscrew-mcp-companions",
|
|
4
|
+
"policy": {
|
|
5
|
+
"optional": true,
|
|
6
|
+
"discoverToolsFirst": true,
|
|
7
|
+
"recommendOnlyForRequestedCapability": true,
|
|
8
|
+
"automaticInstall": false,
|
|
9
|
+
"automaticConfiguration": false
|
|
10
|
+
},
|
|
11
|
+
"companions": [
|
|
12
|
+
{
|
|
13
|
+
"id": "memory",
|
|
14
|
+
"name": "mcp-memory-rs",
|
|
15
|
+
"repository": "https://github.com/DioNanos/mcp-memory-rs",
|
|
16
|
+
"installation": "https://github.com/DioNanos/mcp-memory-rs#install",
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"bundledSkill": "skills/memory/SKILL.md",
|
|
19
|
+
"capabilities": [
|
|
20
|
+
"durable structured agent state",
|
|
21
|
+
"versioned categories",
|
|
22
|
+
"bounded append-only journals",
|
|
23
|
+
"full-text memory search"
|
|
24
|
+
],
|
|
25
|
+
"primaryTools": [
|
|
26
|
+
"memory_read",
|
|
27
|
+
"memory_write",
|
|
28
|
+
"memory_append",
|
|
29
|
+
"memory_search",
|
|
30
|
+
"memory_status"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "msa",
|
|
35
|
+
"name": "mcp-vl-msa-rs",
|
|
36
|
+
"repository": "https://github.com/DioNanos/mcp-vl-msa-rs",
|
|
37
|
+
"installation": "https://github.com/DioNanos/mcp-vl-msa-rs#install",
|
|
38
|
+
"license": "Apache-2.0",
|
|
39
|
+
"bundledSkill": "skills/vl-msa/SKILL.md",
|
|
40
|
+
"capabilities": [
|
|
41
|
+
"persistent document collections",
|
|
42
|
+
"BM25 and optional hybrid retrieval",
|
|
43
|
+
"full-source grounding",
|
|
44
|
+
"bounded multi-hop retrieval"
|
|
45
|
+
],
|
|
46
|
+
"primaryTools": [
|
|
47
|
+
"msa_index",
|
|
48
|
+
"msa_index_batch",
|
|
49
|
+
"msa_search",
|
|
50
|
+
"msa_fetch_doc",
|
|
51
|
+
"msa_remember",
|
|
52
|
+
"msa_forget",
|
|
53
|
+
"msa_interleave_round"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "crew",
|
|
58
|
+
"name": "mcp-crewd-rs",
|
|
59
|
+
"repository": "https://github.com/DioNanos/mcp-crewd-rs",
|
|
60
|
+
"installation": "https://github.com/DioNanos/mcp-crewd-rs#install",
|
|
61
|
+
"license": "Apache-2.0",
|
|
62
|
+
"bundledSkill": "skills/crew/SKILL.md",
|
|
63
|
+
"capabilities": [
|
|
64
|
+
"bounded AI worker spawning",
|
|
65
|
+
"task status and structured results",
|
|
66
|
+
"controlled inter-cell messaging"
|
|
67
|
+
],
|
|
68
|
+
"primaryTools": [
|
|
69
|
+
"cell_spawn",
|
|
70
|
+
"cell_send_task",
|
|
71
|
+
"cell_status",
|
|
72
|
+
"cell_result",
|
|
73
|
+
"cell_list",
|
|
74
|
+
"cell_send",
|
|
75
|
+
"cell_ask"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "mail",
|
|
80
|
+
"name": "mcp-email-rs",
|
|
81
|
+
"repository": "https://github.com/DioNanos/mcp-email-rs",
|
|
82
|
+
"installation": "https://github.com/DioNanos/mcp-email-rs#install",
|
|
83
|
+
"license": "Apache-2.0",
|
|
84
|
+
"bundledSkill": "skills/mail-assistant/SKILL.md",
|
|
85
|
+
"capabilities": [
|
|
86
|
+
"IMAP mailbox discovery",
|
|
87
|
+
"mail search and reading",
|
|
88
|
+
"attachments and folders",
|
|
89
|
+
"drafts and optional SMTP sending"
|
|
90
|
+
],
|
|
91
|
+
"primaryTools": [
|
|
92
|
+
"list_folders",
|
|
93
|
+
"list_emails",
|
|
94
|
+
"search_emails",
|
|
95
|
+
"get_email",
|
|
96
|
+
"download_attachment",
|
|
97
|
+
"create_draft",
|
|
98
|
+
"send_email",
|
|
99
|
+
"email_doctor"
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/nexuscrew",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.34",
|
|
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": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"frontend/dist/",
|
|
14
14
|
"frontend/index.html",
|
|
15
15
|
"CHANGELOG.md",
|
|
16
|
+
"MCP_COMPANIONS.md",
|
|
17
|
+
"mcp-companions.json",
|
|
16
18
|
"LICENSE",
|
|
17
19
|
"README.md",
|
|
18
20
|
"NOTICE"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crew
|
|
3
|
+
description: Use when spawning or coordinating bounded AI worker cells through a Crew MCP fabric, including cell discovery, idempotent spawn, status monitoring, structured results, follow-up tasks, cancellation and the inter-cell message bus. Covers safe worktree isolation, durable audit reports and user-language selection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Crew
|
|
7
|
+
|
|
8
|
+
Use Crew for bounded delegated work. A coordinator submits tasks to worker
|
|
9
|
+
cells through an MCP server; spawning is asynchronous and returns a thread ID.
|
|
10
|
+
|
|
11
|
+
## Select response language
|
|
12
|
+
|
|
13
|
+
Choose the language for user-facing explanations and summaries in this order:
|
|
14
|
+
|
|
15
|
+
1. the user's explicit language preference;
|
|
16
|
+
2. the language of the current request;
|
|
17
|
+
3. a reliable client or system locale;
|
|
18
|
+
4. English.
|
|
19
|
+
|
|
20
|
+
Keep cell names, engine/profile IDs, thread IDs, tool names, paths and quoted
|
|
21
|
+
worker output unchanged unless the user explicitly asks to translate them.
|
|
22
|
+
Tell a worker which output language is required when its deliverable is
|
|
23
|
+
user-facing.
|
|
24
|
+
|
|
25
|
+
## Discover before spawning
|
|
26
|
+
|
|
27
|
+
1. Use `cell_list` to inspect the cells and engines visible to the caller.
|
|
28
|
+
2. Select an engine/profile appropriate to the bounded task.
|
|
29
|
+
3. Verify that the worker's `cwd` contains the instructions and project context
|
|
30
|
+
it needs.
|
|
31
|
+
4. Do not assume an embedded worker inherits the coordinator's MCP servers.
|
|
32
|
+
Required servers must be configured explicitly in the Crew daemon and
|
|
33
|
+
verified inside a fresh worker.
|
|
34
|
+
|
|
35
|
+
If no Crew tools are exposed and this skill is packaged with NexusCrew, the
|
|
36
|
+
optional companion is documented in `../../MCP_COMPANIONS.md`. Explain the
|
|
37
|
+
missing capability and ask before installing, configuring or starting it.
|
|
38
|
+
|
|
39
|
+
## Spawn, monitor and collect
|
|
40
|
+
|
|
41
|
+
1. Call `cell_spawn` with a bounded task, explicit `cwd`, background mode and a
|
|
42
|
+
caller-stable `idempotency_key`.
|
|
43
|
+
2. Keep the returned `crewd_thread_id`; a replayed idempotency key must resolve
|
|
44
|
+
to the existing thread rather than duplicate work.
|
|
45
|
+
3. Monitor with `cell_status`. Use the host's loop or scheduler for waits rather
|
|
46
|
+
than a tight manual poll.
|
|
47
|
+
4. Treat `idle` as normal turn completion. Also handle `timeout`, `failed`,
|
|
48
|
+
`interrupted` and `failed_unknown`; do not wait for a fictional `finished`
|
|
49
|
+
state.
|
|
50
|
+
5. Read `cell_result` and inspect `exit_status`, `final_answer` and the bounded
|
|
51
|
+
event tail.
|
|
52
|
+
6. Verify the worker's files and tests yourself before accepting code changes.
|
|
53
|
+
|
|
54
|
+
A timeout may leave partial edits. Never report success from a thread state
|
|
55
|
+
alone.
|
|
56
|
+
|
|
57
|
+
## Isolate code work
|
|
58
|
+
|
|
59
|
+
- Do not let two mutating workers edit the same worktree concurrently.
|
|
60
|
+
- Give parallel code tasks separate git worktrees.
|
|
61
|
+
- Preserve unrelated user changes and review every worker diff.
|
|
62
|
+
- Use follow-up tasks only when the original thread and scope remain valid.
|
|
63
|
+
- Cancel only the exact thread the user authorized.
|
|
64
|
+
|
|
65
|
+
## Require durable audit evidence
|
|
66
|
+
|
|
67
|
+
For audits, reviews and release gates:
|
|
68
|
+
|
|
69
|
+
1. Choose an authorized absolute report path before spawning, preferably
|
|
70
|
+
outside the repository being audited.
|
|
71
|
+
2. Require scope, evidence, findings by severity, residual risks and a terminal
|
|
72
|
+
verdict in the report.
|
|
73
|
+
3. Require the worker callback to identify the report path and verdict.
|
|
74
|
+
4. Verify that the report is a non-symlink regular file, read it completely and
|
|
75
|
+
confirm its verdict matches the callback.
|
|
76
|
+
5. Recalculate important hashes and gates independently.
|
|
77
|
+
|
|
78
|
+
Chat output or `cell_result.final_answer` is not a substitute for the verified
|
|
79
|
+
report.
|
|
80
|
+
|
|
81
|
+
## Use the message bus deliberately
|
|
82
|
+
|
|
83
|
+
- `cell_send` is fire-and-forget.
|
|
84
|
+
- `cell_ask` plus `cell_await` is appropriate when a reply is required.
|
|
85
|
+
- `cell_send_task` starts a follow-up turn on an existing thread.
|
|
86
|
+
- `cell_inbox` reads queued bus messages.
|
|
87
|
+
|
|
88
|
+
Transport receipts do not prove task acceptance or completion. Report only
|
|
89
|
+
results that have been verified.
|