paris-immersion 0.1.38 → 0.1.39

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.
@@ -11,6 +11,8 @@ import { loginCommand } from './login.js';
11
11
  import { installCommand, ensureWorkspace } from './install.js';
12
12
  import { locateWorkspace } from './repair.js';
13
13
  import { maybeSelfUpdate } from '../lib/self-update.js';
14
+ import { restartDaemon } from '../lib/daemon.js';
15
+ import net from 'node:net';
14
16
  const execFileAsync = promisify(execFile);
15
17
  async function tokenOk() {
16
18
  try {
@@ -45,6 +47,45 @@ async function workspaceForSlug(slug) {
45
47
  }
46
48
  return null;
47
49
  }
50
+ /**
51
+ * Religa o agente local (o `paris serve` que o botão do curso na War Room usa) e
52
+ * só aprova quando ELE disser que conectou no relay. Um agente preso com login
53
+ * antigo deixava a War Room em "Não achei o paris rodando" (D1 de outubro).
54
+ * Dentro do terminal embutido não religa: este terminal É o agente.
55
+ */
56
+ async function ensureLocalAgent() {
57
+ if (process.env.PARIS_MCP_ENABLED === '1')
58
+ return { ok: true, detail: 'você já está no terminal do curso' };
59
+ const logFile = path.join(homedir(), '.paris', 'serve.log');
60
+ const offset = await fs.stat(logFile).then((s) => s.size).catch(() => 0);
61
+ try {
62
+ await restartDaemon();
63
+ }
64
+ catch (err) {
65
+ return { ok: false, detail: `não consegui religar o serviço: ${err instanceof Error ? err.message : String(err)}` };
66
+ }
67
+ const portOpen = () => new Promise((resolve) => {
68
+ const s = net.connect(31000, '127.0.0.1');
69
+ s.once('connect', () => { s.destroy(); resolve(true); });
70
+ s.once('error', () => resolve(false));
71
+ s.setTimeout(1000, () => { s.destroy(); resolve(false); });
72
+ });
73
+ for (let i = 0; i < 40; i++) {
74
+ await new Promise((r) => setTimeout(r, 750));
75
+ const fresh = await fs
76
+ .readFile(logFile)
77
+ .then((b) => b.subarray(offset).toString('utf8'))
78
+ .catch(() => '');
79
+ if (/relay: conectado|relay ativo/.test(fresh))
80
+ return { ok: true, detail: 'conectado' };
81
+ if (/login expirado/.test(fresh))
82
+ return { ok: false, detail: 'o login do agente não vale — rode `paris logout` e `paris conectar`' };
83
+ }
84
+ // Sem relay configurado (dev local) o agente só escuta a porta local.
85
+ return (await portOpen())
86
+ ? { ok: true, detail: 'rodando (sem relay)' }
87
+ : { ok: false, detail: 'o agente não subiu' };
88
+ }
48
89
  async function runHookCheck(dir) {
49
90
  const hook = path.join(dir, '.paris', 'hooks', 'paris-hook.mjs');
50
91
  try {
@@ -137,7 +178,15 @@ export async function conectarCommand(opts = {}) {
137
178
  return;
138
179
  }
139
180
  console.log(c.green(`✓ Envio ok (${r.sent} evento(s) enviados${r.pending ? `, ${r.pending} ainda na fila` : ''})`));
140
- // 4. Prova do lado do servidor
181
+ // 4. Agente local do curso (botão "abrir o curso" da War Room)
182
+ const agent = await ensureLocalAgent();
183
+ if (!agent.ok) {
184
+ console.log(c.red(`✗ Agente do curso: ${agent.detail}. Chame um mentor técnico.`));
185
+ process.exitCode = 1;
186
+ return;
187
+ }
188
+ console.log(c.green(`✓ Agente do curso ${agent.detail}`));
189
+ // 5. Prova do lado do servidor
141
190
  try {
142
191
  const st = await apiAuthFetch('/api/activity/status/me');
143
192
  const age = st.lastActivity ? Date.now() - new Date(st.lastActivity).getTime() : Infinity;
@@ -13,4 +13,8 @@ export declare function startRelayAgent(opts: {
13
13
  loginToken: string;
14
14
  cwd: string;
15
15
  log?: (msg: string) => void;
16
+ /** Lê o token de login atual do disco (default: ~/.paris/credentials). Teste injeta. */
17
+ readLoginToken?: () => Promise<string | null>;
18
+ /** Intervalo entre releituras do login enquanto ele está inválido (default 15s). */
19
+ unauthRetryMs?: number;
16
20
  }): Promise<RelayAgentHandle | null>;
@@ -39,6 +39,11 @@ function open(key, frame) {
39
39
  return null;
40
40
  }
41
41
  }
42
+ async function defaultReadLoginToken() {
43
+ const { readCredentials } = await import('./credentials.js');
44
+ const creds = await readCredentials().catch(() => null);
45
+ return creds?.token ?? null;
46
+ }
42
47
  /**
43
48
  * Inicia o agente relay. Busca o serve-token + info do relay na API (Bearer
44
49
  * creds.token), e se o relay estiver configurado, mantém a conexão wss e spawna
@@ -48,10 +53,28 @@ export async function startRelayAgent(opts) {
48
53
  if (!opts.apiUrl || !opts.loginToken)
49
54
  return null;
50
55
  const log = opts.log ?? (() => { });
56
+ // O loginToken NÃO é fixo: o aluno pode relogar (`paris login`/`paris conectar`)
57
+ // com o daemon rodando. Antes o agente guardava o token do boot pra sempre e,
58
+ // depois de um re-login, ficava a noite inteira em "login expirado" até alguém
59
+ // rodar `paris restart` (visto no D1 de outubro, 06/10 07:59). Agora, a cada
60
+ // 'unauthorized', ele relê ~/.paris/credentials e troca pelo token novo.
61
+ let loginToken = opts.loginToken;
62
+ const reloadLoginToken = async () => {
63
+ try {
64
+ const creds = await (opts.readLoginToken ?? defaultReadLoginToken)();
65
+ if (creds && creds !== loginToken) {
66
+ loginToken = creds;
67
+ log('paris: login novo detectado — reconectando');
68
+ return true;
69
+ }
70
+ }
71
+ catch { /* sem arquivo de credenciais: segue esperando */ }
72
+ return false;
73
+ };
51
74
  const fetchInfo = async () => {
52
75
  try {
53
76
  const res = await fetch(`${opts.apiUrl.replace(/\/$/, '')}/api/onboarding/v2/serve-token`, {
54
- headers: { authorization: `Bearer ${opts.loginToken}` },
77
+ headers: { authorization: `Bearer ${loginToken}` },
55
78
  signal: AbortSignal.timeout(8000),
56
79
  });
57
80
  if (res.ok)
@@ -85,9 +108,15 @@ export async function startRelayAgent(opts) {
85
108
  await sleep(5000);
86
109
  initial = await fetchInfo();
87
110
  }
88
- if (initial.kind === 'unauthorized') {
111
+ // Login inválido no boot: em vez de desistir do relay (o agente ficava fora até
112
+ // um restart manual), espera o aluno relogar — relê o arquivo de login a cada
113
+ // `unauthRetryMs` e segue assim que ele mudar e a API aceitar.
114
+ const unauthRetryMs = opts.unauthRetryMs ?? 15_000;
115
+ while (initial.kind === 'unauthorized') {
89
116
  log(RELOGIN_HINT);
90
- return null;
117
+ await sleep(unauthRetryMs);
118
+ await reloadLoginToken();
119
+ initial = await fetchInfo(); // mesmo token pode passar (ex.: participante criado depois)
91
120
  }
92
121
  if (initial.kind !== 'ok' || !initial.info.relayUrl || !initial.info.token || !initial.info.e2eKeyB64)
93
122
  return null;
@@ -255,8 +284,16 @@ export async function startRelayAgent(opts) {
255
284
  // Token de login morto: NÃO re-disca com o velho (o relay 401-a e o loop
256
285
  // nunca surfacing). Loga o caminho de re-login e agenda uma tentativa
257
286
  // espaçada — se o aluno rodar `paris login`, a próxima busca passa e reata.
287
+ // Login trocado no disco (re-login com o daemon rodando)? Usa o novo e
288
+ // reconecta já. Senão espaça e loga o caminho de re-login.
289
+ if (await reloadLoginToken()) {
290
+ nextRetryMs = RETRY_FAST_MS;
291
+ if (!closed)
292
+ reconnect = setTimeout(connect, 0);
293
+ return;
294
+ }
258
295
  log(RELOGIN_HINT);
259
- nextRetryMs = RETRY_UNAUTH_MS;
296
+ nextRetryMs = opts.unauthRetryMs ?? RETRY_UNAUTH_MS;
260
297
  if (!closed)
261
298
  reconnect = setTimeout(connect, nextRetryMs);
262
299
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Paris Immersion CLI — login, install workspace, status, task control. Para alunos das imersões da Paris Group.",
5
5
  "homepage": "https://parisgroup.ai",
6
6
  "repository": {