paris-immersion 0.1.13 → 0.1.15
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/install.js +10 -0
- package/dist/commands/serve.d.ts +0 -5
- package/dist/commands/serve.js +35 -1
- package/dist/index.js +1 -1
- package/dist/lib/relay-agent.js +26 -3
- package/dist/templates/hook.js +1 -1
- package/dist/templates/tostudy-command.d.ts +10 -0
- package/dist/templates/tostudy-command.js +37 -0
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -10,6 +10,7 @@ import { renderClaudeMd } from '../templates/claude-md.js';
|
|
|
10
10
|
import { renderSettingsJson } from '../templates/settings.js';
|
|
11
11
|
import { renderParisCoachSkill } from '../templates/skill.js';
|
|
12
12
|
import { renderHookScript } from '../templates/hook.js';
|
|
13
|
+
import { renderTostudyCommand } from '../templates/tostudy-command.js';
|
|
13
14
|
export async function installCommand(opts) {
|
|
14
15
|
const creds = await readCredentials();
|
|
15
16
|
if (!creds) {
|
|
@@ -89,6 +90,15 @@ async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
|
|
|
89
90
|
const skillDir = path.join(claudeDir, 'skills', 'paris-coach');
|
|
90
91
|
await fs.mkdir(skillDir, { recursive: true });
|
|
91
92
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), renderParisCoachSkill());
|
|
93
|
+
// /tostudy.md — NOSSO slash-command (rota confiável do curso). Escrito nos DOIS
|
|
94
|
+
// lugares que o Claude carrega: o do PROJETO e o GLOBAL (~/.claude/commands) —
|
|
95
|
+
// o global é o que faz funcionar em máquinas que já rodaram `tostudy init`, e
|
|
96
|
+
// agora vale pra todo aluno novo também. Independe do @tostudy-ai/cli.
|
|
97
|
+
const tostudyCmd = renderTostudyCommand();
|
|
98
|
+
for (const dir of [path.join(claudeDir, 'commands'), path.join(homedir(), '.claude', 'commands')]) {
|
|
99
|
+
await fs.mkdir(dir, { recursive: true });
|
|
100
|
+
await fs.writeFile(path.join(dir, 'tostudy.md'), tostudyCmd);
|
|
101
|
+
}
|
|
92
102
|
// CLAUDE.md — workspace-level rules.
|
|
93
103
|
await fs.writeFile(path.join(targetDir, 'CLAUDE.md'), renderClaudeMd({
|
|
94
104
|
immersionName: boot.immersion.name,
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -38,11 +38,6 @@ export interface ServeHandle {
|
|
|
38
38
|
close: () => void;
|
|
39
39
|
}
|
|
40
40
|
export declare function resolveKickoff(cwd: string): string;
|
|
41
|
-
/**
|
|
42
|
-
* Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
|
|
43
|
-
* workspace). Shared by the local ws server and the relay agent. Throws if
|
|
44
|
-
* node-pty can't load/spawn — callers report it on their channel.
|
|
45
|
-
*/
|
|
46
41
|
export declare function spawnCoachPty(cwd: string): IPty;
|
|
47
42
|
/**
|
|
48
43
|
* Start a localhost WebSocket server that brokers a pty (running `claude`) to a
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { promises as fs, chmodSync, existsSync, statSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { promises as fs, chmodSync, existsSync, statSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
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 { renderTostudyCommand } from '../templates/tostudy-command.js';
|
|
9
10
|
import { c } from '../lib/colors.js';
|
|
10
11
|
/**
|
|
11
12
|
* Restore the executable bit on node-pty's `spawn-helper`. Some install paths
|
|
@@ -119,6 +120,10 @@ export function resolveKickoff(cwd) {
|
|
|
119
120
|
if (names.size === 0)
|
|
120
121
|
return COURSE_TUTOR_PROMPT;
|
|
121
122
|
const all = [...names];
|
|
123
|
+
// Nosso /tostudy (escrito pelo install/serve, sempre presente) é a rota
|
|
124
|
+
// CONFIÁVEL — preferimos ele: o aluno e o tutor sempre rodam via /tostudy.
|
|
125
|
+
if (all.some((n) => n.toLowerCase() === 'tostudy'))
|
|
126
|
+
return '/tostudy';
|
|
122
127
|
// Curso ATIVO deste workspace = fonte da verdade pra escolher entre vários cursos.
|
|
123
128
|
let slug = '';
|
|
124
129
|
try {
|
|
@@ -148,10 +153,34 @@ export function resolveKickoff(cwd) {
|
|
|
148
153
|
* workspace). Shared by the local ws server and the relay agent. Throws if
|
|
149
154
|
* node-pty can't load/spawn — callers report it on their channel.
|
|
150
155
|
*/
|
|
156
|
+
/**
|
|
157
|
+
* Garante que o slash-command `/tostudy` exista no workspace ANTES do Claude
|
|
158
|
+
* subir (slash-commands carregam no startup — criados depois não aparecem sem
|
|
159
|
+
* reload). O `paris install` já escreve, mas reescrevemos aqui pra cobrir
|
|
160
|
+
* workspaces antigos e manter o conteúdo fresco a cada serve. Best-effort.
|
|
161
|
+
*/
|
|
162
|
+
function ensureTostudyCommand(cwd) {
|
|
163
|
+
const body = renderTostudyCommand();
|
|
164
|
+
// Escreve nos DOIS lugares que o Claude Code carrega slash-commands: o do
|
|
165
|
+
// PROJETO (workspace) e o GLOBAL (~/.claude/commands). É justamente o comando
|
|
166
|
+
// global que faz o terminal funcionar em máquinas que já passaram por um
|
|
167
|
+
// `tostudy init` — replicamos isso pra TODA máquina, inclusive a do aluno novo.
|
|
168
|
+
for (const dir of [path.join(cwd, '.claude', 'commands'), path.join(homedir(), '.claude', 'commands')]) {
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(dir, { recursive: true });
|
|
171
|
+
writeFileSync(path.join(dir, 'tostudy.md'), body);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* best-effort — se um falhar, o outro cobre; senão cai no fallback de texto */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
151
178
|
export function spawnCoachPty(cwd) {
|
|
152
179
|
const req = createRequire(import.meta.url);
|
|
153
180
|
ensureSpawnHelperExecutable(req);
|
|
154
181
|
const ptyMod = req('node-pty');
|
|
182
|
+
// Escreve /tostudy ANTES de resolver o kickoff + subir o Claude.
|
|
183
|
+
ensureTostudyCommand(cwd);
|
|
155
184
|
const kickoff = resolveKickoff(cwd);
|
|
156
185
|
const shell = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : 'bash');
|
|
157
186
|
const shellArgs = process.platform === 'win32'
|
|
@@ -214,6 +243,11 @@ export async function startServeServer(opts) {
|
|
|
214
243
|
wss.once('error', reject);
|
|
215
244
|
});
|
|
216
245
|
wss.on('connection', (ws) => {
|
|
246
|
+
// TCP_NODELAY: sem Nagle no socket local (caminho dev ws://localhost).
|
|
247
|
+
try {
|
|
248
|
+
ws._socket?.setNoDelay?.(true);
|
|
249
|
+
}
|
|
250
|
+
catch { /* noop */ }
|
|
217
251
|
if (opts.dryRun) {
|
|
218
252
|
// Tests use this branch — we just confirm the handshake passed.
|
|
219
253
|
ws.send('READY\n');
|
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.15');
|
|
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
|
@@ -100,12 +100,20 @@ export async function startRelayAgent(opts) {
|
|
|
100
100
|
const MAX_OUT_BUF = 256 * 1024;
|
|
101
101
|
let outBuf = '';
|
|
102
102
|
let flushTimer;
|
|
103
|
+
// Adaptativo: o PRIMEIRO chunk de uma rajada (depois de um período de silêncio)
|
|
104
|
+
// sai NA HORA — o eco de uma tecla e o início da resposta do Claude não pagam a
|
|
105
|
+
// janela inteira de coalescing. Só a CAUDA da rajada (tokens que chegam dentro
|
|
106
|
+
// da janela, streaming token-a-token) é coalescida, mantendo a escrita lisa.
|
|
107
|
+
// "Fast first byte, smooth tail." lastFlushAt marca o último envio: se passou
|
|
108
|
+
// mais de COALESCE_MS desde então E o buffer estava vazio, é um novo burst.
|
|
109
|
+
let lastFlushAt = 0;
|
|
103
110
|
const flushOut = () => {
|
|
104
111
|
flushTimer = undefined;
|
|
105
112
|
if (!outBuf)
|
|
106
113
|
return;
|
|
107
114
|
const data = outBuf;
|
|
108
115
|
outBuf = '';
|
|
116
|
+
lastFlushAt = Date.now();
|
|
109
117
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
110
118
|
try {
|
|
111
119
|
ws.send(seal(key, TYPE_DATA, Buffer.from(data, 'utf8')), { binary: true });
|
|
@@ -147,9 +155,14 @@ export async function startRelayAgent(opts) {
|
|
|
147
155
|
return;
|
|
148
156
|
}
|
|
149
157
|
pty.onData((d) => {
|
|
158
|
+
// Novo burst = buffer vazio E sem timer armado E passou >COALESCE_MS desde o
|
|
159
|
+
// último flush (= veio depois de um silêncio, p.ex. o aluno apertou Enter e o
|
|
160
|
+
// Claude começa a responder). Captura ANTES do append (depois nunca é vazio).
|
|
161
|
+
const newBurst = !outBuf && !flushTimer && Date.now() - lastFlushAt >= COALESCE_MS;
|
|
150
162
|
outBuf += d;
|
|
151
|
-
// Flush imediato se acumulou muito (evita um frame gigante)
|
|
152
|
-
|
|
163
|
+
// Flush imediato se acumulou muito (evita um frame gigante) OU se é o primeiro
|
|
164
|
+
// chunk de um novo burst (latência baixa pro início da resposta); senão coalesce.
|
|
165
|
+
if (outBuf.length >= MAX_OUT_BUF || newBurst) {
|
|
153
166
|
if (flushTimer) {
|
|
154
167
|
clearTimeout(flushTimer);
|
|
155
168
|
flushTimer = undefined;
|
|
@@ -201,7 +214,17 @@ export async function startRelayAgent(opts) {
|
|
|
201
214
|
return;
|
|
202
215
|
const url = `${relayUrl.replace(/\/$/, '')}/agent?token=${encodeURIComponent(token)}`;
|
|
203
216
|
ws = new WebSocket(url);
|
|
204
|
-
ws.on('open', () =>
|
|
217
|
+
ws.on('open', () => {
|
|
218
|
+
// TCP_NODELAY: terminal interativo manda frames pequenos; sem isso o Nagle
|
|
219
|
+
// os enfileira (~40ms/hop, pior ainda com delayed-ACK) → digitação e o 1º
|
|
220
|
+
// paint de cada resposta ficam visivelmente lentos. _socket é interno do
|
|
221
|
+
// `ws` — optional-chaining + try/catch degrada pro comportamento atual se mudar.
|
|
222
|
+
try {
|
|
223
|
+
ws._socket?.setNoDelay?.(true);
|
|
224
|
+
}
|
|
225
|
+
catch { /* noop */ }
|
|
226
|
+
log(`relay: conectado (${relayUrl})`);
|
|
227
|
+
});
|
|
205
228
|
ws.on('message', (data, isBinary) => {
|
|
206
229
|
if (!isBinary) {
|
|
207
230
|
// Controle do relay (texto): viewer-joined / viewer-left.
|
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('Rode **/tostudy** pra conduzir o curso da ToStudy (o comando está instalado no workspace e o aluno já chega logado). Ele ativa o curso e ensina lição por lição. 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.')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O slash-command `/tostudy` que O NOSSO LADO escreve no workspace
|
|
3
|
+
* (`<ws>/.claude/commands/tostudy.md`). É a rota CONFIÁVEL pra iniciar o curso:
|
|
4
|
+
* o `@tostudy-ai/cli` só instala um comando específico do curso (`/tostudy-<slug>`)
|
|
5
|
+
* SE rodar `init` — e mesmo então, se o Claude já tinha subido, ele não recarrega
|
|
6
|
+
* slash-commands. Escrevendo `/tostudy` nós mesmos (no `paris install` E no
|
|
7
|
+
* `paris serve`, antes do Claude subir), garantimos que `/tostudy` SEMPRE exista
|
|
8
|
+
* e SEMPRE funcione, conduzindo o curso real via a tostudy CLI.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderTostudyCommand(): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O slash-command `/tostudy` que O NOSSO LADO escreve no workspace
|
|
3
|
+
* (`<ws>/.claude/commands/tostudy.md`). É a rota CONFIÁVEL pra iniciar o curso:
|
|
4
|
+
* o `@tostudy-ai/cli` só instala um comando específico do curso (`/tostudy-<slug>`)
|
|
5
|
+
* SE rodar `init` — e mesmo então, se o Claude já tinha subido, ele não recarrega
|
|
6
|
+
* slash-commands. Escrevendo `/tostudy` nós mesmos (no `paris install` E no
|
|
7
|
+
* `paris serve`, antes do Claude subir), garantimos que `/tostudy` SEMPRE exista
|
|
8
|
+
* e SEMPRE funcione, conduzindo o curso real via a tostudy CLI.
|
|
9
|
+
*/
|
|
10
|
+
export function renderTostudyCommand() {
|
|
11
|
+
return `---
|
|
12
|
+
description: Inicia ou retoma o curso de onboarding da imersão (tutor ToStudy no terminal)
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Você é o **tutor do curso de onboarding** da imersão Paris Group, rodando no terminal do aluno. Conduza o curso da ToStudy em **português**, no ritmo do aluno. O aluno já chega logado na conta dele da ToStudy (auto-login no install).
|
|
16
|
+
|
|
17
|
+
## 1. Prepare o curso (em silêncio, antes de falar)
|
|
18
|
+
|
|
19
|
+
1. Rode \`tostudy courses --json\` para ver o curso em que o aluno está matriculado.
|
|
20
|
+
2. Rode \`tostudy select <id-ou-número-do-curso>\` para ativá-lo (escolha o curso da imersão; normalmente só há um).
|
|
21
|
+
3. Rode \`tostudy progress --json\` para ver onde ele parou.
|
|
22
|
+
|
|
23
|
+
## 2. Ensine lição por lição
|
|
24
|
+
|
|
25
|
+
- \`tostudy next\` — avança para a próxima lição (marca a atual como concluída).
|
|
26
|
+
- \`tostudy lesson\` / \`tostudy theory\` — conteúdo da lição atual.
|
|
27
|
+
- \`tostudy hint\` — dica progressiva quando o aluno travar.
|
|
28
|
+
- \`tostudy validate\` — valida exercícios.
|
|
29
|
+
- \`tostudy progress\` — status atual (lição X de Y, %).
|
|
30
|
+
|
|
31
|
+
Explique cada lição com suas palavras, traga exemplos do contexto de negócio do aluno e só avance quando ele entender. **Comece dando as boas-vindas** e já ensinando a primeira lição — o aluno ainda não digitou nada. NÃO trabalhe em missões da imersão enquanto o curso não terminar.
|
|
32
|
+
|
|
33
|
+
## 3. Fallback (se a ToStudy não estiver disponível)
|
|
34
|
+
|
|
35
|
+
Use as ferramentas MCP do paris: \`get_course\` (currículo + progresso) e \`complete_lesson\` (marcar lição concluída).
|
|
36
|
+
`;
|
|
37
|
+
}
|
package/package.json
CHANGED