paris-immersion 0.1.29 → 0.1.30
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/dist/commands/serve.js +29 -1
- package/dist/lib/orca-launcher.d.ts +16 -0
- package/dist/lib/orca-launcher.js +90 -0
- package/dist/lib/relay-agent.js +33 -2
- package/package.json +1 -1
package/dist/commands/serve.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { readCredentials } from '../lib/credentials.js';
|
|
7
7
|
import { ensureWorkspace } from './install.js';
|
|
8
8
|
import { startRelayAgent } from '../lib/relay-agent.js';
|
|
9
|
+
import { parseMissionAction, startMissionInOrca } from '../lib/orca-launcher.js';
|
|
9
10
|
import { renderTostudyCommand } from '../templates/tostudy-command.js';
|
|
10
11
|
import { renderHookScript } from '../templates/hook.js';
|
|
11
12
|
import { renderSettingsJson } from '../templates/settings.js';
|
|
@@ -332,7 +333,7 @@ export async function startServeServer(opts) {
|
|
|
332
333
|
wss.once('listening', () => resolve());
|
|
333
334
|
wss.once('error', reject);
|
|
334
335
|
});
|
|
335
|
-
wss.on('connection', (ws) => {
|
|
336
|
+
wss.on('connection', (ws, req) => {
|
|
336
337
|
// TCP_NODELAY: sem Nagle no socket local (caminho dev ws://localhost).
|
|
337
338
|
try {
|
|
338
339
|
ws._socket?.setNoDelay?.(true);
|
|
@@ -343,6 +344,33 @@ export async function startServeServer(opts) {
|
|
|
343
344
|
ws.send('READY\n');
|
|
344
345
|
return;
|
|
345
346
|
}
|
|
347
|
+
// Modo AÇÃO (?mode=action): conexão one-shot da War Room ("Iniciar missão"
|
|
348
|
+
// abre o Orca) — sem pty. Espera um único 'ACTION:<json>', responde
|
|
349
|
+
// 'ACTIONRESULT:<json>' e fecha. Espelho local do TYPE_ACTION do relay.
|
|
350
|
+
const connUrl = new URL(req?.url ?? '/', 'http://localhost');
|
|
351
|
+
if (connUrl.searchParams.get('mode') === 'action') {
|
|
352
|
+
ws.on('message', (m) => {
|
|
353
|
+
const s = m.toString();
|
|
354
|
+
if (!s.startsWith('ACTION:'))
|
|
355
|
+
return;
|
|
356
|
+
const action = parseMissionAction(s.slice('ACTION:'.length));
|
|
357
|
+
if (!action) {
|
|
358
|
+
ws.send('ACTIONRESULT:' + JSON.stringify({ ok: false, error: 'Ação inválida.' }));
|
|
359
|
+
ws.close(1000);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
void startMissionInOrca({ cwd: opts.cwd ?? process.cwd(), prompt: action.prompt, title: action.title }).then((result) => {
|
|
363
|
+
try {
|
|
364
|
+
ws.send('ACTIONRESULT:' + JSON.stringify(result));
|
|
365
|
+
ws.close(1000);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* viewer já foi */
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
346
374
|
let p;
|
|
347
375
|
try {
|
|
348
376
|
// Lazy require inside spawnCoachPty — keeps `paris serve --help` working
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface StartMissionResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function startMissionInOrca(opts: {
|
|
6
|
+
cwd: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
}): Promise<StartMissionResult>;
|
|
10
|
+
/** Payload da ação vinda do browser (War Room). */
|
|
11
|
+
export interface MissionAction {
|
|
12
|
+
action: 'start_mission';
|
|
13
|
+
prompt: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseMissionAction(raw: string): MissionAction | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execFileP = promisify(execFile);
|
|
7
|
+
/**
|
|
8
|
+
* "Iniciar missão" (2026-08-14): abre o Orca na máquina do aluno, já na pasta
|
|
9
|
+
* do workspace da imersão, com um terminal rodando `claude` com o prompt
|
|
10
|
+
* inicial da missão. Chamado pelo serve/relay-agent quando a War Room manda a
|
|
11
|
+
* ação `start_mission`.
|
|
12
|
+
*
|
|
13
|
+
* Usa a CLI do Orca (instalada pelo kit do onboarding em ~/.local/bin/orca):
|
|
14
|
+
* orca open → garante app + runtime de pé
|
|
15
|
+
* orca repo add <ws> → registra o workspace (idempotente; falha ignorada)
|
|
16
|
+
* orca terminal create ... → aba nova com o claude + prompt, em foco
|
|
17
|
+
*/
|
|
18
|
+
function findOrcaBin() {
|
|
19
|
+
const candidates = [
|
|
20
|
+
path.join(homedir(), '.local', 'bin', 'orca'),
|
|
21
|
+
'/usr/local/bin/orca',
|
|
22
|
+
'/opt/homebrew/bin/orca',
|
|
23
|
+
];
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
if (existsSync(c))
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export async function startMissionInOrca(opts) {
|
|
31
|
+
const orca = findOrcaBin();
|
|
32
|
+
if (!orca) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: 'Orca não encontrado nesta máquina. Rode o instalador da imersão de novo, ou abra o Orca manualmente na pasta da imersão.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (!opts.cwd || !existsSync(opts.cwd)) {
|
|
39
|
+
return { ok: false, error: 'Workspace da imersão não encontrado nesta máquina.' };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
// 1. App + runtime de pé (no-op se já rodando). `orca open` espera o runtime
|
|
43
|
+
// ficar alcançável — timeout generoso pro cold start do app.
|
|
44
|
+
await execFileP(orca, ['open'], { timeout: 45_000 });
|
|
45
|
+
// 2. Workspace registrado (idempotente na prática: "já existe" é sucesso).
|
|
46
|
+
await execFileP(orca, ['repo', 'add', opts.cwd, '--json'], { timeout: 15_000 }).catch(() => { });
|
|
47
|
+
// 3. Aba de terminal com o Claude já na missão, em foco. Aspas simples no
|
|
48
|
+
// prompt são escapadas pro shell da aba.
|
|
49
|
+
const promptQ = opts.prompt.replace(/'/g, `'\\''`);
|
|
50
|
+
const command = `claude --dangerously-skip-permissions '${promptQ}'`;
|
|
51
|
+
const args = [
|
|
52
|
+
'terminal',
|
|
53
|
+
'create',
|
|
54
|
+
'--worktree',
|
|
55
|
+
`path:${opts.cwd}`,
|
|
56
|
+
'--command',
|
|
57
|
+
command,
|
|
58
|
+
'--focus',
|
|
59
|
+
'--json',
|
|
60
|
+
];
|
|
61
|
+
if (opts.title)
|
|
62
|
+
args.push('--title', opts.title.slice(0, 60));
|
|
63
|
+
await execFileP(orca, args, { timeout: 30_000 });
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
error: `Não consegui abrir a missão no Orca: ${msg.slice(0, 200)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function parseMissionAction(raw) {
|
|
75
|
+
try {
|
|
76
|
+
const j = JSON.parse(raw);
|
|
77
|
+
if (j.action !== 'start_mission')
|
|
78
|
+
return null;
|
|
79
|
+
if (typeof j.prompt !== 'string' || j.prompt.length === 0 || j.prompt.length > 4000)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
action: 'start_mission',
|
|
83
|
+
prompt: j.prompt,
|
|
84
|
+
title: typeof j.title === 'string' ? j.title : undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/lib/relay-agent.js
CHANGED
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
11
|
import { WebSocket } from 'ws';
|
|
12
12
|
import { spawnCoachPty } from '../commands/serve.js';
|
|
13
|
+
import { parseMissionAction, startMissionInOrca } from './orca-launcher.js';
|
|
13
14
|
const TYPE_DATA = 0x00;
|
|
14
15
|
const TYPE_RESIZE = 0x01;
|
|
15
16
|
const TYPE_WORKSPACE = 0x02; // agent → viewer: caminho do workspace (pro handoff nativo)
|
|
17
|
+
const TYPE_ACTION = 0x03; // viewer → agent: ação one-shot JSON (ex.: start_mission → abre o Orca)
|
|
18
|
+
const TYPE_ACTION_RESULT = 0x04; // agent → viewer: resultado JSON da ação
|
|
16
19
|
/** plaintext = type(1) || body → frame = nonce(12) || AES-256-GCM(enc||tag). */
|
|
17
20
|
function seal(key, type, body) {
|
|
18
21
|
const nonce = crypto.randomBytes(12);
|
|
@@ -297,6 +300,11 @@ export async function startRelayAgent(opts) {
|
|
|
297
300
|
try {
|
|
298
301
|
const m = JSON.parse(data.toString());
|
|
299
302
|
if (m.t === 'viewer-joined') {
|
|
303
|
+
// Viewer de AÇÃO ("Iniciar missão" da War Room): não é o terminal do
|
|
304
|
+
// curso — NÃO spawna o pty; a ação chega num frame TYPE_ACTION logo
|
|
305
|
+
// em seguida e é respondida com TYPE_ACTION_RESULT.
|
|
306
|
+
if (m.mode === 'action')
|
|
307
|
+
return;
|
|
300
308
|
if (killTimer) {
|
|
301
309
|
clearTimeout(killTimer);
|
|
302
310
|
killTimer = undefined;
|
|
@@ -328,9 +336,32 @@ export async function startRelayAgent(opts) {
|
|
|
328
336
|
}
|
|
329
337
|
return;
|
|
330
338
|
}
|
|
331
|
-
// Frame E2E do viewer (stdin/resize) — decifra e aplica
|
|
339
|
+
// Frame E2E do viewer (stdin/resize/ação) — decifra e aplica.
|
|
332
340
|
const msg = open(key, data);
|
|
333
|
-
if (!msg
|
|
341
|
+
if (!msg)
|
|
342
|
+
return;
|
|
343
|
+
if (msg.type === TYPE_ACTION) {
|
|
344
|
+
// Ação one-shot independente do pty (o viewer de ação nem o spawna).
|
|
345
|
+
const action = parseMissionAction(msg.body.toString('utf8'));
|
|
346
|
+
const respond = (result) => {
|
|
347
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
348
|
+
try {
|
|
349
|
+
ws.send(seal(key, TYPE_ACTION_RESULT, Buffer.from(JSON.stringify(result), 'utf8')), { binary: true });
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
/* viewer já foi */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
if (!action) {
|
|
357
|
+
respond({ ok: false, error: 'Ação inválida.' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
log(`relay: ação start_mission (${action.title ?? 'sem título'})`);
|
|
361
|
+
void startMissionInOrca({ cwd: opts.cwd, prompt: action.prompt, title: action.title }).then(respond);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!pty)
|
|
334
365
|
return;
|
|
335
366
|
if (msg.type === TYPE_DATA) {
|
|
336
367
|
pty.write(msg.body.toString('utf8'));
|
package/package.json
CHANGED