paris-immersion 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +20 -1
- package/dist/commands/serve.d.ts +0 -9
- package/dist/commands/serve.js +27 -2
- package/dist/index.js +6 -2
- package/dist/lib/daemon.d.ts +15 -0
- package/dist/lib/daemon.js +43 -0
- package/dist/lib/relay-agent.js +44 -15
- package/dist/templates/hook.js +1 -1
- package/package.json +1 -1
|
@@ -4,3 +4,10 @@
|
|
|
4
4
|
* instalador chama `install`; o `paris logout` chama `uninstall`.
|
|
5
5
|
*/
|
|
6
6
|
export declare function daemonCommand(action: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* `paris restart` — religa o agente do terminal embutido. Sempre dá certo:
|
|
9
|
+
* libera a porta (mata serve preso) e reinstala/reinicia o serviço persistente,
|
|
10
|
+
* que sobe um serve NOVO com serve-token fresco e reconecta ao relay. É o comando
|
|
11
|
+
* que a UI do curso oferece quando o terminal não conecta sozinho.
|
|
12
|
+
*/
|
|
13
|
+
export declare function restartCommand(): Promise<void>;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installDaemon, uninstallDaemon, daemonStatus } from '../lib/daemon.js';
|
|
1
|
+
import { installDaemon, uninstallDaemon, daemonStatus, restartDaemon } from '../lib/daemon.js';
|
|
2
2
|
import { c } from '../lib/colors.js';
|
|
3
3
|
/**
|
|
4
4
|
* `paris daemon <install|uninstall|status>` — gerencia o serviço persistente
|
|
@@ -33,3 +33,22 @@ export async function daemonCommand(action) {
|
|
|
33
33
|
console.log(c.yellow("Uso: paris daemon <install|uninstall|status>"));
|
|
34
34
|
process.exitCode = 1;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* `paris restart` — religa o agente do terminal embutido. Sempre dá certo:
|
|
38
|
+
* libera a porta (mata serve preso) e reinstala/reinicia o serviço persistente,
|
|
39
|
+
* que sobe um serve NOVO com serve-token fresco e reconecta ao relay. É o comando
|
|
40
|
+
* que a UI do curso oferece quando o terminal não conecta sozinho.
|
|
41
|
+
*/
|
|
42
|
+
export async function restartCommand() {
|
|
43
|
+
try {
|
|
44
|
+
process.stdout.write(c.dim('Religando o agente do terminal embutido…\n'));
|
|
45
|
+
const { mechanism } = await restartDaemon();
|
|
46
|
+
console.log(c.green('✓ Agente reiniciado.') + c.dim(` (${mechanism})`));
|
|
47
|
+
console.log(c.dim(' Volte para a aba do curso — o terminal reconecta sozinho em alguns segundos.'));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.log(c.yellow(`! Não consegui reiniciar o serviço: ${err instanceof Error ? err.message : String(err)}`));
|
|
51
|
+
console.log(c.dim(' Tente: paris daemon install'));
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -37,15 +37,6 @@ export interface ServeHandle {
|
|
|
37
37
|
port: number;
|
|
38
38
|
close: () => void;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Descobre o slash-command do tutor pra injetar no kickoff. O `@tostudy-ai/cli`
|
|
42
|
-
* instala o comando com nome ESPECÍFICO do curso (`.claude/commands/tostudy-<slug>.md`),
|
|
43
|
-
* NÃO `/tostudy` — então cravar `/tostudy` dava "Unknown command: /tostudy" e o
|
|
44
|
-
* curso não iniciava. Aqui varremos `.claude/commands/tostudy*.md` e usamos o
|
|
45
|
-
* comando real; com vários cursos, casamos com o `slug` ativo do
|
|
46
|
-
* `.tostudy/workspace.json`. Override explícito via PARIS_COACH_KICKOFF.
|
|
47
|
-
* Fallback `/tostudy` (legado / antes do `tostudy` instalar o comando).
|
|
48
|
-
*/
|
|
49
40
|
export declare function resolveKickoff(cwd: string): string;
|
|
50
41
|
/**
|
|
51
42
|
* Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
|
package/dist/commands/serve.js
CHANGED
|
@@ -79,6 +79,21 @@ const DEFAULT_ALLOWED_ORIGINS = [
|
|
|
79
79
|
* `.tostudy/workspace.json`. Override explícito via PARIS_COACH_KICKOFF.
|
|
80
80
|
* Fallback `/tostudy` (legado / antes do `tostudy` instalar o comando).
|
|
81
81
|
*/
|
|
82
|
+
// Fallback ROBUSTO quando o slash-command `/tostudy-<slug>` não existe (o
|
|
83
|
+
// `@tostudy-ai/cli` não rodou `init` no workspace, ou rodou DEPOIS do Claude
|
|
84
|
+
// subir — slash-commands carregam no startup, então um comando criado depois dá
|
|
85
|
+
// "Unknown command: /tostudy"). Em vez de cravar `/tostudy` (que erra), passamos
|
|
86
|
+
// um prompt de tutor em TEXTO: o Claude conduz o curso direto pela tostudy CLI
|
|
87
|
+
// (select/next/progress), sem depender de nenhum comando instalado. Sem apóstrofo
|
|
88
|
+
// no texto (o kickoff vai dentro de aspas simples no shell).
|
|
89
|
+
const COURSE_TUTOR_PROMPT = 'Você é o tutor do curso de onboarding da imersão Paris Group, rodando no terminal do aluno. ' +
|
|
90
|
+
'Comece AGORA falando primeiro, em português — o aluno ainda não digitou nada. ' +
|
|
91
|
+
'Antes de ensinar, prepare o curso da ToStudy em silêncio: rode no bash "tostudy courses --json" para ver o curso matriculado; ' +
|
|
92
|
+
'rode "tostudy select <id-ou-número-do-curso>" para ativá-lo; e "tostudy progress --json" para ver onde o aluno parou. ' +
|
|
93
|
+
'Depois conduza lição por lição, no ritmo do aluno, usando: "tostudy next" (avançar), "tostudy lesson" e "tostudy theory" (conteúdo da lição), ' +
|
|
94
|
+
'"tostudy hint" (dica) e "tostudy progress" (status). ' +
|
|
95
|
+
'Se o tostudy não estiver disponível ou sem curso ativo, use as ferramentas MCP get_course e complete_lesson do paris como fallback. ' +
|
|
96
|
+
'Dê as boas-vindas e já comece a primeira lição.';
|
|
82
97
|
export function resolveKickoff(cwd) {
|
|
83
98
|
const override = process.env.PARIS_COACH_KICKOFF;
|
|
84
99
|
if (override)
|
|
@@ -102,7 +117,7 @@ export function resolveKickoff(cwd) {
|
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
if (names.size === 0)
|
|
105
|
-
return
|
|
120
|
+
return COURSE_TUTOR_PROMPT;
|
|
106
121
|
const all = [...names];
|
|
107
122
|
// Curso ATIVO deste workspace = fonte da verdade pra escolher entre vários cursos.
|
|
108
123
|
let slug = '';
|
|
@@ -126,7 +141,7 @@ export function resolveKickoff(cwd) {
|
|
|
126
141
|
const courseSpecific = all.filter((n) => /^tostudy-.+/i.test(n)).sort();
|
|
127
142
|
if (courseSpecific.length)
|
|
128
143
|
return '/' + courseSpecific[0];
|
|
129
|
-
return
|
|
144
|
+
return COURSE_TUTOR_PROMPT;
|
|
130
145
|
}
|
|
131
146
|
/**
|
|
132
147
|
* Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
|
|
@@ -337,6 +352,16 @@ export async function serveCommand(opts) {
|
|
|
337
352
|
}
|
|
338
353
|
catch (err) {
|
|
339
354
|
const msg = err instanceof Error ? err.message : String(err);
|
|
355
|
+
// EADDRINUSE não é erro: o serviço em background (daemon) já tem um `paris
|
|
356
|
+
// serve` segurando a porta. Não precisa rodar de novo — o terminal embutido
|
|
357
|
+
// conecta no que já está rodando. Se ele NÃO conectar, é melhor reiniciar
|
|
358
|
+
// (religa com serve-token fresco) do que falhar com vermelho assustador.
|
|
359
|
+
if (/EADDRINUSE/i.test(msg)) {
|
|
360
|
+
console.log(c.green('✓ paris já está rodando') + c.dim(` (porta ${portNum} em uso pelo serviço em background).`));
|
|
361
|
+
console.log(c.dim(' O terminal do curso conecta no agente que já está ativo.'));
|
|
362
|
+
console.log(c.dim(' Se o terminal não conectar, rode: ') + c.bold('paris restart'));
|
|
363
|
+
process.exit(0);
|
|
364
|
+
}
|
|
340
365
|
console.error(c.red(`Falha ao iniciar paris serve: ${msg}`));
|
|
341
366
|
process.exit(1);
|
|
342
367
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,11 +9,11 @@ import { reopenCommand } from './commands/reopen.js';
|
|
|
9
9
|
import { graduateCommand } from './commands/graduate.js';
|
|
10
10
|
import { logoutCommand } from './commands/logout.js';
|
|
11
11
|
import { serveCommand } from './commands/serve.js';
|
|
12
|
-
import { daemonCommand } from './commands/daemon.js';
|
|
12
|
+
import { daemonCommand, restartCommand } from './commands/daemon.js';
|
|
13
13
|
const program = new Command()
|
|
14
14
|
.name('paris')
|
|
15
15
|
.description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
|
|
16
|
-
.version('0.1.
|
|
16
|
+
.version('0.1.13');
|
|
17
17
|
program
|
|
18
18
|
.command('login')
|
|
19
19
|
.description('Autenticar com sua conta Paris (device-code OR --token direto)')
|
|
@@ -61,6 +61,10 @@ program
|
|
|
61
61
|
.command('daemon <action>')
|
|
62
62
|
.description('Serviço persistente do paris serve (install | uninstall | status)')
|
|
63
63
|
.action(daemonCommand);
|
|
64
|
+
program
|
|
65
|
+
.command('restart')
|
|
66
|
+
.description('Religa o agente do terminal embutido (libera a porta + reinicia o serviço). Use se o terminal não conectar.')
|
|
67
|
+
.action(restartCommand);
|
|
64
68
|
program.parseAsync().catch((err) => {
|
|
65
69
|
console.error('\n[paris] erro:', err?.message ?? err);
|
|
66
70
|
process.exit(1);
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -4,3 +4,18 @@ export declare function installDaemon(): Promise<{
|
|
|
4
4
|
/** Best-effort — nunca lança (usado no logout, não pode travar). */
|
|
5
5
|
export declare function uninstallDaemon(): Promise<void>;
|
|
6
6
|
export declare function daemonStatus(): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Mata, best-effort, qualquer processo escutando na porta do serve (default
|
|
9
|
+
* 31000). Cobre o caso de um `paris serve` "preso" (manual ou daemon zumbi)
|
|
10
|
+
* segurando a porta — origem do `EADDRINUSE` e do terminal que não conecta.
|
|
11
|
+
*/
|
|
12
|
+
export declare function killServePort(port?: number): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Reinicia o agente do terminal embutido de forma que SEMPRE dá certo: libera a
|
|
15
|
+
* porta (mata serve preso) e re-registra o serviço persistente (que sobe um serve
|
|
16
|
+
* NOVO — com serve-token fresco, reconectando ao relay). É o que o comando
|
|
17
|
+
* copiável da UI roda quando o terminal não conecta sozinho.
|
|
18
|
+
*/
|
|
19
|
+
export declare function restartDaemon(port?: number): Promise<{
|
|
20
|
+
mechanism: string;
|
|
21
|
+
}>;
|
package/dist/lib/daemon.js
CHANGED
|
@@ -244,3 +244,46 @@ export async function daemonStatus() {
|
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Mata, best-effort, qualquer processo escutando na porta do serve (default
|
|
249
|
+
* 31000). Cobre o caso de um `paris serve` "preso" (manual ou daemon zumbi)
|
|
250
|
+
* segurando a porta — origem do `EADDRINUSE` e do terminal que não conecta.
|
|
251
|
+
*/
|
|
252
|
+
export async function killServePort(port = 31000) {
|
|
253
|
+
try {
|
|
254
|
+
if (platform() === 'win32') {
|
|
255
|
+
await execFileAsync('powershell', [
|
|
256
|
+
'-NoProfile',
|
|
257
|
+
'-Command',
|
|
258
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ` +
|
|
259
|
+
`Select-Object -ExpandProperty OwningProcess -Unique | ` +
|
|
260
|
+
`ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }`,
|
|
261
|
+
]).catch(() => { });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// macOS / Linux: lsof → PIDs escutando na porta → kill.
|
|
265
|
+
const { stdout } = await execFileAsync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN']).catch(() => ({ stdout: '' }));
|
|
266
|
+
const pids = stdout
|
|
267
|
+
.split('\n')
|
|
268
|
+
.map((s) => s.trim())
|
|
269
|
+
.filter(Boolean);
|
|
270
|
+
for (const pid of pids) {
|
|
271
|
+
await execFileAsync('kill', ['-TERM', pid]).catch(() => { });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
/* best-effort */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Reinicia o agente do terminal embutido de forma que SEMPRE dá certo: libera a
|
|
280
|
+
* porta (mata serve preso) e re-registra o serviço persistente (que sobe um serve
|
|
281
|
+
* NOVO — com serve-token fresco, reconectando ao relay). É o que o comando
|
|
282
|
+
* copiável da UI roda quando o terminal não conecta sozinho.
|
|
283
|
+
*/
|
|
284
|
+
export async function restartDaemon(port = 31000) {
|
|
285
|
+
await killServePort(port);
|
|
286
|
+
// pequena folga pro SO liberar o socket antes do novo bind
|
|
287
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
288
|
+
return installDaemon();
|
|
289
|
+
}
|
package/dist/lib/relay-agent.js
CHANGED
|
@@ -45,23 +45,40 @@ export async function startRelayAgent(opts) {
|
|
|
45
45
|
if (!opts.apiUrl || !opts.loginToken)
|
|
46
46
|
return null;
|
|
47
47
|
const log = opts.log ?? (() => { });
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const fetchInfo = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${opts.apiUrl.replace(/\/$/, '')}/api/onboarding/v2/serve-token`, {
|
|
51
|
+
headers: { authorization: `Bearer ${opts.loginToken}` },
|
|
52
|
+
signal: AbortSignal.timeout(8000),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
return null;
|
|
56
|
+
return (await res.json());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
55
59
|
return null;
|
|
56
|
-
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// Primeira busca decide se o relay está configurado (senão → modo local-only).
|
|
63
|
+
// Retry só quando a API está inalcançável (fetchInfo → null): cobre o blip de
|
|
64
|
+
// rede no boot (o daemon sobe antes da rede) sem penalizar o dev local (a API
|
|
65
|
+
// responde 200 na 1ª tentativa). KeepAlive mantém o serve, mas sem este retry o
|
|
66
|
+
// agente relay nunca subia se a 1ª busca caísse no boot.
|
|
67
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
68
|
+
let initial = await fetchInfo();
|
|
69
|
+
for (let i = 0; i < 6 && !initial; i++) {
|
|
70
|
+
await sleep(5000);
|
|
71
|
+
initial = await fetchInfo();
|
|
57
72
|
}
|
|
58
|
-
|
|
73
|
+
if (!initial || !initial.relayUrl || !initial.token || !initial.e2eKeyB64)
|
|
59
74
|
return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
// Mutáveis: o serve-token é de vida curta e é RENOVADO a cada (re)conexão (o
|
|
76
|
+
// loginToken/JWT do aluno é estável). Sem isso, o daemon que sobe no boot fica
|
|
77
|
+
// preso com um token expirado, o relay rejeita o agente, e o terminal embutido
|
|
78
|
+
// fica "Esperando o paris CLI subir" pra sempre mesmo com o serve rodando.
|
|
79
|
+
let relayUrl = initial.relayUrl;
|
|
80
|
+
let token = initial.token;
|
|
81
|
+
let key = Buffer.from(initial.e2eKeyB64, 'base64'); // chave E2E é estável por participante
|
|
65
82
|
if (key.length !== 32)
|
|
66
83
|
return null;
|
|
67
84
|
let ws = null;
|
|
@@ -167,7 +184,19 @@ export async function startRelayAgent(opts) {
|
|
|
167
184
|
}
|
|
168
185
|
}, 80);
|
|
169
186
|
};
|
|
170
|
-
const connect = () => {
|
|
187
|
+
const connect = async () => {
|
|
188
|
+
if (closed)
|
|
189
|
+
return;
|
|
190
|
+
// Renova token + relayUrl + chave a cada tentativa (o serve-token expira;
|
|
191
|
+
// o loginToken é estável). É isto que conserta o "agente preso no boot".
|
|
192
|
+
const fresh = await fetchInfo();
|
|
193
|
+
if (fresh?.relayUrl && fresh.token && fresh.e2eKeyB64) {
|
|
194
|
+
relayUrl = fresh.relayUrl;
|
|
195
|
+
token = fresh.token;
|
|
196
|
+
const k = Buffer.from(fresh.e2eKeyB64, 'base64');
|
|
197
|
+
if (k.length === 32)
|
|
198
|
+
key = k;
|
|
199
|
+
}
|
|
171
200
|
if (closed)
|
|
172
201
|
return;
|
|
173
202
|
const url = `${relayUrl.replace(/\/$/, '')}/agent?token=${encodeURIComponent(token)}`;
|
package/dist/templates/hook.js
CHANGED
|
@@ -139,7 +139,7 @@ async function main() {
|
|
|
139
139
|
if (inCourse) {
|
|
140
140
|
lines.push('## FASE DO CURSO DE ONBOARDING')
|
|
141
141
|
lines.push('O aluno está na etapa do curso **' + course.title + '** (progresso: **' + course.lessonsCompleted + '/' + course.totalLessons + '**).')
|
|
142
|
-
lines.push('
|
|
142
|
+
lines.push('Conduza o curso da ToStudy DIRETO pela CLI (o aluno já chega logado no install): rode no bash **tostudy courses --json**, depois **tostudy select <id>** pra ativar o curso, e ensine lição por lição com **tostudy next** / **tostudy progress** / **tostudy lesson** / **tostudy hint**. Se existir um slash-command **/tostudy-<curso>**, pode usá-lo. NÃO trabalhe em missões da imersão enquanto o curso não terminar. (Fallback: ferramentas MCP **get_course**/**complete_lesson** do paris.)')
|
|
143
143
|
if (PHASE === 'session-start') {
|
|
144
144
|
lines.push('')
|
|
145
145
|
lines.push('**Comece falando primeiro**: rode /tostudy e dê as boas-vindas. O aluno ainda não digitou nada.')
|
package/package.json
CHANGED