@mmmbuto/nexuscrew 0.8.58 → 0.9.0

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 (49) hide show
  1. package/CHANGELOG.md +137 -2
  2. package/README.md +1 -0
  3. package/frontend/dist/assets/index-0vuhL1YP.css +32 -0
  4. package/frontend/dist/assets/index-zjL6kZ7J.js +93 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/dist/version.json +1 -1
  7. package/lib/audio/adapters.js +50 -6
  8. package/lib/cli/commands.js +40 -2
  9. package/lib/cli/doctor.js +95 -12
  10. package/lib/cli/init.js +25 -3
  11. package/lib/cli/path.js +43 -10
  12. package/lib/cli/pidfile.js +23 -2
  13. package/lib/config.js +15 -0
  14. package/lib/fleet/builtin.js +161 -19
  15. package/lib/fleet/catalogs/opencode-go.json +50 -4
  16. package/lib/fleet/cell-exec.js +87 -9
  17. package/lib/fleet/cell-lease-server.js +719 -0
  18. package/lib/fleet/cell-lease.js +112 -0
  19. package/lib/fleet/definitions.js +101 -7
  20. package/lib/fleet/launch-broker.js +115 -3
  21. package/lib/fleet/lease-client.js +191 -0
  22. package/lib/fleet/lease-routes.js +92 -0
  23. package/lib/fleet/lease-verifier.js +230 -0
  24. package/lib/fleet/managed.js +366 -48
  25. package/lib/fleet/prompt-delivery.js +50 -2
  26. package/lib/fleet/provider.js +1 -1
  27. package/lib/fleet/runtime.js +53 -6
  28. package/lib/live-host/bridge.js +369 -0
  29. package/lib/live-host/routes.js +184 -0
  30. package/lib/live-host/store.js +96 -0
  31. package/lib/mcp/tools.js +51 -0
  32. package/lib/nodes/commands.js +9 -2
  33. package/lib/nodes/store.js +14 -0
  34. package/lib/nodes/tunnel.js +4 -1
  35. package/lib/proxy/federation.js +106 -9
  36. package/lib/proxy/node-proxy.js +33 -0
  37. package/lib/proxy/panel-auth.js +307 -0
  38. package/lib/proxy/panel-proxy.js +305 -0
  39. package/lib/server.js +127 -4
  40. package/package.json +1 -1
  41. package/skills/alibaba-token-media/SKILL.md +19 -0
  42. package/skills/crew/SKILL.md +15 -0
  43. package/skills/fill-forms/SKILL.md +23 -0
  44. package/skills/mail-assistant/SKILL.md +15 -0
  45. package/skills/memory/SKILL.md +15 -0
  46. package/skills/nexuscrew-agent/SKILL.md +18 -0
  47. package/skills/vl-msa/SKILL.md +15 -0
  48. package/frontend/dist/assets/index-BEGNtmx2.js +0 -93
  49. package/frontend/dist/assets/index-CYi_lhCg.css +0 -32
package/lib/server.js CHANGED
@@ -23,7 +23,11 @@ const VERSION = require('../package.json').version;
23
23
  const { transcribe } = require('./voice/transcribe.js');
24
24
  const { selectProvider } = require('./fleet/provider.js');
25
25
  const { fleetRoutes } = require('./fleet/routes.js');
26
+ const { leaseRoutes } = require('./fleet/lease-routes.js');
26
27
  const { cellsRoutes } = require('./cells/routes.js');
28
+ const { liveHostRoutes } = require('./live-host/routes.js');
29
+ const { createLiveHostStore, liveHostPath } = require('./live-host/store.js');
30
+ const { createLiveBridge } = require('./live-host/bridge.js');
27
31
  const { fsRoutes } = require('./fs/routes.js');
28
32
  const nodesStore = require('./nodes/store.js');
29
33
  const nodesTunnel = require('./nodes/tunnel.js');
@@ -33,6 +37,8 @@ const { createReverseSlotListeners } = require('./nodes/reverse-slot-listeners.j
33
37
  const reverseRotation = require('./nodes/reverse-rotation.js');
34
38
  const topologyCache = require('./nodes/topology-cache.js');
35
39
  const { createNodeProxy, handleNodeUpgrade } = require('./proxy/node-proxy.js');
40
+ const { createPanelProxy, handlePanelUpgrade } = require('./proxy/panel-proxy.js');
41
+ const { createPanelAuth } = require('./proxy/panel-auth.js');
36
42
  const federation = require('./proxy/federation.js');
37
43
  const { audioRoutes } = require('./audio/routes.js');
38
44
  const { isConsent: isAudioConsent } = require('./audio/consent.js');
@@ -549,9 +555,81 @@ function createServer(opts = {}) {
549
555
  if (!reverseSlotListeners.respond(req, res)) res.status(404).json({ error: 'reverse slot non disponibile' });
550
556
  });
551
557
 
552
- // Tutte le /api dietro Bearer: sul loopback il gate vero è il tunnel,
558
+ // Pannello per-cella (D8): inoltra il traffico verso il `panelUrl` di UNA cella
559
+ // LOCALE. La destinazione non arriva mai dal chiamante — si risolve dallo stato
560
+ // della cella — e il token di NexusCrew non prosegue verso il pannello, che e'
561
+ // un servizio terzo e non un nodo. Local-only come /api/live-host: il pass-through
562
+ // /node/<name>/ NON deve portarci un peer qualsiasi (vedi LOCAL_ONLY_PREFIXES in
563
+ // proxy/node-proxy.js). L'attraversamento verso i nodi del proprietario passera'
564
+ // dalla via allowlistata della federazione, dietro il gate di proprieta'.
565
+ async function resolveCellPanel(cellId) {
566
+ const fleet = await fleetP;
567
+ if (!fleet || fleet.available !== true) return null; // fleet non interrogabile
568
+ const statusFn = typeof fleet.status === 'function' ? fleet.status : fleet.cellStatus;
569
+ if (typeof statusFn !== 'function') return null;
570
+ const st = await statusFn.call(fleet);
571
+ const cells = Array.isArray(st && st.cells) ? st.cells : [];
572
+ // La chiave e' `cell`, non `id`: e' cio' che cellStatus PRODUCE
573
+ // (fleet/runtime.js scrive `cell: c.id`, dove `id` e' il nome interno della
574
+ // definizione). Cercare `c.id` qui non trovava MAI nulla, quindi ogni cella
575
+ // con un pannello configurato rispondeva «pannello non disponibile» — e la
576
+ // UI mostrava una causa col nome sbagliato. Tutti gli altri consumatori di
577
+ // cellStatus usano gia' `c.cell` (cells/routes.js, live-host/bridge.js,
578
+ // live-host/routes.js): questo era l'unico fuori posto.
579
+ const cell = cells.find((c) => c && c.cell === cellId);
580
+ if (!cell) return undefined; // cella sconosciuta
581
+ return typeof cell.panelUrl === 'string' ? cell.panelUrl.trim() : '';
582
+ }
583
+ // Tutte le altre /api dietro Bearer: sul loopback il gate vero è il tunnel,
553
584
  // ma il token chiude anche altri processi locali della stessa macchina.
554
585
  const api = express.Router();
586
+ // L'INGRESSO al pannello sta PRIMA del requireToken generale: un <iframe> e'
587
+ // una navigazione del browser e non porta header, quindi l'autenticazione
588
+ // qui e' dedicata — Bearer per la PWA, ticket monouso + cookie di visione
589
+ // per l'iframe (proxy/panel-auth.js, misura 2026-08-15). Il cookie vale
590
+ // SOLO sul path della cella che lo ha emesso: non e' e non diventa
591
+ // un'autenticazione dell'origine.
592
+ const panelAuth = createPanelAuth({
593
+ verifyToken: (t) => verify(tokenHolder.value, t),
594
+ resolveCellPanel,
595
+ // Senza questo segreto il pannello non saprebbe distinguere la PWA
596
+ // dall'ultimo hop di una route federata — che entra qui col Bearer che il
597
+ // proxy si e' iniettato da se'. E' lo stesso segreto per-processo che
598
+ // firma gli hop di sotto: due valori diversi renderebbero ogni federata
599
+ // 'sospetta', cioe' chiuderebbero il pannello remoto invece di proteggerlo.
600
+ hopSecret: () => hopSecret,
601
+ log: (entry) => diagnostics.record('panel-auth', 'info', entry.outcome, {
602
+ cell: entry.cell, reason: entry.reason, state: entry.outcome,
603
+ }),
604
+ });
605
+ api.use('/panel', panelAuth.panelAuthMiddleware, createPanelProxy({
606
+ resolveCellPanel,
607
+ log: (entry) => diagnostics.record('panel-proxy', 'info', entry.outcome, {
608
+ cell: entry.cell, reason: entry.reason, state: entry.outcome,
609
+ }),
610
+ }));
611
+ // L'ingresso ai pannelli REMOTI ha la stessa malattia dell'iframe locale: la
612
+ // via federata /api/route/<nodi>/_/panel/... sta per finire sotto requireToken
613
+ // e una navigazione del browser non porta header. QUI il ticket non si può
614
+ // validare — è del nodo che lo ha emesso — quindi per le panel-resource
615
+ // federate il requireToken NON si applica: decide il nodo proprietario (gate
616
+ // panelAccess + panelAuth col ticket/cookie, e l'ultimo hop lo riconosce
617
+ // dalla prova di hop: di là il Bearer non apre più il pannello, altrimenti
618
+ // il contenuto uscirebbe per ogni peer con panelAccess senza che nessuno
619
+ // abbia mai preso un ticket). Resta della PWA autenticata l'EMISSIONE: il POST
620
+ // .../panel/<cella>/ticket transita di sotto, col Bearer, come ogni altra /api.
621
+ const panelFederatoRouter = federation.localRouter({
622
+ nodesPath, localPort: () => (server && server.address() ? server.address().port : cfg.port), localCredential: () => tokenHolder.value, readonly: proxyReadonly,
623
+ hopSecret: () => hopSecret,
624
+ });
625
+ api.use('/route', (req, res, next) => {
626
+ const i = req.url.indexOf('/_/');
627
+ if (i === -1) return next();
628
+ const resource = req.url.slice(i + 2);
629
+ if (!/^\/panel\/[A-Za-z0-9._-]{1,32}(?:\/.*)?$/.test(resource)) return next();
630
+ if (req.method === 'POST' && /^\/panel\/[A-Za-z0-9._-]{1,32}\/ticket\/?$/.test(resource)) return next();
631
+ panelFederatoRouter(req, res, next);
632
+ });
555
633
  api.use(requireToken(tokenStore));
556
634
  // Origine per lo scope celle: stessa prova di hop usata da audio e notify.
557
635
  // requireCell:false perche' qui interessa QUALE NODO parla, non quale cella
@@ -710,6 +788,10 @@ function createServer(opts = {}) {
710
788
  federatedRate: createSpeakRateLimiter(),
711
789
  }));
712
790
  api.use('/fleet', fleetRoutes(fleetP, { ...cfg, diagnostics }));
791
+ // Fetta 2b (D3): superficie child del lease Live via canale nativo del
792
+ // bridge (HTTP loopback + Bearer). La cella e' derivata dalla sessione; il
793
+ // proof firmato dal verifier per-installazione autorizza refresh/recovery.
794
+ api.use('/lease', leaseRoutes({ fleetP, readonly: proxyReadonly }));
713
795
  // Audio Share. L'identita' del nodo NON e' un campo di cfg: si legge dal node
714
796
  // store, la stessa fonte usata da /api/cells e /api/peers. Lo stato Fleet e'
715
797
  // asincrono e va atteso: leggerlo come se fosse sincrono lascerebbe la
@@ -776,6 +858,27 @@ function createServer(opts = {}) {
776
858
  })),
777
859
  readonly: proxyReadonly,
778
860
  }));
861
+ // Cella ospite Live (contratto rev6 §2): hostCell unico per nodo con CAS. Lo store
862
+ // vive accanto al token (stessa dir isolata nei test); le route sono local-only e
863
+ // il proxy nega /api/live-host via /node (federazione default-deny).
864
+ // Fetta 3: il ponte risolve il puntamento all'avvio di una Live (POST /bridge
865
+ // nello stesso gruppo, stessa auth, stesso local-only). Isolabile da cfg
866
+ // (rev5 MC0): spento non crea connessioni e la Live resta standard.
867
+ api.use('/live-host', liveHostRoutes({
868
+ fleetP,
869
+ store: createLiveHostStore({ filePath: liveHostPath(cfg) }),
870
+ readonly: proxyReadonly,
871
+ bridge: createLiveBridge({
872
+ cfg,
873
+ fleetP,
874
+ tokenGet: () => tokenHolder.value,
875
+ filesRoot: cfg.filesRoot,
876
+ log: opts.log || console.log,
877
+ }),
878
+ }));
879
+ // Pannello per-cella: montato con il suo ingresso dedicato (ticket+cookie)
880
+ // PRIMA del requireToken generale — vedi il blocco sopra, dove nascono
881
+ // `panelAuth` e `resolveCellPanel`.
779
882
  api.use('/vl-nodes', vlNodeApiRoutes({
780
883
  storePath: vlNodesPath, broker: vlNodeBroker, ownerId: vlOwnerId, readonly: proxyReadonly,
781
884
  }));
@@ -954,7 +1057,14 @@ function createServer(opts = {}) {
954
1057
  if (pathname.startsWith('/api/route/')) {
955
1058
  let u; try { u = new URL(req.url, 'http://127.0.0.1'); } catch (_) { return socket.destroy(); }
956
1059
  const given = bearerFrom(req) || u.searchParams.get('token') || '';
957
- if (!verify(tokenHolder.value, given)) return socket.destroy();
1060
+ // Le WebSocket di un pannello REMOTO partono dalla pagina nel frame e
1061
+ // portano solo il cookie di visione: il token qui non c'è e non deve
1062
+ // esserci. L'HUB non può validarle (il cookie è del nodo che lo ha
1063
+ // emesso): transita e decide il proprietario, gate panelAccess compreso
1064
+ // — stessa forma del bypass HTTP sulle panel-resource federate.
1065
+ const panelFederato = /^\/api\/route\/[^?#]*\/_\/panel\/[A-Za-z0-9._-]{1,32}(?:\/.*)?$/.test(pathname)
1066
+ && /(?:^|;\s*)npanel=/.test(String(req.headers.cookie || ''));
1067
+ if (!panelFederato && !verify(tokenHolder.value, given)) return socket.destroy();
958
1068
  federation.forwardUpgrade({ req, socket, head, nodesPath, localPort: runtimePort, localCredential: () => tokenHolder.value, ingress: null, readonly: proxyReadonly, activeSockets: proxySockets, hopSecret: () => hopSecret });
959
1069
  return;
960
1070
  }
@@ -964,6 +1074,19 @@ function createServer(opts = {}) {
964
1074
  federation.forwardUpgrade({ req, socket, head, nodesPath, localPort: runtimePort, localCredential: () => tokenHolder.value, ingress, readonly: proxyReadonly, activeSockets: proxySockets, hopSecret: () => hopSecret });
965
1075
  return;
966
1076
  }
1077
+ if (pathname.startsWith('/api/panel/')) {
1078
+ handlePanelUpgrade({
1079
+ req, socket, head, resolveCellPanel,
1080
+ verifyToken: (t) => verify(tokenHolder.value, t),
1081
+ // Il cookie di visione apre le WS del pannello (la pagina nel frame
1082
+ // non puo' mettere header); Bearer e ?token= restano validi come prima.
1083
+ authorize: panelAuth.authorizeUpgrade,
1084
+ log: (entry) => diagnostics.record('panel-proxy', 'info', entry.outcome, {
1085
+ cell: entry.cell, reason: entry.reason, state: entry.outcome,
1086
+ }),
1087
+ });
1088
+ return;
1089
+ }
967
1090
  if (pathname === '/node' || pathname.startsWith('/node/')) {
968
1091
  handleNodeUpgrade({
969
1092
  req, socket, head, resolveNode,
@@ -991,7 +1114,7 @@ function createServer(opts = {}) {
991
1114
  // async questo handler rischierebbe di perdere i frame che arrivano prima
992
1115
  // di bindWs. `/ws` attacca per NOME DI SESSIONE e senza questo gate ogni
993
1116
  // filtro sugli elenchi sarebbe decorativo — basterebbe indovinare
994
- // `cloud-Dev`.
1117
+ // `cloud-X`.
995
1118
  const wsScope = wsCellScope(req);
996
1119
  bindWs(ws, {
997
1120
  openAttach,
@@ -1024,7 +1147,7 @@ function createServer(opts = {}) {
1024
1147
  }
1025
1148
 
1026
1149
  function start(opts = {}) {
1027
- const { server, cfg } = createServer(opts);
1150
+ const { server, cfg } = createServer({ ...opts, cellLeaseEnabled: true });
1028
1151
  const log = opts.log || console.log;
1029
1152
  const requestedPort = cfg.port;
1030
1153
  const nodesPath = opts.nodesPath || nodesStore.defaultNodesPath(cfg.home || os.homedir());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.58",
3
+ "version": "0.9.0",
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": {
@@ -131,3 +131,22 @@ overwrite. An explicit `--output` must remain under the current user's home.
131
131
 
132
132
  Read [references/api-contract.md](references/api-contract.md) before changing
133
133
  payloads, limits, endpoints, or model handling.
134
+
135
+ ## Dependencies
136
+
137
+ **Bundled (installed with the package):** the CLI
138
+ `scripts/alibaba_token_media.py` ships with this skill and is
139
+ dependency-free — Python 3 standard library only (verified against the
140
+ script's imports; it needs a Unix-like environment because it uses `fcntl`,
141
+ so Windows is out of scope).
142
+
143
+ **External (you must provide):**
144
+
145
+ | Need | Install | Probe / failure mode |
146
+ |---|---|---|
147
+ | Python 3 | Debian/Ubuntu `apt install python3`, Fedora `dnf install python3`, macOS `brew install python`, Termux `pkg install python` | `python3 --version` fails → nothing in this skill runs |
148
+ | An Alibaba Cloud **Token Plan Personal** subscription that includes the media models | activate on the Alibaba Cloud console — no local install | API calls fail with an authorization error from the service |
149
+ | API key in the environment | `export ALIBABA_CODE_API_KEY=<your key>` in the shell/service that runs the script | the script refuses with `ALIBABA_CODE_API_KEY is not set in this process environment` — the failure names the exact variable; no network call is made without it |
150
+
151
+ The key is read only from the environment: never paste it into a prompt, a
152
+ script argument, or a file.
@@ -87,3 +87,18 @@ report.
87
87
 
88
88
  Transport receipts do not prove task acceptance or completion. Report only
89
89
  results that have been verified.
90
+
91
+ ## Dependencies
92
+
93
+ **Bundled:** nothing — this skill is documentation only.
94
+
95
+ **External (you must provide):**
96
+
97
+ | Need | Install | Probe / failure mode |
98
+ |---|---|---|
99
+ | A Crew MCP fabric (companion) | optional companion `mcp-crewd-rs`: <https://github.com/DioNanos/mcp-crewd-rs> — the repository's Install section is authoritative and may change; this skill deliberately does not duplicate its commands (**not verified here**) | if the companion is not registered, the client exposes no `cell_*` tools; ask the client for its tool list |
100
+ | Client MCP registration | register the companion's stdio command in the AI client's MCP config | same failure mode: absent tools, not a runtime error |
101
+ | A `crew` token issued by the fabric | `crew token issue` on the host that runs the fabric (see the companion's docs) | spawns are rejected as unauthorized — the rejection names the missing authorization |
102
+
103
+ Without the companion there is no worker delegation in that session: say so
104
+ and suggest the companion once instead of emulating cells with raw tmux.
@@ -152,3 +152,26 @@ user deliberately chooses `--allow-unused`.
152
152
  name.
153
153
  5. Show the final preview or document to the user before calling it ready.
154
154
  6. State clearly which fields remain empty, uncertain, unsigned or unsubmitted.
155
+
156
+ ## Dependencies
157
+
158
+ **Bundled (installed with the package):** the five scripts
159
+ (`inspect_pdf.py`, `fill_pdf.py`, `fill_docx.py`, `dump_docx.py`,
160
+ `prepare_signature.py`) and `requirements.txt` ship inside this skill
161
+ directory.
162
+
163
+ **External (you must provide):**
164
+
165
+ | Need | Install | Probe / failure mode |
166
+ |---|---|---|
167
+ | Python 3 | Debian/Ubuntu `apt install python3 python3-pip`, Fedora `dnf install python3 pip`, macOS `brew install python`, Termux `pkg install python` | `python3 --version` fails → nothing in this skill runs |
168
+ | PyMuPDF, Pillow, numpy, python-docx (exact pins in the bundled `requirements.txt`) | add those four packages to the Python environment that runs the scripts, taking the versions from the bundled `requirements.txt`; this skill deliberately does not spell out the command, because it must ask before touching an environment | probe with `python3 -c "import fitz, PIL, numpy, docx"` — a missing module fails by naming itself (`ModuleNotFoundError: No module named 'fitz'`); add that package, not "dependencies" in the abstract |
169
+
170
+ Termux note: Termux has no systemd and no Docker assumptions here, but the
171
+ native wheels for PyMuPDF/numpy on Termux are **not verified**: if `pip`
172
+ cannot build them, this skill is not usable on that device — report that
173
+ plainly instead of switching to an unverified PDF writer.
174
+
175
+ Every script keeps the blank source untouched and writes a new output file;
176
+ if a dependency is missing mid-flow, the scripts refuse rather than emit a
177
+ half-filled form.
@@ -69,3 +69,18 @@ client-native scheduler or loop mechanism instead of manual polling, avoid
69
69
  duplicate jobs, respect quiet hours, and remain silent on unchanged ticks when
70
70
  the host workflow supports silent monitoring. A recurring authorization does
71
71
  not automatically authorize sending or permanent deletion.
72
+
73
+ ## Dependencies
74
+
75
+ **Bundled:** nothing — this skill is documentation only.
76
+
77
+ **External (you must provide):**
78
+
79
+ | Need | Install | Probe / failure mode |
80
+ |---|---|---|
81
+ | A mail MCP connector already exposed by the current client (any Gmail/IMAP-capable MCP server; e.g. the optional companion `mcp-email-rs` <https://github.com/DioNanos/mcp-email-rs> — its Install section is authoritative, **not verified here**) | register the connector in the AI client's MCP config, following that connector's own instructions | if no mail tools are exposed, mailbox discovery returns nothing to call — ask the client for its tool list; do not assume provider folders or account layout |
82
+ | Mailbox credentials | provided by the connector's own auth flow | auth failures surface from the connector, not from this skill |
83
+
84
+ This skill never sends or deletes mail on its own initiative; a missing
85
+ connector means the task cannot start, which must be reported, not worked
86
+ around with direct IMAP scripting.
@@ -79,3 +79,18 @@ document store, not in a bounded log.
79
79
  If no Memory MCP tool is available and this skill is packaged with NexusCrew,
80
80
  the optional companion is documented in `../../MCP_COMPANIONS.md`. Explain the
81
81
  missing capability and ask before installing or configuring anything.
82
+
83
+ ## Dependencies
84
+
85
+ **Bundled:** nothing — this skill is documentation only.
86
+
87
+ **External (you must provide):**
88
+
89
+ | Need | Install | Probe / failure mode |
90
+ |---|---|---|
91
+ | A Memory MCP server (companion) | optional companion `mcp-memory-rs`: <https://github.com/DioNanos/mcp-memory-rs> — the repository's Install section is authoritative and may change; this skill deliberately does not duplicate its commands (**not verified here**) | if the companion is not registered, the client exposes no `memory_*` tools; ask the client for its tool list rather than guessing |
92
+ | Client MCP registration | register the companion's stdio command in the AI client's MCP config | same failure mode as above: absent tools, not a runtime error |
93
+
94
+ Without the companion this skill is not usable in that session: say so and
95
+ suggest the companion once (see the packaged `mcp-companions.json` catalog)
96
+ instead of approximating with file reads.
@@ -169,3 +169,21 @@ work. The setting is also applied to windows created later in that session.
169
169
  - **Assuming a node listens on the port you know** → NexusCrew selects a free port per installation, and a peer's remote port is not the port that node listens on locally. Read it from `nexuscrew status` on that node; a health check aimed at the wrong port reports a dead service that is perfectly alive.
170
170
  - **Writing a reply into a local inbox directory** → the inbox is per-installation and is not synchronised between nodes. Answering a remote caller by dropping a file in your own inbox reaches nobody; reply through the tool that addressed you.
171
171
  - **Treating a dead scroll gesture as a web-terminal bug** → the pane is in the alternate buffer. Check whether it predates the NexusCrew setting or opted out with `alternateScreen:true`; never send raw page keys to a TUI to work around it.
172
+
173
+ ## Dependencies
174
+
175
+ **Bundled (installed with the package):** the `nexuscrew` CLI, `lib/`, these
176
+ skills, and the `bin/nc-send` / `bin/nc-deliver` helpers arrive with
177
+ `npm install -g @mmmbuto/nexuscrew` (Node.js >= 18 required by `engines`).
178
+
179
+ **External (you must provide):**
180
+
181
+ | Need | Install | Probe / failure mode |
182
+ |---|---|---|
183
+ | Node.js >= 18 | Debian/Ubuntu `apt install nodejs` (nodesource for 18+), Fedora `dnf install nodejs`, macOS `brew install node`, Termux `pkg install nodejs-lts` | `node -v` prints >= 18; below that `npm install` refuses per `engines` |
184
+ | tmux | Debian/Ubuntu `apt install tmux`, Fedora `dnf install tmux`, macOS `brew install tmux`, Termux `pkg install tmux` | `nexuscrew doctor` reports tmux missing by name; `nc-send` exits 127 with `nc-send: tmux not found on PATH (set TMUX_BIN)` — the failure names itself |
185
+ | An AI client that can register the MCP server | register the stdio command `nexuscrew mcp` in the client's MCP config | if the `nc_*` tools are not exposed, no `nc_` tool exists in the session — see "MCP bridge" above |
186
+ | A running NexusCrew service (for most tools) | `nexuscrew serve` (foreground) or your platform service manager | tools fail to reach the bridge; Termux has no systemd — run `nexuscrew serve` inside a tmux session or your own keep-alive |
187
+
188
+ If tmux is missing, MCP inspection (`nc_status`, `nc_identity`) still works;
189
+ anything that targets a session (including the `nc-send` fallback) does not.
@@ -66,3 +66,18 @@ round. Avoid a single unbounded search.
66
66
  If no VL-MSA tool is available and this skill is packaged with NexusCrew, the
67
67
  optional companion is documented in `../../MCP_COMPANIONS.md`. Explain the
68
68
  missing capability and ask before installing or configuring anything.
69
+
70
+ ## Dependencies
71
+
72
+ **Bundled:** nothing — this skill is documentation only.
73
+
74
+ **External (you must provide):**
75
+
76
+ | Need | Install | Probe / failure mode |
77
+ |---|---|---|
78
+ | A VL-MSA MCP server (companion) | optional companion `mcp-vl-msa-rs`: <https://github.com/DioNanos/mcp-vl-msa-rs> — the repository's Install section is authoritative and may change; this skill deliberately does not duplicate its commands (**not verified here**) | if the companion is not registered, the client exposes no `msa_*` tools; ask the client for its tool list |
79
+ | Client MCP registration | register the companion's stdio command in the AI client's MCP config | same failure mode: absent tools, not a runtime error |
80
+
81
+ Without the companion, durable retrieval is simply unavailable in that
82
+ session: say so and suggest the companion once instead of reading the
83
+ server's on-disk collections directly (which this skill forbids).