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.
- package/dist/commands/install.js +36 -1
- package/dist/commands/repair.d.ts +3 -0
- package/dist/commands/repair.js +48 -0
- package/dist/commands/serve.js +6 -1
- package/dist/index.js +6 -0
- package/dist/templates/claude-md.js +2 -1
- package/dist/templates/hook.js +29 -14
- package/dist/templates/settings.d.ts +8 -0
- package/dist/templates/settings.js +30 -4
- package/package.json +1 -1
- package/templates/mcp/admin.mjs +547 -147
- package/templates/mcp/index.mjs +547 -147
package/dist/commands/install.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/commands/serve.js
CHANGED
|
@@ -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.** \`
|
|
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
|
|
package/dist/templates/hook.js
CHANGED
|
@@ -212,7 +212,7 @@ async function refreshMarkerStatus(markerDir, marker, status) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// ---------------------------------------------------------------------------
|
|
215
|
-
// Dossiê do projeto (
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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