paris-immersion 0.1.35 → 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';
@@ -119,6 +119,8 @@ async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
119
119
  const settingsPayload = renderSettingsJson({ apiUrl });
120
120
  await fs.writeFile(path.join(claudeDir, 'settings.json'), JSON.stringify(settingsPayload, null, 2));
121
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));
122
124
  // .claude/skills/paris-coach/SKILL.md
123
125
  const skillDir = path.join(claudeDir, 'skills', 'paris-coach');
124
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')
@@ -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.35",
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": {