paris-immersion 0.1.34 → 0.1.36

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.
@@ -7,7 +7,7 @@ import { findMarker, writeMarker } from '../lib/marker.js';
7
7
  import { readCredentials } from '../lib/credentials.js';
8
8
  import { c } from '../lib/colors.js';
9
9
  import { renderClaudeMd } from '../templates/claude-md.js';
10
- import { renderSettingsJson } from '../templates/settings.js';
10
+ import { renderSettingsJson, renderMcpJson } from '../templates/settings.js';
11
11
  import { renderParisCoachSkill } from '../templates/skill.js';
12
12
  import { renderHookScript } from '../templates/hook.js';
13
13
  import { renderTostudyCommand } from '../templates/tostudy-command.js';
@@ -27,6 +27,7 @@ export async function installCommand(opts) {
27
27
  const boot = await bootstrapWorkspace(opts.immersion);
28
28
  const targetDir = path.resolve(opts.dir ?? `paris-${boot.immersion.slug}`);
29
29
  await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
30
+ await ensurePrototipador(targetDir);
30
31
  console.log();
31
32
  console.log(c.green(`✓ Workspace pronto em ${targetDir}`));
32
33
  console.log();
@@ -55,8 +56,40 @@ export async function ensureWorkspace(opts = {}) {
55
56
  // Predictable, findable location regardless of where `paris serve` was run.
56
57
  const targetDir = path.join(homedir(), `paris-${boot.immersion.slug}`);
57
58
  await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
59
+ await ensurePrototipador(targetDir);
58
60
  return { dir: targetDir, created: true };
59
61
  }
62
+ /**
63
+ * Prototipador (fork literal do buildermethods/design-os, 2026-08-17): é um
64
+ * TEMPLATE que o aluno trabalha dentro — clona pra `<ws>/prototipador`.
65
+ * Preferência: copiar do clone que o instalador deixou em ~/prototipador
66
+ * (offline-friendly); fallback: clone direto. Best-effort — sem git/rede, a
67
+ * missão 4 instrui o clone manual.
68
+ */
69
+ async function ensurePrototipador(targetDir) {
70
+ const dest = path.join(targetDir, 'prototipador');
71
+ try {
72
+ await fs.stat(dest);
73
+ return; // já existe
74
+ }
75
+ catch { /* segue */ }
76
+ const { execFile } = await import('node:child_process');
77
+ const { promisify } = await import('node:util');
78
+ const execFileP = promisify(execFile);
79
+ const homeClone = path.join(homedir(), 'prototipador');
80
+ try {
81
+ await fs.stat(path.join(homeClone, '.git'));
82
+ await execFileP('git', ['clone', homeClone, dest], { timeout: 30_000 });
83
+ // Aponta o origin pro remoto real (o clone local viraria origin=~/prototipador).
84
+ await execFileP('git', ['-C', dest, 'remote', 'set-url', 'origin', 'https://github.com/parisgroup-ai/prototipador.git'], { timeout: 10_000 }).catch(() => { });
85
+ return;
86
+ }
87
+ catch { /* sem clone local — tenta a rede */ }
88
+ try {
89
+ await execFileP('git', ['clone', 'https://github.com/parisgroup-ai/prototipador', dest], { timeout: 60_000 });
90
+ }
91
+ catch { /* best-effort */ }
92
+ }
60
93
  async function bootstrapWorkspace(immersion) {
61
94
  const slugQuery = immersion ? `?immersion=${encodeURIComponent(immersion)}` : '';
62
95
  return apiAuthFetch(`/immersion-app/v1/workspace/bootstrap${slugQuery}`, {
@@ -86,6 +119,8 @@ async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
86
119
  const settingsPayload = renderSettingsJson({ apiUrl });
87
120
  await fs.writeFile(path.join(claudeDir, 'settings.json'), JSON.stringify(settingsPayload, null, 2));
88
121
  await fs.writeFile(path.join(claudeDir, 'settings.local.json'), JSON.stringify(settingsPayload, null, 2));
122
+ // .mcp.json — é daqui que o Claude Code carrega o MCP do projeto (não do settings).
123
+ await fs.writeFile(path.join(targetDir, '.mcp.json'), JSON.stringify(renderMcpJson({ apiUrl }), null, 2));
89
124
  // .claude/skills/paris-coach/SKILL.md
90
125
  const skillDir = path.join(claudeDir, 'skills', 'paris-coach');
91
126
  await fs.mkdir(skillDir, { recursive: true });
@@ -0,0 +1,3 @@
1
+ export declare function repairCommand(opts: {
2
+ open?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { findMarker } from '../lib/marker.js';
6
+ import { c } from '../lib/colors.js';
7
+ import { installCommand } from './install.js';
8
+ /**
9
+ * `paris repair` — uma palavra só, sem `cd` nem `&&`. Existe porque o comando
10
+ * longo colado do WhatsApp quebrava (o app troca espaços por NBSP e o shell lê
11
+ * `cd: too many arguments`). Acha o workspace sozinho (a partir da pasta atual,
12
+ * subindo; senão o `~/paris-*` mais recente), reescreve hooks, MCP (.mcp.json),
13
+ * settings e CLAUDE.md SEM tocar no trabalho do aluno, e reabre o Claude na
14
+ * raiz continuando a última conversa.
15
+ */
16
+ async function locateWorkspace() {
17
+ const here = await findMarker();
18
+ if (here)
19
+ return here.dir;
20
+ const home = homedir();
21
+ const entries = await fs.readdir(home).catch(() => []);
22
+ let best = null;
23
+ for (const name of entries) {
24
+ if (!name.startsWith('paris-'))
25
+ continue;
26
+ const marker = path.join(home, name, '.paris', 'workspace.json');
27
+ const st = await fs.stat(marker).catch(() => null);
28
+ if (st && (!best || st.mtimeMs > best.mtime))
29
+ best = { dir: path.join(home, name), mtime: st.mtimeMs };
30
+ }
31
+ return best?.dir ?? null;
32
+ }
33
+ export async function repairCommand(opts) {
34
+ const dir = await locateWorkspace();
35
+ if (!dir) {
36
+ console.error(c.red('Não achei a pasta da imersão. Rode `paris install` primeiro.'));
37
+ process.exit(1);
38
+ }
39
+ console.log(c.dim(`Consertando o workspace em ${dir}...`));
40
+ await installCommand({ dir });
41
+ if (opts.open === false)
42
+ return;
43
+ console.log(c.green('✓ Pronto. Abrindo o Claude na pasta da imersão (continuando a última conversa)...'));
44
+ const cont = spawnSync('claude', ['--continue'], { cwd: dir, stdio: 'inherit' });
45
+ if (cont.error || (cont.status !== 0 && cont.status !== null)) {
46
+ spawnSync('claude', [], { cwd: dir, stdio: 'inherit' });
47
+ }
48
+ }
@@ -9,7 +9,7 @@ import { startRelayAgent } from '../lib/relay-agent.js';
9
9
  import { parseMissionAction, startMissionInOrca } from '../lib/orca-launcher.js';
10
10
  import { renderTostudyCommand } from '../templates/tostudy-command.js';
11
11
  import { renderHookScript } from '../templates/hook.js';
12
- import { renderSettingsJson } from '../templates/settings.js';
12
+ import { renderSettingsJson, renderMcpJson } from '../templates/settings.js';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { maybeSelfUpdate } from '../lib/self-update.js';
15
15
  import { c } from '../lib/colors.js';
@@ -210,6 +210,7 @@ function ensureWorkspaceVendored(cwd) {
210
210
  const payload = JSON.stringify(renderSettingsJson({ apiUrl: marker.api_url }), null, 2);
211
211
  writeFileSync(path.join(claudeDir, 'settings.json'), payload);
212
212
  writeFileSync(path.join(claudeDir, 'settings.local.json'), payload);
213
+ writeFileSync(path.join(cwd, '.mcp.json'), JSON.stringify(renderMcpJson({ apiUrl: marker.api_url }), null, 2));
213
214
  }
214
215
  }
215
216
  catch {
@@ -478,6 +479,10 @@ export async function serveCommand(opts) {
478
479
  try {
479
480
  const { dir, created } = await ensureWorkspace({});
480
481
  cwd = dir;
482
+ // Auto-cura já no boot do daemon (não só quando o terminal embutido abre):
483
+ // quem usa o Claude no terminal próprio/Orca também recebe .mcp.json/hook novos
484
+ // assim que o self-update reinicia o serve.
485
+ ensureWorkspaceVendored(dir);
481
486
  console.log(created
482
487
  ? ` ${c.green('✓')} workspace criado em ${c.bold(dir)}`
483
488
  : ` ${c.dim('workspace:')} ${dir}`);
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { createRequire } from 'node:module';
4
4
  import { loginCommand } from './commands/login.js';
5
5
  import { installCommand } from './commands/install.js';
6
+ import { repairCommand } from './commands/repair.js';
6
7
  import { doctorCommand } from './commands/doctor.js';
7
8
  import { tasksCommand } from './commands/tasks.js';
8
9
  import { statusCommand } from './commands/status.js';
@@ -32,6 +33,11 @@ program
32
33
  .option('-d, --dir <path>', 'pasta destino (padrão: cria uma nova com o slug da imersão)')
33
34
  .option('--immersion <slug>', 'imersão específica (caso o aluno esteja em mais de uma)')
34
35
  .action(installCommand);
36
+ program
37
+ .command('repair')
38
+ .description('Conserta a pasta da imersão (hooks, ferramentas do Claude) sem apagar nada e reabre o Claude')
39
+ .option('--no-open', 'só conserta, não abre o Claude')
40
+ .action(repairCommand);
35
41
  program
36
42
  .command('doctor')
37
43
  .description('Diagnostica credentials, marker, hooks, conectividade')
@@ -21,7 +21,8 @@ export function renderClaudeMd(opts) {
21
21
  3. **Transparência total.** Tudo aqui é gravado em audit_logs e o instrutor pode reabrir qualquer tarefa.
22
22
  4. **Nunca invente progresso.** Só marque o que de fato foi feito nessa sessão.
23
23
  5. **Inicie a sessão falando primeiro.** O hook SessionStart te entrega o cronograma + a tarefa atual antes do aluno digitar — abra o dia com um resumo claro (fase, tarefa, primeiro passo sugerido).
24
- 6. **O dossiê do projeto é sagrado.** \`produto/visao.md\` precisa SEMPRE refletir o que é o projeto, a dor, a ideia, como resolve e pra quem (o /visao do Prototipador escreve nesse formato). Toda decisão nova de escopo vai pro arquivo na hora. Ele sincroniza sozinho com a equipe no fim do turno e é pré-requisito pra concluir missões de entregável — sem dossiê, o mark_task_complete recusa.
24
+ 6. **O dossiê do projeto é sagrado.** O \`product/product-overview.md\` do Prototipador (gerado pelo /visao dentro da pasta \`prototipador/\` deste workspace — fork do design-os) precisa SEMPRE refletir o que é o projeto, a dor, a ideia, como resolve e pra quem. Toda decisão nova de escopo vai pro arquivo na hora. Ele sincroniza sozinho com a equipe no fim do turno e é pré-requisito pra concluir missões de entregável — sem dossiê, o mark_task_complete recusa.
25
+ 7. **O Prototipador é a pasta \`prototipador/\` deste workspace.** No dia 2, trabalhe DENTRO dela: /visao → /roadmap → /dados → /estilo → /estrutura → /secao → /dados-exemplo → /tela, e /exportar pra fechar. Se a pasta não existir: \`git clone https://github.com/parisgroup-ai/prototipador\`.
25
26
 
26
27
  ## MCP tools que você tem
27
28
 
@@ -212,7 +212,7 @@ async function refreshMarkerStatus(markerDir, marker, status) {
212
212
  }
213
213
 
214
214
  // ---------------------------------------------------------------------------
215
- // Dossiê do projeto (produto/visao.md + roadmap.md do Prototipador) → Caso do
215
+ // Dossiê do projeto (product/product-overview.md + product-roadmap.md) → Caso do
216
216
  // aluno no servidor. DETERMINÍSTICO: roda em todo Stop/SessionStart e envia
217
217
  // quando o arquivo mudou — o mentor sempre vê o que é o projeto, a dor e o
218
218
  // plano, sem depender do modelo lembrar de anotar.
@@ -223,22 +223,37 @@ async function syncCaseDossier(apiUrl, token, markerDir) {
223
223
  let state = {}
224
224
  try { state = JSON.parse(await fs.readFile(statePath, 'utf-8')) } catch {}
225
225
 
226
- const files = {
227
- visao: path.join(markerDir, 'produto', 'visao.md'),
228
- roadmap: path.join(markerDir, 'produto', 'roadmap.md'),
226
+ // Prototipador virou fork literal do design-os (2026-08-17): a saída canônica
227
+ // é product/product-overview.md + product/product-roadmap.md — na raiz do
228
+ // workspace OU dentro do clone do template (<ws>/prototipador). Os caminhos
229
+ // legados (produto/visao.md) seguem como fallback pra quem começou antes.
230
+ const candidates = {
231
+ visao: [
232
+ path.join(markerDir, 'product', 'product-overview.md'),
233
+ path.join(markerDir, 'prototipador', 'product', 'product-overview.md'),
234
+ path.join(markerDir, 'produto', 'visao.md'),
235
+ ],
236
+ roadmap: [
237
+ path.join(markerDir, 'product', 'product-roadmap.md'),
238
+ path.join(markerDir, 'prototipador', 'product', 'product-roadmap.md'),
239
+ path.join(markerDir, 'produto', 'roadmap.md'),
240
+ ],
229
241
  }
230
242
  const payload = {}
231
243
  const newState = { ...state }
232
- for (const [key, fp] of Object.entries(files)) {
233
- try {
234
- const st = await fs.stat(fp)
235
- const mtime = st.mtimeMs
236
- if (state[key] !== mtime) {
237
- const content = await fs.readFile(fp, 'utf-8')
238
- payload[key] = content.slice(0, 16384)
239
- newState[key] = mtime
240
- }
241
- } catch { /* arquivo ainda não existe — normal antes do /visao */ }
244
+ for (const [key, paths] of Object.entries(candidates)) {
245
+ for (const fp of paths) {
246
+ try {
247
+ const st = await fs.stat(fp)
248
+ const mtime = st.mtimeMs
249
+ if (state[key] !== mtime) {
250
+ const content = await fs.readFile(fp, 'utf-8')
251
+ payload[key] = content.slice(0, 16384)
252
+ newState[key] = mtime
253
+ }
254
+ break // primeiro caminho existente vence
255
+ } catch { /* tenta o próximo candidato */ }
256
+ }
242
257
  }
243
258
  if (Object.keys(payload).length === 0) return
244
259
 
@@ -10,3 +10,11 @@
10
10
  export declare function renderSettingsJson(opts: {
11
11
  apiUrl: string;
12
12
  }): Record<string, unknown>;
13
+ /**
14
+ * `.mcp.json` do workspace — o ÚNICO lugar de onde o Claude Code carrega MCP de
15
+ * projeto. Sem ele o aluno fica sem get_current_task/mark_task_complete/
16
+ * log_event e a missão não conclui sozinha (só pelo botão da War Room).
17
+ */
18
+ export declare function renderMcpJson(opts: {
19
+ apiUrl: string;
20
+ }): Record<string, unknown>;
@@ -7,6 +7,10 @@
7
7
  * already a prerequisite. Paths are relative to the workspace root, which is
8
8
  * the cwd Claude Code uses when launching hooks and MCP servers.
9
9
  */
10
+ // Hooks apontam pra RAIZ do projeto ($CLAUDE_PROJECT_DIR, setado pelo Claude Code
11
+ // em todo hook). Com caminho relativo, bastava o Claude dar `cd` numa subpasta
12
+ // (ex.: vault/10-SOPs) pra TODO hook falhar com MODULE_NOT_FOUND — o aluno some
13
+ // do painel e a sessão enche de "Stop hook error" (treino 30/09, 2026-09-30).
10
14
  export function renderSettingsJson(opts) {
11
15
  return {
12
16
  $schema: 'https://json.schemastore.org/claude-code-settings.json',
@@ -14,6 +18,12 @@ export function renderSettingsJson(opts) {
14
18
  // pode não conectar o server numa sessão não-interativa (terminal embutido),
15
19
  // e o tutor abre sem as tools get_course/complete_lesson.
16
20
  enableAllProjectMcpServers: true,
21
+ // O Claude Code NÃO carrega `mcpServers` do settings.json — MCP de projeto
22
+ // vive no `.mcp.json` (renderMcpJson abaixo). Este bloco fica só por
23
+ // compatibilidade; o que liga o server de verdade é o .mcp.json + esta
24
+ // pré-aprovação (sem ela o server fica "Pending approval"). Achado no
25
+ // teste real de 2026-09-29: o Claude do aluno abria SEM mark_task_complete.
26
+ enabledMcpjsonServers: ['paris-immersion'],
17
27
  mcpServers: {
18
28
  'paris-immersion': {
19
29
  command: 'node',
@@ -30,7 +40,7 @@ export function renderSettingsJson(opts) {
30
40
  hooks: [
31
41
  {
32
42
  type: 'command',
33
- command: 'node .paris/hooks/paris-hook.mjs session-start',
43
+ command: 'node "$CLAUDE_PROJECT_DIR/.paris/hooks/paris-hook.mjs" session-start',
34
44
  },
35
45
  ],
36
46
  },
@@ -41,7 +51,7 @@ export function renderSettingsJson(opts) {
41
51
  hooks: [
42
52
  {
43
53
  type: 'command',
44
- command: 'node .paris/hooks/paris-hook.mjs user-prompt-submit',
54
+ command: 'node "$CLAUDE_PROJECT_DIR/.paris/hooks/paris-hook.mjs" user-prompt-submit',
45
55
  },
46
56
  ],
47
57
  },
@@ -56,7 +66,7 @@ export function renderSettingsJson(opts) {
56
66
  hooks: [
57
67
  {
58
68
  type: 'command',
59
- command: 'node .paris/hooks/paris-hook.mjs post-tool-use',
69
+ command: 'node "$CLAUDE_PROJECT_DIR/.paris/hooks/paris-hook.mjs" post-tool-use',
60
70
  },
61
71
  ],
62
72
  },
@@ -67,7 +77,7 @@ export function renderSettingsJson(opts) {
67
77
  hooks: [
68
78
  {
69
79
  type: 'command',
70
- command: 'node .paris/hooks/paris-hook.mjs stop',
80
+ command: 'node "$CLAUDE_PROJECT_DIR/.paris/hooks/paris-hook.mjs" stop',
71
81
  },
72
82
  ],
73
83
  },
@@ -75,3 +85,19 @@ export function renderSettingsJson(opts) {
75
85
  },
76
86
  };
77
87
  }
88
+ /**
89
+ * `.mcp.json` do workspace — o ÚNICO lugar de onde o Claude Code carrega MCP de
90
+ * projeto. Sem ele o aluno fica sem get_current_task/mark_task_complete/
91
+ * log_event e a missão não conclui sozinha (só pelo botão da War Room).
92
+ */
93
+ export function renderMcpJson(opts) {
94
+ return {
95
+ mcpServers: {
96
+ 'paris-immersion': {
97
+ command: 'node',
98
+ args: ['.paris/mcp/index.mjs'],
99
+ env: { PARIS_API_URL: opts.apiUrl },
100
+ },
101
+ },
102
+ };
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
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": {