paris-immersion 0.1.14 → 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.
@@ -90,11 +90,15 @@ async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
90
90
  const skillDir = path.join(claudeDir, 'skills', 'paris-coach');
91
91
  await fs.mkdir(skillDir, { recursive: true });
92
92
  await fs.writeFile(path.join(skillDir, 'SKILL.md'), renderParisCoachSkill());
93
- // .claude/commands/tostudy.md — NOSSO slash-command /tostudy (rota confiável do
94
- // curso). Garante que /tostudy sempre exista, independente do @tostudy-ai/cli.
95
- const commandsDir = path.join(claudeDir, 'commands');
96
- await fs.mkdir(commandsDir, { recursive: true });
97
- await fs.writeFile(path.join(commandsDir, 'tostudy.md'), renderTostudyCommand());
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
+ }
98
102
  // CLAUDE.md — workspace-level rules.
99
103
  await fs.writeFile(path.join(targetDir, 'CLAUDE.md'), renderClaudeMd({
100
104
  immersionName: boot.immersion.name,
@@ -160,13 +160,19 @@ export function resolveKickoff(cwd) {
160
160
  * workspaces antigos e manter o conteúdo fresco a cada serve. Best-effort.
161
161
  */
162
162
  function ensureTostudyCommand(cwd) {
163
- try {
164
- const dir = path.join(cwd, '.claude', 'commands');
165
- mkdirSync(dir, { recursive: true });
166
- writeFileSync(path.join(dir, 'tostudy.md'), renderTostudyCommand());
167
- }
168
- catch {
169
- /* best-effort — se falhar, o resolveKickoff cai no fallback de texto */
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
+ }
170
176
  }
171
177
  }
172
178
  export function spawnCoachPty(cwd) {
@@ -237,6 +243,11 @@ export async function startServeServer(opts) {
237
243
  wss.once('error', reject);
238
244
  });
239
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 */ }
240
251
  if (opts.dryRun) {
241
252
  // Tests use this branch — we just confirm the handshake passed.
242
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.14');
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)')
@@ -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); senão coalesce.
152
- if (outBuf.length >= MAX_OUT_BUF) {
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', () => log(`relay: conectado (${relayUrl})`));
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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": {