@mmmbuto/nexuscrew 0.8.45 → 0.8.47
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 +46 -0
- package/README.md +1 -1
- package/frontend/dist/assets/{index-DNFEGdog.js → index-Db2ivuxA.js} +28 -28
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +19 -2
- package/lib/fleet/cell-exec.js +126 -18
- package/lib/fleet/launch.js +80 -23
- package/lib/fleet/managed.js +53 -5
- package/lib/fleet/prompt-delivery.js +275 -0
- package/lib/fleet/runtime.js +49 -10
- package/package.json +1 -1
package/frontend/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-Db2ivuxA.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-BAq6N1Md.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.47"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -34,7 +34,7 @@ const {
|
|
|
34
34
|
resolveCwd, normalizeCwdRel, deriveCwdRel,
|
|
35
35
|
} = require('./definitions.js');
|
|
36
36
|
const {
|
|
37
|
-
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine,
|
|
37
|
+
publicCatalog, describeManaged, describeCatalogCredential, defaultShellEngine, defaultAgyEngine, defaultKimiEngine,
|
|
38
38
|
} = require('./managed.js');
|
|
39
39
|
const { validEnvKey } = require('./env-key.js');
|
|
40
40
|
const { setCredential, removeCredential } = require('./credentials.js');
|
|
@@ -107,6 +107,21 @@ function backfillAgyEngine(defsPath, defs, cfg = {}) {
|
|
|
107
107
|
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// Backfill dell'engine Kimi Code CLI nativo: installazioni esistenti ricevono
|
|
111
|
+
// kimi.native in modo idempotente e non distruttivo. Nessun platform gate: il
|
|
112
|
+
// CLI gira ovunque giri Node e su Termux il resolver applica gia' il workaround
|
|
113
|
+
// shebang. Idempotente (gia' presente -> skip), NON sovrascrive un id
|
|
114
|
+
// 'kimi.native' gia' scelto dall'utente per altro (collisione -> skip, store
|
|
115
|
+
// invariato), rispetta il cap MAX_ENGINES. Non tocca CELLE.
|
|
116
|
+
function backfillKimiEngine(defsPath, defs) {
|
|
117
|
+
if (!defs || defs.engines.some((engine) => engine.managed?.client === 'kimi')) return defs;
|
|
118
|
+
if (defs.engines.some((engine) => engine.id === 'kimi.native')) return defs;
|
|
119
|
+
if (defs.engines.length >= CAPS.MAX_ENGINES) return defs;
|
|
120
|
+
const draft = draftFrom(defs);
|
|
121
|
+
draft.engines.push(defaultKimiEngine());
|
|
122
|
+
try { return atomicWrite(defsPath, draft); } catch (_) { return defs; }
|
|
123
|
+
}
|
|
124
|
+
|
|
110
125
|
// Applica engine + modello + policy come un'unica transizione. Ogni engine ricorda
|
|
111
126
|
// il proprio ultimo modello E l'ultima policy; passando a un altro engine né
|
|
112
127
|
// l'uno né l'altra attraversano il confine. La policy e' PER-CELL PER-ENGINE:
|
|
@@ -275,6 +290,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
275
290
|
}
|
|
276
291
|
boot = backfillShellEngine(defsPath, boot);
|
|
277
292
|
boot = backfillAgyEngine(defsPath, boot, cfg);
|
|
293
|
+
boot = backfillKimiEngine(defsPath, boot);
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
// Adopt or create the shared server before exposing a mutable Fleet. Reapply
|
|
@@ -808,7 +824,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
808
824
|
rc: { type: 'boolean', required: false, default: false },
|
|
809
825
|
managed: {
|
|
810
826
|
type: 'object', requiredFor: 'managed',
|
|
811
|
-
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi', 'agy', 'shell'] },
|
|
827
|
+
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi', 'agy', 'kimi', 'shell'] },
|
|
812
828
|
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
813
829
|
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
814
830
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
@@ -873,6 +889,7 @@ module.exports = {
|
|
|
873
889
|
createBuiltinFleet,
|
|
874
890
|
backfillShellEngine,
|
|
875
891
|
backfillAgyEngine,
|
|
892
|
+
backfillKimiEngine,
|
|
876
893
|
resolveCellCwd,
|
|
877
894
|
composeLaunchArgv,
|
|
878
895
|
composeClientInvocation,
|
package/lib/fleet/cell-exec.js
CHANGED
|
@@ -65,12 +65,14 @@ function promptCharsOk(prompt) {
|
|
|
65
65
|
function validRestartPrompt(value) {
|
|
66
66
|
if (value === undefined) return true;
|
|
67
67
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
68
|
-
if (Object.keys(value).some((key) => !['tmuxBin', 'tmuxSession', 'prompt', 'readyMs'].includes(key))) return false;
|
|
68
|
+
if (Object.keys(value).some((key) => !['tmuxBin', 'tmuxSession', 'prompt', 'readyMs', 'client', 'readyWaitMs'].includes(key))) return false;
|
|
69
69
|
return typeof value.tmuxBin === 'string' && value.tmuxBin.length > 0 && value.tmuxBin.length <= 4096
|
|
70
70
|
&& !/[\0\r\n]/.test(value.tmuxBin)
|
|
71
71
|
&& typeof value.tmuxSession === 'string' && /^[\w.@%:+-]{1,128}$/.test(value.tmuxSession)
|
|
72
72
|
&& promptCharsOk(value.prompt)
|
|
73
|
-
&& (value.readyMs === undefined || validInteger(value.readyMs, 0, 30000))
|
|
73
|
+
&& (value.readyMs === undefined || validInteger(value.readyMs, 0, 30000))
|
|
74
|
+
&& (value.client === undefined || value.client === '' || value.client === 'kimi' || value.client === 'claude')
|
|
75
|
+
&& (value.readyWaitMs === undefined || validInteger(value.readyWaitMs, 0, 120000));
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
function validPayload(payload) {
|
|
@@ -138,24 +140,117 @@ function waitChild(child) {
|
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
// Delivery del bootstrap prompt per generazione (0.8.47, R2/R3).
|
|
144
|
+
//
|
|
145
|
+
// OWNER UNICO: per gli engine managed Kimi (client 'kimi'/'claude') il
|
|
146
|
+
// supervisore consegna il prompt per TUTTE le generazioni, gen0 compresa —
|
|
147
|
+
// il runtime non esegue MAI paste/Enter per questi engine (legge solo l'esito
|
|
148
|
+
// bounded via opzione di pane @nc_delivery). Cosi' non esiste doppio writer
|
|
149
|
+
// cross-generation: se gen0 muore durante l'attesa readiness, la sua delivery
|
|
150
|
+
// viene cancellata e attesa PRIMA dello spawn di gen1, che ricevera' al
|
|
151
|
+
// massimo una nuova consegna. Engine custom send-keys: contratto legacy
|
|
152
|
+
// invariato (gen0 = runtime.injectPrompt; qui solo gen>0, paste senza Enter).
|
|
153
|
+
//
|
|
154
|
+
// CANCELLAZIONE (R3): cancel() ferma il timer E il polling in volo tramite
|
|
155
|
+
// isCancelled valutato dentro deliverBootstrapPrompt (ogni poll, pre-paste,
|
|
156
|
+
// pre-Enter); il chiamante fa cancel() + await settled prima dello spawn
|
|
157
|
+
// della generazione successiva: zero paste/Enter da un task della generazione
|
|
158
|
+
// precedente.
|
|
159
|
+
//
|
|
160
|
+
// REPORT: esito pubblicato con set-option -p @nc_delivery '<state>[:notReady]'
|
|
161
|
+
// (solo enum chiusi; l'opzione muore col pane, nessuno state file). Best
|
|
162
|
+
// effort: un set-option fallito lascia up() in report-timeout (onesto), la
|
|
163
|
+
// consegna resta fatta.
|
|
164
|
+
// CANCELLAZIONE (R3+R8): cancel() ferma il timer E il polling in volo tramite
|
|
165
|
+
// isCancelled valutato dentro deliverBootstrapPrompt (ogni poll, pre-paste,
|
|
166
|
+
// pre-Enter, post-Enter); il chiamante fa cancel() + await settled prima dello
|
|
167
|
+
// spawn della generazione successiva. cancel() con timer ANCORA PENDENTE
|
|
168
|
+
// (child uscito prima di readyMs, caso early-exit) risolve settled SUBITO:
|
|
169
|
+
// mai deadlock (R8). settled risolve con l'esito della delivery: null se
|
|
170
|
+
// nessun paste incerto, {state} se la generazione e' terminata con un
|
|
171
|
+
// post-paste incerto (delivery-unknown / staged-not-submitted) — il main loop
|
|
172
|
+
// NON auto-restarta in quel caso (R9: byte potenzialmente residui nel PTY del
|
|
173
|
+
// pane riusato; fermo bounded + restart operatore).
|
|
174
|
+
function startGenerationPrompt(config, generation, childState, seams = {}) {
|
|
175
|
+
if (!config) return null;
|
|
176
|
+
const classified = config.client === 'kimi' || config.client === 'claude';
|
|
177
|
+
if (!classified && generation === 0) return null; // legacy: gen0 resta al runtime
|
|
144
178
|
const setTimer = seams.setTimeout || setTimeout;
|
|
145
179
|
const clearTimer = seams.clearTimeout || clearTimeout;
|
|
180
|
+
const runTmux = seams.tmuxExec || ((bin, args, opts = {}) => new Promise((resolve) => {
|
|
181
|
+
require('node:child_process').execFile(bin, args, { env: opts.env, timeout: opts.timeoutMs || 10000 },
|
|
182
|
+
(err, stdout, stderr) => resolve({ err, stdout: String(stdout || ''), stderr: String(stderr || ''), code: err ? (typeof err.code === 'number' ? err.code : 1) : 0 }));
|
|
183
|
+
}));
|
|
184
|
+
const paneTarget = process.env.TMUX_PANE || `=${config.tmuxSession}`;
|
|
185
|
+
const markDelivery = async (value) => {
|
|
186
|
+
if (!classified) return;
|
|
187
|
+
try { await runTmux(config.tmuxBin, ['set-option', '-p', '-t', paneTarget, '@nc_delivery', value], {}); }
|
|
188
|
+
catch (_) { /* best-effort: il report timeout di up() resta onesto */ }
|
|
189
|
+
};
|
|
190
|
+
let timer = null; let cancelled = false;
|
|
191
|
+
let settledDone = false; let settleResolve = null;
|
|
192
|
+
const settled = new Promise((resolve) => { settleResolve = resolve; });
|
|
193
|
+
const settle = (value) => {
|
|
194
|
+
if (settledDone) return;
|
|
195
|
+
settledDone = true;
|
|
196
|
+
settleResolve(value);
|
|
197
|
+
};
|
|
198
|
+
const isCancelled = () => cancelled || childState.exited === true;
|
|
146
199
|
timer = setTimer(async () => {
|
|
147
200
|
timer = null;
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
if (isCancelled()) { settle(null); return; }
|
|
202
|
+
const task = (async () => {
|
|
203
|
+
try {
|
|
204
|
+
if (classified) {
|
|
205
|
+
await markDelivery('');
|
|
206
|
+
const deliver = seams.deliverBootstrapPrompt
|
|
207
|
+
|| require('./prompt-delivery.js').deliverBootstrapPrompt;
|
|
208
|
+
const result = await deliver({
|
|
209
|
+
tmuxBin: config.tmuxBin,
|
|
210
|
+
session: config.tmuxSession,
|
|
211
|
+
prompt: config.prompt,
|
|
212
|
+
client: config.client,
|
|
213
|
+
paneTarget: process.env.TMUX_PANE || undefined,
|
|
214
|
+
readyWaitMs: config.readyWaitMs,
|
|
215
|
+
isCancelled,
|
|
216
|
+
});
|
|
217
|
+
const state = result && typeof result.state === 'string' ? result.state : '';
|
|
218
|
+
const uncertain = state === 'delivery-unknown' || state === 'staged-not-submitted';
|
|
219
|
+
if (isCancelled()) {
|
|
220
|
+
// R9: la generazione e' finita. Propago SOLO gli esiti post-paste
|
|
221
|
+
// incerti (residuo PTY possibile); cancelled pre-paste e' pulito.
|
|
222
|
+
return uncertain ? { state } : null;
|
|
223
|
+
}
|
|
224
|
+
if (state) {
|
|
225
|
+
const kind = state === 'skipped-not-ready' && result.notReady ? `:${result.notReady}` : '';
|
|
226
|
+
await markDelivery(`${state}${kind}`);
|
|
227
|
+
}
|
|
228
|
+
// R12: l'esito post-paste incerto va conservato ANCHE se il child
|
|
229
|
+
// era vivo al ritorno di deliver: i byte possono restare nel PTY del
|
|
230
|
+
// pane e sommarsi al bootstrap della generazione successiva. Al
|
|
231
|
+
// prossimo child exit il main ferma il supervisor (no auto-restart).
|
|
232
|
+
return uncertain ? { state } : null;
|
|
233
|
+
}
|
|
234
|
+
const inject = seams.injectPrompt || require('./launch.js').injectPrompt;
|
|
235
|
+
await inject(config.tmuxBin, config.tmuxSession, config.prompt, {
|
|
236
|
+
target: paneTarget,
|
|
237
|
+
readyMs: 0,
|
|
238
|
+
});
|
|
239
|
+
return null;
|
|
240
|
+
} catch (_) { return null; /* keepalive must not die because prompt reinjection failed */ }
|
|
241
|
+
})();
|
|
242
|
+
settle(await task);
|
|
156
243
|
}, config.readyMs ?? 400);
|
|
157
244
|
timer.unref?.();
|
|
158
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
settled,
|
|
247
|
+
// R8: timer ancora pendente -> settle IMMEDIATO (idempotente): il main
|
|
248
|
+
// loop non resta mai appeso su un child uscito prima di readyMs.
|
|
249
|
+
cancel() {
|
|
250
|
+
cancelled = true;
|
|
251
|
+
if (timer) { clearTimer(timer); timer = null; settle(null); }
|
|
252
|
+
},
|
|
253
|
+
};
|
|
159
254
|
}
|
|
160
255
|
|
|
161
256
|
async function main(argv = process.argv.slice(2), seams = {}) {
|
|
@@ -195,13 +290,26 @@ async function main(argv = process.argv.slice(2), seams = {}) {
|
|
|
195
290
|
for (;;) {
|
|
196
291
|
if (stopping) return 0;
|
|
197
292
|
const startedAt = now();
|
|
198
|
-
current = spawnImpl(payload.command, payload.args, { env: childEnv, stdio: 'inherit' });
|
|
199
293
|
const childState = { exited: false };
|
|
200
|
-
const
|
|
294
|
+
const promptCtl = startGenerationPrompt(payload.restartPrompt, generation, childState, seams);
|
|
295
|
+
current = spawnImpl(payload.command, payload.args, { env: childEnv, stdio: 'inherit' });
|
|
201
296
|
const result = await waitChild(current);
|
|
202
297
|
childState.exited = true;
|
|
203
|
-
prompt?.cancel();
|
|
204
298
|
current = null;
|
|
299
|
+
// R2/R3: la generazione e' finita. Cancella la delivery in volo e ATTESA
|
|
300
|
+
// del suo termine PRIMA di qualunque nuovo spawn. R9: se l'esito e' un
|
|
301
|
+
// post-paste incerto (delivery-unknown / staged-not-submitted) i byte del
|
|
302
|
+
// prompt possono essere residui nel PTY del pane riusato: NIENTE auto-
|
|
303
|
+
// restart — fermo bounded del supervisor, restart esplicito operatore.
|
|
304
|
+
if (promptCtl) {
|
|
305
|
+
promptCtl.cancel();
|
|
306
|
+
const promptOutcome = await promptCtl.settled;
|
|
307
|
+
if (promptOutcome && (promptOutcome.state === 'delivery-unknown'
|
|
308
|
+
|| promptOutcome.state === 'staged-not-submitted')) {
|
|
309
|
+
writeError('nexuscrew cell supervisor stopped: uncertain prompt delivery (operator restart required)\n');
|
|
310
|
+
return result.signal ? 128 : (result.code || 1);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
205
313
|
if (result.error) {
|
|
206
314
|
writeError(`${sanitizeSpawnError(result.error, payload.command)}\n`);
|
|
207
315
|
return 1;
|
|
@@ -246,5 +354,5 @@ if (require.main === module) {
|
|
|
246
354
|
|
|
247
355
|
module.exports = {
|
|
248
356
|
DEFAULT_SUPERVISE, parseArgs, validSupervise, validRestartPrompt, validPayload,
|
|
249
|
-
receivePayload, sanitizeSpawnError, normalizeSupervise, waitChild,
|
|
357
|
+
receivePayload, sanitizeSpawnError, normalizeSupervise, waitChild, startGenerationPrompt, main,
|
|
250
358
|
};
|
package/lib/fleet/launch.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
const fs = require('node:fs');
|
|
21
21
|
const os = require('node:os');
|
|
22
22
|
const path = require('node:path');
|
|
23
|
+
const crypto = require('node:crypto');
|
|
23
24
|
const { execFile } = require('node:child_process');
|
|
24
25
|
const { minimalRuntimeEnv } = require('../runtime/env.js');
|
|
25
26
|
const { codeOf, phaseOf } = require('./causes.js');
|
|
@@ -355,34 +356,88 @@ async function waitStablePane(tmuxBin, target, { env, readyMs }) {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
359
|
+
// Risolve il pane id (%N) esatto della sessione, o verifica un %N gia' noto.
|
|
360
|
+
// Contratto submitToSession (R5): output session_name + pane_dead + pane_id,
|
|
361
|
+
// parsing ESATTO per campi (mai regex %N libera), session verificata uguale
|
|
362
|
+
// a quella attesa prima di qualunque paste/Enter. Ritorna null se il pane non
|
|
363
|
+
// e' risolvibile, morto, di un'altra sessione o diverso dal %N atteso.
|
|
364
|
+
async function resolveSessionPane(tmuxBin, session, { env, exec, target } = {}) {
|
|
365
|
+
const run = exec || tmuxExec;
|
|
366
|
+
const to = target || `=${session}`;
|
|
367
|
+
const r = await run(tmuxBin,
|
|
368
|
+
['display-message', '-p', '-t', to, '#{session_name}\t#{pane_dead}\t#{pane_id}'],
|
|
369
|
+
{ env, timeoutMs: 2000 });
|
|
370
|
+
if (r.err) return null;
|
|
371
|
+
const fields = r.stdout.trim().split('\t');
|
|
372
|
+
if (fields.length !== 3) return null;
|
|
373
|
+
const [sess, dead, pane] = fields;
|
|
374
|
+
if (dead !== '0') return null; // morto o non bounded
|
|
375
|
+
if (!/^%[0-9]+$/.test(pane)) return null;
|
|
376
|
+
if (typeof session === 'string' && session && sess !== session) return null;
|
|
377
|
+
if (target && target.startsWith('%') && pane !== target) return null;
|
|
378
|
+
return pane;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Stadio sicuro del testo nel composer del TUI (0.8.47): temp file wx/0600 con
|
|
382
|
+
// nome random + buffer tmux random per invio + bracketed paste sul %N esatto
|
|
383
|
+
// risolto subito prima. NESSUN Enter: la sottomissione e' una decisione
|
|
384
|
+
// separata del chiamante (solo deliverBootstrapPrompt, per kimi.native e
|
|
385
|
+
// claude.kimi-code). Niente buffer condiviso 'ncsend': due celle concorrenti
|
|
386
|
+
// non possono piu' sovrascriversi il buffer a vicenda.
|
|
387
|
+
// Ritorna { ok, stage, paneId?, reason } con reason costante (mai stderr/pane).
|
|
388
|
+
async function securePaste(tmuxBin, session, text, { env, exec, tmpdir, target, fsImpl } = {}) {
|
|
389
|
+
const run = exec || tmuxExec;
|
|
390
|
+
const fsx = fsImpl || fs;
|
|
391
|
+
if (!promptCharsOk(text)) {
|
|
392
|
+
return { ok: false, stage: 'validate', reason: 'prompt contiene byte di controllo (rifiutato)' };
|
|
393
|
+
}
|
|
394
|
+
const paneId = await resolveSessionPane(tmuxBin, session, { env, exec: run, target });
|
|
395
|
+
if (!paneId) return { ok: false, stage: 'resolve', reason: 'pane non risolvibile' };
|
|
396
|
+
const nonce = crypto.randomBytes(8).toString('hex');
|
|
397
|
+
const buffer = `ncstage-${nonce}`;
|
|
398
|
+
const tmp = path.join(tmpdir || os.tmpdir(), `.ncstage-${process.pid}-${nonce}.txt`);
|
|
399
|
+
let loaded = false;
|
|
400
|
+
try {
|
|
401
|
+
fsx.writeFileSync(tmp, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
402
|
+
try { fsx.chmodSync(tmp, 0o600); } catch (_) { /* best-effort */ }
|
|
403
|
+
const load = await run(tmuxBin, ['load-buffer', '-b', buffer, tmp], { env });
|
|
404
|
+
if (load.err) return { ok: false, stage: 'load', reason: 'buffer non disponibile' };
|
|
405
|
+
loaded = true;
|
|
406
|
+
// R4: da qui in poi il paste e' stato TENTATO: qualunque throw/rejection
|
|
407
|
+
// dell'executor e' post-paste (stage 'paste'), mai classificato come
|
|
408
|
+
// pre-paste 'load' — il composer potrebbe contenere testo parziale o
|
|
409
|
+
// completo e un retry duplicherebbe il prompt (G1).
|
|
410
|
+
try {
|
|
411
|
+
const paste = await run(tmuxBin, ['paste-buffer', '-p', '-t', paneId, '-b', buffer], { env });
|
|
412
|
+
if (paste.err) return { ok: false, stage: 'paste', reason: 'paste-buffer failed' };
|
|
413
|
+
return { ok: true, stage: 'pasted', paneId };
|
|
414
|
+
} catch (_) {
|
|
415
|
+
return { ok: false, stage: 'paste', reason: 'paste-buffer failed' };
|
|
416
|
+
}
|
|
417
|
+
} catch (_) {
|
|
418
|
+
return { ok: false, stage: 'load', reason: 'buffer non disponibile' };
|
|
419
|
+
} finally {
|
|
420
|
+
try { fsx.unlinkSync(tmp); } catch (_) { /* best-effort */ }
|
|
421
|
+
if (loaded) { try { await run(tmuxBin, ['delete-buffer', '-b', buffer], { env }); } catch (_) { /* best-effort */ } }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Iniezione prompt send-keys via bracketed paste (contratto legacy per engine
|
|
426
|
+
// custom promptMode 'send-keys': paste SENZA Enter, invariato — 0.8.47 G4).
|
|
427
|
+
// Readiness best-effort: se la sessione non e' viva quando paste-iamo (command
|
|
428
|
+
// gia' uscito) NON digita (design §9e). Ritorna {injected, reason}.
|
|
429
|
+
// Trasporto backportato su securePaste: buffer random, temp wx/0600, pane %N.
|
|
362
430
|
async function injectPrompt(tmuxBin, session, prompt, { env, readyMs = 400, target, engine, cell } = {}) {
|
|
363
431
|
if (!promptCharsOk(prompt)) {
|
|
364
432
|
return { injected: false, reason: 'prompt contiene byte di controllo (rifiutato)' };
|
|
365
433
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const alive = await waitAlive(tmuxBin, session, { env, readyMs });
|
|
373
|
-
if (!alive) return { injected: false, reason: 'sessione non viva (command uscito?): nessuna digitazione' };
|
|
374
|
-
|
|
375
|
-
// Target esatto: pane id (%N) se disponibile, altrimenti '=sessione' (match
|
|
376
|
-
// esatto, mai prefix-match) — audit impl #5.
|
|
377
|
-
const to = target || `=${session}`;
|
|
378
|
-
await tmuxExec(tmuxBin, ['load-buffer', '-b', 'ncsend', tmp], { env });
|
|
379
|
-
const paste = await tmuxExec(tmuxBin, ['paste-buffer', '-p', '-t', to, '-b', 'ncsend'], { env });
|
|
380
|
-
if (paste.err) return { injected: false, reason: redactSecrets(`paste-buffer failed: ${paste.stderr.trim()}`, engine, cell) };
|
|
381
|
-
return { injected: true, reason: 'bracketed paste (load-buffer + paste-buffer -p)' };
|
|
382
|
-
} finally {
|
|
383
|
-
try { if (tmp) fs.unlinkSync(tmp); } catch (_) { /* best-effort */ }
|
|
384
|
-
try { await tmuxExec(tmuxBin, ['delete-buffer', '-b', 'ncsend'], { env }); } catch (_) { /* best-effort */ }
|
|
434
|
+
const alive = await waitAlive(tmuxBin, session, { env, readyMs });
|
|
435
|
+
if (!alive) return { injected: false, reason: 'sessione non viva (command uscito?): nessuna digitazione' };
|
|
436
|
+
const stage = await securePaste(tmuxBin, session, prompt, { env, target });
|
|
437
|
+
if (!stage.ok) {
|
|
438
|
+
return { injected: false, reason: redactSecrets(stage.reason, engine, cell) };
|
|
385
439
|
}
|
|
440
|
+
return { injected: true, reason: 'bracketed paste (load-buffer + paste-buffer -p)' };
|
|
386
441
|
}
|
|
387
442
|
|
|
388
443
|
module.exports = {
|
|
@@ -400,6 +455,8 @@ module.exports = {
|
|
|
400
455
|
waitAlive,
|
|
401
456
|
waitStablePane,
|
|
402
457
|
injectPrompt,
|
|
458
|
+
resolveSessionPane,
|
|
459
|
+
securePaste,
|
|
403
460
|
redactSecrets,
|
|
404
461
|
sanitizeEarlyDiagnostic,
|
|
405
462
|
};
|
package/lib/fleet/managed.js
CHANGED
|
@@ -65,7 +65,7 @@ const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model'
|
|
|
65
65
|
// resolution order (runtime -> store -> shell -> key files -> legacy) so a
|
|
66
66
|
// pre-WP1 fleet.json migrates no-op: no existing cell changes resolution.
|
|
67
67
|
const CREDENTIAL_SOURCES = Object.freeze(['environment', 'nexuscrew-store', 'auto']);
|
|
68
|
-
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', agy: 'Agy', shell: 'Shell' });
|
|
68
|
+
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi', agy: 'Agy', kimi: 'Kimi Code CLI', shell: 'Shell' });
|
|
69
69
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
70
70
|
|
|
71
71
|
function validBaseUrl(value) {
|
|
@@ -142,6 +142,16 @@ const CATALOG = Object.freeze([
|
|
|
142
142
|
// (core) per la UI; describeManaged lo dichiara non configurato altrove.
|
|
143
143
|
// Termux/Windows restano fuori dal primary: l'utente usa agy via shell.local.
|
|
144
144
|
{ id: 'agy.native', client: 'agy', provider: 'native', label: 'Agy', auth: 'login', protocol: 'agy_native', core: true },
|
|
145
|
+
|
|
146
|
+
// Kimi Code CLI nativo (@moonshot-ai/kimi-code): client gestito con auth
|
|
147
|
+
// delegata al login del CLI (device-code flow, provider in config.toml).
|
|
148
|
+
// NexusCrew non legge ne' copia credenziali: nessun env provider, nessun
|
|
149
|
+
// token su argv. Distinto dal provider claude.kimi-code (adattatore Claude
|
|
150
|
+
// Code sull'endpoint Kimi), che resta il percorso K3 gestito via ANTHROPIC_*.
|
|
151
|
+
// Non e' un default seed: backfill idempotente in builtin.js, come Agy ma
|
|
152
|
+
// senza platform gate (il CLI gira ovunque giri Node; su Termux il resolver
|
|
153
|
+
// applica gia' il workaround shebang needsExplicitNode).
|
|
154
|
+
{ id: 'kimi.native', client: 'kimi', provider: 'native', label: 'Kimi account (CLI login)', auth: 'login', protocol: 'kimi_native', core: true, notice: 'kimi-native' },
|
|
145
155
|
]);
|
|
146
156
|
|
|
147
157
|
function profileFor(client, provider, credentialProfile) {
|
|
@@ -234,6 +244,18 @@ function defaultAgyEngine() {
|
|
|
234
244
|
};
|
|
235
245
|
}
|
|
236
246
|
|
|
247
|
+
// Engine Kimi Code CLI per il backfill (builtin.js): standard di default, auth
|
|
248
|
+
// delegata al login nativo del CLI, niente remote-control. Non e' un default seed.
|
|
249
|
+
function defaultKimiEngine() {
|
|
250
|
+
const profile = CATALOG.find((entry) => entry.id === 'kimi.native');
|
|
251
|
+
return {
|
|
252
|
+
id: profile.id,
|
|
253
|
+
label: CLIENT_LABELS.kimi,
|
|
254
|
+
rc: false,
|
|
255
|
+
managed: { client: 'kimi', provider: 'native', model: '', permissionPolicy: 'standard' },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
237
259
|
function parseAssignments(raw) {
|
|
238
260
|
const out = {};
|
|
239
261
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -753,6 +775,11 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
753
775
|
if (effectivePolicy === 'unsafe') {
|
|
754
776
|
if (spec.client === 'claude' || spec.client === 'agy') args.push('--dangerously-skip-permissions');
|
|
755
777
|
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
778
|
+
// Kimi Code CLI: unsafe mappa su --yolo (auto-approva le chiamate tool
|
|
779
|
+
// ordinarie ma l'agente puo' ancora fare domande). --auto (fully
|
|
780
|
+
// autonomous, nessuna domanda) NON e' mappato: il contratto NexusCrew
|
|
781
|
+
// distingue solo standard/unsafe e il default resta interattivo.
|
|
782
|
+
if (spec.client === 'kimi') args.push('--yolo');
|
|
756
783
|
}
|
|
757
784
|
let shellOneShot = false;
|
|
758
785
|
if (spec.client === 'shell') {
|
|
@@ -884,8 +911,25 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
884
911
|
// dal push generico qui sotto). Senza prompt parte il TUI interattivo `agy`.
|
|
885
912
|
if (model) args.push('--model', model);
|
|
886
913
|
if (cell?.prompt) args.push('--prompt-interactive');
|
|
914
|
+
} else if (spec.client === 'kimi') {
|
|
915
|
+
// Kimi Code CLI nativo: auth e provider gestiti dal CLI (login device-code,
|
|
916
|
+
// config.toml): niente env provider, niente credenziali su argv. Il CLI non
|
|
917
|
+
// documenta un flag prompt interattivo (`kimi -p` e' non-interattivo, senza
|
|
918
|
+
// TUI): il prompt della cella NON va su argv ma viene iniettato via
|
|
919
|
+
// bracketed paste dopo la readiness (promptMode 'send-keys' qui sotto,
|
|
920
|
+
// reiniettato anche ai restart supervisionati). Senza argomenti parte il
|
|
921
|
+
// TUI interattivo nella cwd della cella.
|
|
922
|
+
if (model) args.push('--model', model);
|
|
887
923
|
}
|
|
888
|
-
|
|
924
|
+
// Prompt su argv (0.8.47): SOLO i client che non hanno un percorso classified
|
|
925
|
+
// delivery. kimi.native e claude.kimi-code usano promptMode 'send-keys' con
|
|
926
|
+
// deliverBootstrapPrompt (readiness classificata + at-most-once): il prompt
|
|
927
|
+
// NON deve mai comparire nel loro argv (visibile in ps / perso dietro
|
|
928
|
+
// consenso/onboarding). Gli altri managed conservano il contratto argv
|
|
929
|
+
// (finding separato, fuori da questa patch).
|
|
930
|
+
const promptViaDelivery = spec.client === 'kimi'
|
|
931
|
+
|| (spec.client === 'claude' && spec.provider === 'kimi-code');
|
|
932
|
+
if (spec.client !== 'shell' && !promptViaDelivery && cell?.prompt) args.push(cell.prompt);
|
|
889
933
|
// nexuscrew-store source: neutralize the profile's env set in the composed
|
|
890
934
|
// child env (unset, never empty), so the runtime cannot leak credentials that
|
|
891
935
|
// the local store is meant to own.
|
|
@@ -896,7 +940,8 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
896
940
|
args.unshift(info.binary);
|
|
897
941
|
}
|
|
898
942
|
return { ok: true, info, engine: {
|
|
899
|
-
...engine, command, args, env,
|
|
943
|
+
...engine, command, args, env,
|
|
944
|
+
promptMode: promptViaDelivery ? 'send-keys' : 'managed-argv', clientBinary: info.binary,
|
|
900
945
|
...(spec.client === 'shell' ? { shellOneShot } : {}),
|
|
901
946
|
} };
|
|
902
947
|
}
|
|
@@ -909,7 +954,10 @@ function publicCatalog() {
|
|
|
909
954
|
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
|
|
910
955
|
permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
|
|
911
956
|
rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
|
|
912
|
-
|
|
957
|
+
// 'login'/'none' non sono variabili d'ambiente: nessuna KEY section per gli
|
|
958
|
+
// engine che delegano l'auth al login del CLI (rappresentazione onesta).
|
|
959
|
+
credentialEnv: p.auth === 'dynamic' ? !!p.credentialEnv
|
|
960
|
+
: (p.auth !== 'login' && p.auth !== 'none' && ENV_KEY_RE.test(p.auth || '') ? p.auth : false),
|
|
913
961
|
defaultEnvKey: p.defaultEnvKey || '',
|
|
914
962
|
}));
|
|
915
963
|
}
|
|
@@ -918,7 +966,7 @@ module.exports = {
|
|
|
918
966
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
|
|
919
967
|
ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
|
|
920
968
|
CLIENT_LABELS, normalizeManagedSpec, profileFor,
|
|
921
|
-
defaultDefinitions, defaultShellEngine, defaultAgyEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
969
|
+
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
922
970
|
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
923
971
|
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential,
|
|
924
972
|
credentialEnvNeutralizeSet, applyStoreNeutralization,
|