paris-immersion 0.1.16 → 0.1.18
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/doctor.js +54 -4
- package/dist/commands/serve.js +74 -4
- package/dist/index.js +1 -1
- package/dist/lib/relay-agent.js +82 -14
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
3
7
|
import { apiAuthFetch, apiFetch, defaultApiUrl, ApiError } from '../lib/api.js';
|
|
4
8
|
import { readCredentials, credentialsPath } from '../lib/credentials.js';
|
|
5
9
|
import { findMarker } from '../lib/marker.js';
|
|
6
10
|
import { c } from '../lib/colors.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
7
12
|
export async function doctorCommand(opts = {}) {
|
|
8
13
|
const checks = [];
|
|
9
14
|
// 1. Credentials file
|
|
@@ -44,6 +49,51 @@ export async function doctorCommand(opts = {}) {
|
|
|
44
49
|
const settingsExists = await fs.access(settingsPath).then(() => true).catch(() => false);
|
|
45
50
|
checks.push({ name: '.claude/settings.json', ok: settingsExists, detail: settingsExists ? settingsPath : 'missing — re-run `paris install`' });
|
|
46
51
|
}
|
|
52
|
+
// 6. Claude Code instalado (o terminal embutido roda `claude`) — bloqueia o install.
|
|
53
|
+
let claudeInstalled = false;
|
|
54
|
+
try {
|
|
55
|
+
await execFileAsync('claude', ['--version'], { timeout: 8000 });
|
|
56
|
+
claudeInstalled = true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
claudeInstalled = false;
|
|
60
|
+
}
|
|
61
|
+
checks.push({
|
|
62
|
+
name: 'claude code',
|
|
63
|
+
ok: claudeInstalled,
|
|
64
|
+
detail: claudeInstalled ? 'instalado' : 'não instalado / fora do PATH — rode: npm i -g @anthropic-ai/claude-code',
|
|
65
|
+
});
|
|
66
|
+
// 7. Login do Claude (INFORMATIVO — o aluno loga na etapa do curso, não bloqueia install).
|
|
67
|
+
if (claudeInstalled) {
|
|
68
|
+
let loggedIn = false;
|
|
69
|
+
try {
|
|
70
|
+
const j = JSON.parse(await fs.readFile(path.join(homedir(), '.claude.json'), 'utf8'));
|
|
71
|
+
loggedIn = !!j.oauthAccount && j.hasCompletedOnboarding === true;
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
loggedIn = e.code !== 'ENOENT';
|
|
75
|
+
}
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'claude login',
|
|
78
|
+
ok: loggedIn,
|
|
79
|
+
info: true,
|
|
80
|
+
detail: loggedIn ? 'logado' : 'ainda não logado — você loga uma vez no terminal do curso (rápido)',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// 8. node-pty (motor do terminal embutido) — bloqueia o install se não carrega.
|
|
84
|
+
let ptyOk = false;
|
|
85
|
+
try {
|
|
86
|
+
createRequire(import.meta.url)('node-pty');
|
|
87
|
+
ptyOk = true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
ptyOk = false;
|
|
91
|
+
}
|
|
92
|
+
checks.push({
|
|
93
|
+
name: 'node-pty',
|
|
94
|
+
ok: ptyOk,
|
|
95
|
+
detail: ptyOk ? 'ok' : 'falhou ao carregar — o terminal embutido não abre. Reinstale: npm i -g paris-immersion@latest',
|
|
96
|
+
});
|
|
47
97
|
// Report
|
|
48
98
|
console.log();
|
|
49
99
|
console.log(c.bold('paris doctor'));
|
|
@@ -51,9 +101,9 @@ export async function doctorCommand(opts = {}) {
|
|
|
51
101
|
console.log();
|
|
52
102
|
let failures = 0;
|
|
53
103
|
for (const ch of checks) {
|
|
54
|
-
const icon = ch.ok ? c.green('✓') : c.red('✗');
|
|
104
|
+
const icon = ch.ok ? c.green('✓') : ch.info ? c.yellow('○') : c.red('✗');
|
|
55
105
|
console.log(` ${icon} ${ch.name}${ch.detail ? c.dim(' — ' + ch.detail) : ''}`);
|
|
56
|
-
if (!ch.ok)
|
|
106
|
+
if (!ch.ok && !ch.info)
|
|
57
107
|
failures++;
|
|
58
108
|
}
|
|
59
109
|
console.log();
|
|
@@ -79,8 +129,8 @@ async function reportInstallStatus(checks, token, apiUrl) {
|
|
|
79
129
|
console.error(c.yellow(' ! --report ignorado: sem credenciais (rode `paris login`).'));
|
|
80
130
|
return;
|
|
81
131
|
}
|
|
82
|
-
const allOk = checks.every((c) => c.ok);
|
|
83
|
-
const failedNames = checks.filter((c) => !c.ok).map((c) => c.name);
|
|
132
|
+
const allOk = checks.filter((c) => !c.info).every((c) => c.ok);
|
|
133
|
+
const failedNames = checks.filter((c) => !c.ok && !c.info).map((c) => c.name);
|
|
84
134
|
const body = {
|
|
85
135
|
step: 'all',
|
|
86
136
|
status: allOk ? 'ok' : 'failed',
|
package/dist/commands/serve.js
CHANGED
|
@@ -7,6 +7,8 @@ import { readCredentials } from '../lib/credentials.js';
|
|
|
7
7
|
import { ensureWorkspace } from './install.js';
|
|
8
8
|
import { startRelayAgent } from '../lib/relay-agent.js';
|
|
9
9
|
import { renderTostudyCommand } from '../templates/tostudy-command.js';
|
|
10
|
+
import { renderHookScript } from '../templates/hook.js';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
10
12
|
import { maybeSelfUpdate } from '../lib/self-update.js';
|
|
11
13
|
import { c } from '../lib/colors.js';
|
|
12
14
|
/**
|
|
@@ -176,17 +178,85 @@ function ensureTostudyCommand(cwd) {
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Reescreve o HOOK (`.paris/hooks/paris-hook.mjs`) e o bundle MCP vendorizado a
|
|
183
|
+
* cada serve. Crucial: o `paris install` só os escreve na CRIAÇÃO do workspace —
|
|
184
|
+
* num workspace ANTIGO (instalado antes do sync de progresso existir), o binário
|
|
185
|
+
* do CLI se auto-atualiza mas o hook fica CONGELADO e o progresso NUNCA sincroniza
|
|
186
|
+
* (gestão eterna em 0/N). Reescrever a cada serve fecha esse gap. Best-effort,
|
|
187
|
+
* idempotente (ambos são 100% auto-gerados; o settings.json aponta pro mesmo path).
|
|
188
|
+
*/
|
|
189
|
+
function ensureWorkspaceVendored(cwd) {
|
|
190
|
+
try {
|
|
191
|
+
const hooksDir = path.join(cwd, '.paris', 'hooks');
|
|
192
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
193
|
+
writeFileSync(path.join(hooksDir, 'paris-hook.mjs'), renderHookScript(), { mode: 0o755 });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* best-effort */
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const mcpSrc = fileURLToPath(new URL('../../templates/mcp/index.mjs', import.meta.url));
|
|
200
|
+
const mcpDir = path.join(cwd, '.paris', 'mcp');
|
|
201
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
202
|
+
writeFileSync(path.join(mcpDir, 'index.mjs'), readFileSync(mcpSrc), { mode: 0o755 });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* best-effort — sem o bundle, o /tostudy ainda roda; só o fallback MCP fica velho */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* O aluno está logado no Claude Code? Checagem FILE-BASED (rápida, zero-custo) —
|
|
210
|
+
* NÃO usar `claude -p` como probe (ele trava esperando input/streama). No mac o
|
|
211
|
+
* token OAuth fica no Keychain, mas `~/.claude.json` guarda `oauthAccount` +
|
|
212
|
+
* `hasCompletedOnboarding` quando logado. Sem arquivo (ENOENT) = nunca rodou =
|
|
213
|
+
* não-logado. Arquivo ilegível por OUTRO motivo = fail-open (não bloqueia o curso).
|
|
214
|
+
*/
|
|
215
|
+
function isClaudeLoggedIn() {
|
|
216
|
+
try {
|
|
217
|
+
const raw = readFileSync(path.join(homedir(), '.claude.json'), 'utf8');
|
|
218
|
+
try {
|
|
219
|
+
const j = JSON.parse(raw);
|
|
220
|
+
return !!j.oauthAccount && j.hasCompletedOnboarding === true;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return true; // arquivo corrompido → não trava o curso
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
return e.code !== 'ENOENT';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
179
230
|
export function spawnCoachPty(cwd) {
|
|
180
231
|
const req = createRequire(import.meta.url);
|
|
181
232
|
ensureSpawnHelperExecutable(req);
|
|
182
233
|
const ptyMod = req('node-pty');
|
|
183
|
-
// Escreve /tostudy ANTES de resolver o kickoff + subir o Claude.
|
|
234
|
+
// Escreve /tostudy + hook + MCP frescos ANTES de resolver o kickoff + subir o Claude.
|
|
184
235
|
ensureTostudyCommand(cwd);
|
|
236
|
+
ensureWorkspaceVendored(cwd);
|
|
185
237
|
const kickoff = resolveKickoff(cwd);
|
|
186
238
|
const shell = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : 'bash');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
239
|
+
// --dangerously-skip-permissions: terminal de curso CONTROLADO — sem isso o Claude
|
|
240
|
+
// pede aprovação a CADA comando (`tostudy next` etc.) e o curso trava esperando o
|
|
241
|
+
// aluno escolher "Yes/No" sem entender. Aqui o tutor (/tostudy) precisa rodar a
|
|
242
|
+
// tostudy CLI livremente; é o mesmo modo que o terminal nativo do dev usa.
|
|
243
|
+
let shellArgs;
|
|
244
|
+
if (process.platform === 'win32') {
|
|
245
|
+
shellArgs = ['-NoLogo', '-Command', `claude --dangerously-skip-permissions "${kickoff.replace(/"/g, '`"')}"`];
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const q = kickoff.replace(/'/g, `'\\''`);
|
|
249
|
+
// (1) Claude não instalado → mensagem clara + cai num shell (sem tela branca/erro cru).
|
|
250
|
+
const notInstalled = `if ! command -v claude >/dev/null 2>&1; then ` +
|
|
251
|
+
`printf '\\n \\033[33m! O Claude Code nao esta instalado nesta maquina.\\033[0m\\n Rode: \\033[1mnpm i -g @anthropic-ai/claude-code\\033[0m e reabra a aba do curso.\\n\\n'; ` +
|
|
252
|
+
`exec bash -l; fi`;
|
|
253
|
+
// (2) Não logado → banner explicando o login (o kickoff /tostudy roda DEPOIS do
|
|
254
|
+
// login automaticamente — o Claude difere o prompt inicial até autenticar).
|
|
255
|
+
const loginBanner = isClaudeLoggedIn()
|
|
256
|
+
? ''
|
|
257
|
+
: `printf '\\n \\033[36mAntes do curso voce vai logar no Claude Code uma vez (e rapido):\\033[0m\\n escolha um tema com as setas, depois Sign in with Claude, abra a URL e cole o codigo.\\n Assim que logar, o curso comeca sozinho.\\n\\n'; `;
|
|
258
|
+
shellArgs = ['-l', '-c', `${notInstalled}\n${loginBanner}exec claude --dangerously-skip-permissions '${q}'`];
|
|
259
|
+
}
|
|
190
260
|
return ptyMod.spawn(shell, shellArgs, {
|
|
191
261
|
name: 'xterm-color',
|
|
192
262
|
cols: 80,
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ 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.18');
|
|
17
17
|
program
|
|
18
18
|
.command('login')
|
|
19
19
|
.description('Autenticar com sua conta Paris (device-code OR --token direto)')
|
package/dist/lib/relay-agent.js
CHANGED
|
@@ -51,12 +51,17 @@ export async function startRelayAgent(opts) {
|
|
|
51
51
|
headers: { authorization: `Bearer ${opts.loginToken}` },
|
|
52
52
|
signal: AbortSignal.timeout(8000),
|
|
53
53
|
});
|
|
54
|
-
if (
|
|
55
|
-
return
|
|
56
|
-
|
|
54
|
+
if (res.ok)
|
|
55
|
+
return { kind: 'ok', info: (await res.json()) };
|
|
56
|
+
// 401 (JWT inválido/expirado) e 404 (participant_not_found) são definitivos:
|
|
57
|
+
// re-tentar com o mesmo loginToken nunca vai passar. 5xx é tratado como
|
|
58
|
+
// inalcançável (transitório).
|
|
59
|
+
if (res.status === 401 || res.status === 403 || res.status === 404)
|
|
60
|
+
return { kind: 'unauthorized' };
|
|
61
|
+
return { kind: 'unreachable' };
|
|
57
62
|
}
|
|
58
63
|
catch {
|
|
59
|
-
return
|
|
64
|
+
return { kind: 'unreachable' }; // DNS/timeout/conn refused — a API pode voltar
|
|
60
65
|
}
|
|
61
66
|
};
|
|
62
67
|
// Primeira busca decide se o relay está configurado (senão → modo local-only).
|
|
@@ -65,20 +70,31 @@ export async function startRelayAgent(opts) {
|
|
|
65
70
|
// responde 200 na 1ª tentativa). KeepAlive mantém o serve, mas sem este retry o
|
|
66
71
|
// agente relay nunca subia se a 1ª busca caísse no boot.
|
|
67
72
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
73
|
+
// Mensagem acionável quando o loginToken (installer JWT ~7d) expirou ou o
|
|
74
|
+
// participante foi removido. O daemon roda detached, então isto vai pro log do
|
|
75
|
+
// launchd/systemd (pro diagnóstico) — o aluno é guiado pelo overlay do browser.
|
|
76
|
+
const RELOGIN_HINT = 'paris: login expirado — rode: npm i -g paris-immersion@latest && paris login';
|
|
68
77
|
let initial = await fetchInfo();
|
|
69
|
-
|
|
78
|
+
// Retry no boot SÓ enquanto a API está inalcançável (blip de rede: o daemon sobe
|
|
79
|
+
// antes da rede). Um 'unauthorized' é definitivo — não adianta re-tentar com o
|
|
80
|
+
// mesmo token; sai pro modo local-only e loga o caminho de re-login.
|
|
81
|
+
for (let i = 0; i < 6 && initial.kind === 'unreachable'; i++) {
|
|
70
82
|
await sleep(5000);
|
|
71
83
|
initial = await fetchInfo();
|
|
72
84
|
}
|
|
73
|
-
if (
|
|
85
|
+
if (initial.kind === 'unauthorized') {
|
|
86
|
+
log(RELOGIN_HINT);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (initial.kind !== 'ok' || !initial.info.relayUrl || !initial.info.token || !initial.info.e2eKeyB64)
|
|
74
90
|
return null;
|
|
75
91
|
// Mutáveis: o serve-token é de vida curta e é RENOVADO a cada (re)conexão (o
|
|
76
92
|
// loginToken/JWT do aluno é estável). Sem isso, o daemon que sobe no boot fica
|
|
77
93
|
// preso com um token expirado, o relay rejeita o agente, e o terminal embutido
|
|
78
94
|
// 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
|
|
95
|
+
let relayUrl = initial.info.relayUrl;
|
|
96
|
+
let token = initial.info.token;
|
|
97
|
+
let key = Buffer.from(initial.info.e2eKeyB64, 'base64'); // chave E2E é estável por participante
|
|
82
98
|
if (key.length !== 32)
|
|
83
99
|
return null;
|
|
84
100
|
let ws = null;
|
|
@@ -152,6 +168,20 @@ export async function startRelayAgent(opts) {
|
|
|
152
168
|
}
|
|
153
169
|
catch (err) {
|
|
154
170
|
log(`relay: pty spawn falhou (${err instanceof Error ? err.message : String(err)})`);
|
|
171
|
+
// Sem isto o viewer já recebeu 'agent-online' (verde "conectado") e ficaria
|
|
172
|
+
// com a TELA EM BRANCO pra sempre, sem pista. Manda um frame de texto E2E
|
|
173
|
+
// explicando + o conserto — renderiza direto no xterm do aluno.
|
|
174
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
175
|
+
const msg = '\r\n \x1b[33m! Nao consegui abrir o terminal nesta maquina.\x1b[0m\r\n' +
|
|
176
|
+
' Rode no Terminal: \x1b[1mnpm i -g paris-immersion@latest && paris restart\x1b[0m\r\n' +
|
|
177
|
+
' Se persistir, rode o instalador de novo (a CLI pode nao ter compilado aqui).\r\n\r\n';
|
|
178
|
+
try {
|
|
179
|
+
ws.send(seal(key, TYPE_DATA, Buffer.from(msg, 'utf8')), { binary: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* ws caiu */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
155
185
|
return;
|
|
156
186
|
}
|
|
157
187
|
pty.onData((d) => {
|
|
@@ -197,23 +227,59 @@ export async function startRelayAgent(opts) {
|
|
|
197
227
|
}
|
|
198
228
|
}, 80);
|
|
199
229
|
};
|
|
230
|
+
// Backoff de reconexão. Caminho normal = 2s (rápido, pra reatachar o aluno que
|
|
231
|
+
// recarregou). Mas quando a API responde 401/404 (loginToken expirou ou
|
|
232
|
+
// participante removido), re-discar a 2s com o token velho é um loop infinito
|
|
233
|
+
// que o relay 401-a sem parar — então espaçamos pra 60s e logamos o re-login.
|
|
234
|
+
const RETRY_FAST_MS = 2000;
|
|
235
|
+
const RETRY_UNAUTH_MS = 60_000;
|
|
236
|
+
let nextRetryMs = RETRY_FAST_MS;
|
|
200
237
|
const connect = async () => {
|
|
201
238
|
if (closed)
|
|
202
239
|
return;
|
|
203
240
|
// Renova token + relayUrl + chave a cada tentativa (o serve-token expira;
|
|
204
241
|
// o loginToken é estável). É isto que conserta o "agente preso no boot".
|
|
205
242
|
const fresh = await fetchInfo();
|
|
206
|
-
if (fresh
|
|
207
|
-
relayUrl = fresh.relayUrl;
|
|
208
|
-
token = fresh.token;
|
|
209
|
-
const k = Buffer.from(fresh.e2eKeyB64, 'base64');
|
|
243
|
+
if (fresh.kind === 'ok' && fresh.info.relayUrl && fresh.info.token && fresh.info.e2eKeyB64) {
|
|
244
|
+
relayUrl = fresh.info.relayUrl;
|
|
245
|
+
token = fresh.info.token;
|
|
246
|
+
const k = Buffer.from(fresh.info.e2eKeyB64, 'base64');
|
|
210
247
|
if (k.length === 32)
|
|
211
248
|
key = k;
|
|
249
|
+
nextRetryMs = RETRY_FAST_MS;
|
|
250
|
+
}
|
|
251
|
+
else if (fresh.kind === 'unauthorized') {
|
|
252
|
+
// Token de login morto: NÃO re-disca com o velho (o relay 401-a e o loop
|
|
253
|
+
// nunca surfacing). Loga o caminho de re-login e agenda uma tentativa
|
|
254
|
+
// espaçada — se o aluno rodar `paris login`, a próxima busca passa e reata.
|
|
255
|
+
log(RELOGIN_HINT);
|
|
256
|
+
nextRetryMs = RETRY_UNAUTH_MS;
|
|
257
|
+
if (!closed)
|
|
258
|
+
reconnect = setTimeout(connect, nextRetryMs);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Inalcançável (rede/5xx): re-disca com o token ATUAL (pode ainda ser válido
|
|
263
|
+
// — cobre o blip de rede no boot) no ritmo rápido.
|
|
264
|
+
nextRetryMs = RETRY_FAST_MS;
|
|
212
265
|
}
|
|
213
266
|
if (closed)
|
|
214
267
|
return;
|
|
215
268
|
const url = `${relayUrl.replace(/\/$/, '')}/agent?token=${encodeURIComponent(token)}`;
|
|
216
269
|
ws = new WebSocket(url);
|
|
270
|
+
// O relay rejeita o handshake (401) ANTES do upgrade quando o serve-token não
|
|
271
|
+
// valida — isso chega aqui como 'unexpected-response' (não 'open'/'close').
|
|
272
|
+
// Sem este handler o `ws` cai pra abortHandshake → error+close e re-disca a 2s.
|
|
273
|
+
// Tratamos como um sinal de re-busca: o próximo connect() chama fetchInfo() e,
|
|
274
|
+
// se a API disser 'unauthorized', o branch acima espaça/loga. O socket já está
|
|
275
|
+
// sendo abortado pelo `ws`; só registramos pra não cair no caminho silencioso.
|
|
276
|
+
ws.on('unexpected-response', (_req, res) => {
|
|
277
|
+
log(`relay: handshake rejeitado (${res.statusCode}) — re-buscando token`);
|
|
278
|
+
try {
|
|
279
|
+
res.resume();
|
|
280
|
+
}
|
|
281
|
+
catch { /* drena o corpo pra liberar o socket */ }
|
|
282
|
+
});
|
|
217
283
|
ws.on('open', () => {
|
|
218
284
|
// TCP_NODELAY: terminal interativo manda frames pequenos; sem isso o Nagle
|
|
219
285
|
// os enfileira (~40ms/hop, pior ainda com delayed-ACK) → digitação e o 1º
|
|
@@ -291,8 +357,10 @@ export async function startRelayAgent(opts) {
|
|
|
291
357
|
ws.on('close', () => {
|
|
292
358
|
// NÃO mata o pty aqui — mantém o Claude vivo pra reatachar quando o ws
|
|
293
359
|
// reconectar (o viewer-left + grace timer e o shutdown cuidam de matar).
|
|
360
|
+
// nextRetryMs = 2s no caminho normal; 60s quando a API marcou o token como
|
|
361
|
+
// morto (o connect() seguinte re-busca e re-avalia). Evita o loop 2s eterno.
|
|
294
362
|
if (!closed)
|
|
295
|
-
reconnect = setTimeout(connect,
|
|
363
|
+
reconnect = setTimeout(connect, nextRetryMs);
|
|
296
364
|
});
|
|
297
365
|
ws.on('error', () => {
|
|
298
366
|
try {
|
package/package.json
CHANGED