paris-immersion 0.1.0

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.
Files changed (39) hide show
  1. package/README.md +50 -0
  2. package/dist/commands/doctor.d.ts +5 -0
  3. package/dist/commands/doctor.js +104 -0
  4. package/dist/commands/graduate.d.ts +1 -0
  5. package/dist/commands/graduate.js +13 -0
  6. package/dist/commands/install.d.ts +4 -0
  7. package/dist/commands/install.js +88 -0
  8. package/dist/commands/login.d.ts +4 -0
  9. package/dist/commands/login.js +101 -0
  10. package/dist/commands/logout.d.ts +1 -0
  11. package/dist/commands/logout.js +20 -0
  12. package/dist/commands/reopen.d.ts +3 -0
  13. package/dist/commands/reopen.js +9 -0
  14. package/dist/commands/serve.d.ts +31 -0
  15. package/dist/commands/serve.js +180 -0
  16. package/dist/commands/status.d.ts +1 -0
  17. package/dist/commands/status.js +25 -0
  18. package/dist/commands/tasks.d.ts +1 -0
  19. package/dist/commands/tasks.js +32 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +62 -0
  22. package/dist/lib/api.d.ts +16 -0
  23. package/dist/lib/api.js +45 -0
  24. package/dist/lib/colors.d.ts +10 -0
  25. package/dist/lib/colors.js +18 -0
  26. package/dist/lib/credentials.d.ts +16 -0
  27. package/dist/lib/credentials.js +45 -0
  28. package/dist/lib/marker.d.ts +17 -0
  29. package/dist/lib/marker.js +27 -0
  30. package/dist/templates/claude-md.d.ts +5 -0
  31. package/dist/templates/claude-md.js +47 -0
  32. package/dist/templates/hook.d.ts +12 -0
  33. package/dist/templates/hook.js +132 -0
  34. package/dist/templates/settings.d.ts +12 -0
  35. package/dist/templates/settings.js +58 -0
  36. package/dist/templates/skill.d.ts +1 -0
  37. package/dist/templates/skill.js +61 -0
  38. package/package.json +55 -0
  39. package/templates/mcp/index.mjs +18171 -0
@@ -0,0 +1,32 @@
1
+ import { apiAuthFetch } from '../lib/api.js';
2
+ import { c } from '../lib/colors.js';
3
+ const STATUS_GLYPH = {
4
+ completed: c.green('✓'),
5
+ in_progress: c.yellow('◐'),
6
+ available: c.dim('○'),
7
+ locked: c.dim('🔒'),
8
+ skipped: c.dim('—'),
9
+ };
10
+ export async function tasksCommand() {
11
+ const today = await apiAuthFetch('/immersion-app/v1/today');
12
+ console.log();
13
+ console.log(c.bold(`${today.immersion.name} · dia ${today.day_number} · fase ${today.phase}`));
14
+ console.log();
15
+ if (today.schedule.length > 0) {
16
+ console.log(c.bold('Cronograma do dia:'));
17
+ for (const s of today.schedule) {
18
+ console.log(` ${c.cyan(s.starts_at + '–' + s.ends_at)} ${s.title}`);
19
+ }
20
+ console.log();
21
+ }
22
+ if (today.missions.length === 0) {
23
+ console.log(c.dim('Nenhuma tarefa pra essa fase.'));
24
+ return;
25
+ }
26
+ console.log(c.bold('Tarefas:'));
27
+ for (const m of today.missions) {
28
+ const glyph = STATUS_GLYPH[m.status] ?? c.dim('?');
29
+ console.log(` ${glyph} ${m.title} ${c.dim('(' + m.deliverable_type + ')')}`);
30
+ console.log(` ${c.dim(m.id)}`);
31
+ }
32
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { loginCommand } from './commands/login.js';
4
+ import { installCommand } from './commands/install.js';
5
+ import { doctorCommand } from './commands/doctor.js';
6
+ import { tasksCommand } from './commands/tasks.js';
7
+ import { statusCommand } from './commands/status.js';
8
+ import { reopenCommand } from './commands/reopen.js';
9
+ import { graduateCommand } from './commands/graduate.js';
10
+ import { logoutCommand } from './commands/logout.js';
11
+ import { serveCommand } from './commands/serve.js';
12
+ const program = new Command()
13
+ .name('paris')
14
+ .description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
15
+ .version('0.1.0');
16
+ program
17
+ .command('login')
18
+ .description('Autenticar com sua conta Paris (device-code OR --token direto)')
19
+ .option('--api-url <url>', 'API URL override')
20
+ .option('--token <jwt>', 'Skip device-code: usa JWT direto (do wizard fase 3)')
21
+ .action(loginCommand);
22
+ program
23
+ .command('install')
24
+ .description('Instala o workspace da imersão na pasta atual (ou em pasta nova)')
25
+ .option('-d, --dir <path>', 'pasta destino (padrão: cria uma nova com o slug da imersão)')
26
+ .option('--immersion <slug>', 'imersão específica (caso o aluno esteja em mais de uma)')
27
+ .action(installCommand);
28
+ program
29
+ .command('doctor')
30
+ .description('Diagnostica credentials, marker, hooks, conectividade')
31
+ .option('--report', 'Envia o resultado para /api/onboarding/v2/install-status (usado pelo wizard de onboarding)')
32
+ .action(doctorCommand);
33
+ program
34
+ .command('tasks')
35
+ .description('Lista as tarefas do dia (CLI-only, não abre Claude)')
36
+ .action(tasksCommand);
37
+ program
38
+ .command('status')
39
+ .description('Resumo da fase atual + tarefa em foco')
40
+ .action(statusCommand);
41
+ program
42
+ .command('reopen <mission-id>')
43
+ .description('Reabre uma tarefa marcada como concluída')
44
+ .option('--reason <text>', 'razão (visível ao instrutor)', 'student reopen')
45
+ .action(reopenCommand);
46
+ program
47
+ .command('graduate')
48
+ .description('Marcar a imersão como concluída do seu lado (override)')
49
+ .action(graduateCommand);
50
+ program
51
+ .command('logout')
52
+ .description('Apagar credenciais locais + revogar no servidor')
53
+ .action(logoutCommand);
54
+ program
55
+ .command('serve')
56
+ .description('Sobe WS local (ws://localhost:31000) para o terminal embarcado do onboarding')
57
+ .option('--port <port>', 'Porta do WebSocket', '31000')
58
+ .action(serveCommand);
59
+ program.parseAsync().catch((err) => {
60
+ console.error('\n[paris] erro:', err?.message ?? err);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,16 @@
1
+ export declare class ApiError extends Error {
2
+ status: number;
3
+ body: unknown;
4
+ constructor(message: string, status: number, body: unknown);
5
+ }
6
+ export type ApiOptions = {
7
+ method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
8
+ body?: unknown;
9
+ token?: string;
10
+ apiUrl?: string;
11
+ signal?: AbortSignal;
12
+ };
13
+ export declare function apiFetch<T = unknown>(pathname: string, opts?: ApiOptions): Promise<T>;
14
+ /** Fetch using whatever token is on disk. Throws if not logged in. */
15
+ export declare function apiAuthFetch<T = unknown>(pathname: string, opts?: Omit<ApiOptions, 'token'>): Promise<T>;
16
+ export declare function defaultApiUrl(): string;
@@ -0,0 +1,45 @@
1
+ import { readCredentials } from './credentials.js';
2
+ const DEFAULT_API_URL = process.env.PARIS_API_URL ?? 'http://localhost:3011';
3
+ export class ApiError extends Error {
4
+ status;
5
+ body;
6
+ constructor(message, status, body) {
7
+ super(message);
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ }
12
+ export async function apiFetch(pathname, opts = {}) {
13
+ const apiUrl = opts.apiUrl ?? DEFAULT_API_URL;
14
+ const headers = { accept: 'application/json' };
15
+ if (opts.body != null)
16
+ headers['content-type'] = 'application/json';
17
+ if (opts.token)
18
+ headers['authorization'] = `Bearer ${opts.token}`;
19
+ const res = await fetch(new URL(pathname, apiUrl), {
20
+ method: opts.method ?? (opts.body ? 'POST' : 'GET'),
21
+ headers,
22
+ body: opts.body != null ? JSON.stringify(opts.body) : undefined,
23
+ signal: opts.signal,
24
+ });
25
+ const contentType = res.headers.get('content-type') ?? '';
26
+ const payload = contentType.includes('application/json')
27
+ ? await res.json().catch(() => null)
28
+ : await res.text().catch(() => null);
29
+ if (!res.ok) {
30
+ const msg = (payload && typeof payload === 'object' && 'error' in payload && payload.error) || res.statusText;
31
+ throw new ApiError(String(msg), res.status, payload);
32
+ }
33
+ return payload;
34
+ }
35
+ /** Fetch using whatever token is on disk. Throws if not logged in. */
36
+ export async function apiAuthFetch(pathname, opts = {}) {
37
+ const creds = await readCredentials();
38
+ if (!creds) {
39
+ throw new ApiError('Not logged in — run `paris login` first.', 401, null);
40
+ }
41
+ return apiFetch(pathname, { ...opts, token: creds.token, apiUrl: opts.apiUrl ?? creds.api_url });
42
+ }
43
+ export function defaultApiUrl() {
44
+ return DEFAULT_API_URL;
45
+ }
@@ -0,0 +1,10 @@
1
+ export declare const c: {
2
+ bold: (s: string) => string;
3
+ dim: (s: string) => string;
4
+ red: (s: string) => string;
5
+ green: (s: string) => string;
6
+ yellow: (s: string) => string;
7
+ blue: (s: string) => string;
8
+ magenta: (s: string) => string;
9
+ cyan: (s: string) => string;
10
+ };
@@ -0,0 +1,18 @@
1
+ // Minimal ANSI helpers — avoids pulling in `kleur`/`chalk` deps. If stdout
2
+ // isn't a TTY we strip the codes so logs piped to files stay clean.
3
+ const isTTY = process.stdout.isTTY === true;
4
+ function wrap(open, close, text) {
5
+ if (!isTTY)
6
+ return text;
7
+ return `\x1b[${open}m${text}\x1b[${close}m`;
8
+ }
9
+ export const c = {
10
+ bold: (s) => wrap('1', '22', s),
11
+ dim: (s) => wrap('2', '22', s),
12
+ red: (s) => wrap('31', '39', s),
13
+ green: (s) => wrap('32', '39', s),
14
+ yellow: (s) => wrap('33', '39', s),
15
+ blue: (s) => wrap('34', '39', s),
16
+ magenta: (s) => wrap('35', '39', s),
17
+ cyan: (s) => wrap('36', '39', s),
18
+ };
@@ -0,0 +1,16 @@
1
+ export type Credentials = {
2
+ token: string;
3
+ user_id: string;
4
+ email: string;
5
+ api_url: string;
6
+ obtained_at: string;
7
+ };
8
+ /**
9
+ * Read the on-disk credentials file. Returns null if missing. We deliberately
10
+ * do NOT throw on file-not-found because most CLI commands need to detect
11
+ * "not logged in" and show a friendly message instead of a stack trace.
12
+ */
13
+ export declare function readCredentials(): Promise<Credentials | null>;
14
+ export declare function writeCredentials(creds: Credentials): Promise<void>;
15
+ export declare function deleteCredentials(): Promise<boolean>;
16
+ export declare function credentialsPath(): string;
@@ -0,0 +1,45 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ // PARIS_CREDENTIALS_FILE lets tests / dev override the on-disk credentials
5
+ // location without faking $HOME (which breaks every other tool's auth).
6
+ const CREDENTIALS_FILE = process.env.PARIS_CREDENTIALS_FILE ?? path.join(homedir(), '.paris', 'credentials');
7
+ const CREDENTIALS_DIR = path.dirname(CREDENTIALS_FILE);
8
+ /**
9
+ * Read the on-disk credentials file. Returns null if missing. We deliberately
10
+ * do NOT throw on file-not-found because most CLI commands need to detect
11
+ * "not logged in" and show a friendly message instead of a stack trace.
12
+ */
13
+ export async function readCredentials() {
14
+ try {
15
+ const raw = await fs.readFile(CREDENTIALS_FILE, 'utf-8');
16
+ return JSON.parse(raw);
17
+ }
18
+ catch (err) {
19
+ if (err?.code === 'ENOENT')
20
+ return null;
21
+ throw err;
22
+ }
23
+ }
24
+ export async function writeCredentials(creds) {
25
+ await fs.mkdir(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
26
+ // Write to a temp + rename so a concurrent read can't see a half-written file,
27
+ // and force 0600 perms (only owner can read the token).
28
+ const tmp = CREDENTIALS_FILE + '.tmp';
29
+ await fs.writeFile(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
30
+ await fs.rename(tmp, CREDENTIALS_FILE);
31
+ }
32
+ export async function deleteCredentials() {
33
+ try {
34
+ await fs.unlink(CREDENTIALS_FILE);
35
+ return true;
36
+ }
37
+ catch (err) {
38
+ if (err?.code === 'ENOENT')
39
+ return false;
40
+ throw err;
41
+ }
42
+ }
43
+ export function credentialsPath() {
44
+ return CREDENTIALS_FILE;
45
+ }
@@ -0,0 +1,17 @@
1
+ export type WorkspaceMarker = {
2
+ immersion_id: string;
3
+ immersion_slug: string;
4
+ participant_id: string;
5
+ student_user_id: string;
6
+ created_at: string;
7
+ api_url: string;
8
+ /** Local override — server is the source of truth, but if this is set the
9
+ * hooks treat the workspace as graduated regardless of server response. */
10
+ status?: 'active' | 'graduated';
11
+ };
12
+ /** Walk up from `start` looking for `.paris/workspace.json`. */
13
+ export declare function findMarker(start?: string): Promise<{
14
+ dir: string;
15
+ marker: WorkspaceMarker;
16
+ } | null>;
17
+ export declare function writeMarker(workspaceDir: string, marker: WorkspaceMarker): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ const MARKER_REL_PATH = '.paris/workspace.json';
4
+ /** Walk up from `start` looking for `.paris/workspace.json`. */
5
+ export async function findMarker(start = process.cwd()) {
6
+ let dir = path.resolve(start);
7
+ while (true) {
8
+ const candidate = path.join(dir, MARKER_REL_PATH);
9
+ try {
10
+ const raw = await fs.readFile(candidate, 'utf-8');
11
+ const marker = JSON.parse(raw);
12
+ return { dir, marker };
13
+ }
14
+ catch {
15
+ // fall through to parent
16
+ }
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir)
19
+ return null;
20
+ dir = parent;
21
+ }
22
+ }
23
+ export async function writeMarker(workspaceDir, marker) {
24
+ const parisDir = path.join(workspaceDir, '.paris');
25
+ await fs.mkdir(parisDir, { recursive: true });
26
+ await fs.writeFile(path.join(parisDir, 'workspace.json'), JSON.stringify(marker, null, 2));
27
+ }
@@ -0,0 +1,5 @@
1
+ export declare function renderClaudeMd(opts: {
2
+ immersionName: string;
3
+ immersionSlug: string;
4
+ participantEmail: string;
5
+ }): string;
@@ -0,0 +1,47 @@
1
+ export function renderClaudeMd(opts) {
2
+ return `# ${opts.immersionName} — workspace do aluno
3
+
4
+ > Você (Claude) é o **coach técnico** desse aluno durante a Imersão Paris.
5
+ > A pasta atual foi criada por \`paris install\` e é o canvas onde a jornada acontece.
6
+
7
+ ## Identidade
8
+
9
+ - **Imersão:** ${opts.immersionName} (\`${opts.immersionSlug}\`)
10
+ - **Aluno:** ${opts.participantEmail}
11
+ - **Modo:** \`active\` enquanto a imersão estiver rodando; \`graduated\` (silencioso) após.
12
+
13
+ ## Suas regras
14
+
15
+ 1. **Coach, não tribunal.** Antes de marcar uma tarefa como concluída, você DEVE:
16
+ - Rodar os testes/validações pertinentes ao \`deliverable_type\` da tarefa.
17
+ - Pedir evidência objetiva ao aluno (link, arquivo, número, screenshot).
18
+ - Checar o \`socio_prompt\` da tarefa pra garantir que o aluno respondeu o que a imersão pediu.
19
+ - Apresentar um mini-checklist do que falta ANTES de aceitar um "acabei".
20
+ 2. **Respeite a decisão final do aluno.** Se ele disser "acabei" depois do checklist, marque (via MCP \`mark_task_complete\`). Você não é juiz.
21
+ 3. **Transparência total.** Tudo aqui é gravado em audit_logs e o instrutor pode reabrir qualquer tarefa.
22
+ 4. **Nunca invente progresso.** Só marque o que de fato foi feito nessa sessão.
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
+
25
+ ## MCP tools que você tem
26
+
27
+ Carregadas via servidor MCP local \`.paris/mcp/index.mjs\` (configurado em \`.claude/settings.json\`):
28
+
29
+ | Tool | Quando usar |
30
+ |------|-------------|
31
+ | \`get_today\` | Sessão começou OU o aluno perguntou o que tem hoje |
32
+ | \`get_current_task\` | Aluno mudou de assunto OU você quer reorientar |
33
+ | \`mark_task_complete\` | Aluno disse "acabei" + checklist OK |
34
+ | \`reopen_task\` | Aluno (ou instrutor via comentário) pediu pra reabrir |
35
+ | \`comment_on_task\` | Aluno quer registrar nota visível ao instrutor |
36
+ | \`log_event\` | Telemetria leve (\`blocked\`, \`milestone\`, etc.) |
37
+ | \`get_immersion_context\` | Refresh do contexto inteiro |
38
+
39
+ ## Skill associada
40
+
41
+ A skill \`paris-coach\` (\`.claude/skills/paris-coach/SKILL.md\`) aprofunda as regras de checklist por tipo de deliverable (\`text\`, \`artifact\`, \`checklist\`, \`pitch\`). Ative quando entrar numa tarefa.
42
+
43
+ ---
44
+
45
+ Pós-imersão (status \`graduated\`): essas regras param de valer. A pasta é sua, faça o que quiser com ela.
46
+ `;
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The hook script that Claude Code invokes on SessionStart / UserPromptSubmit /
3
+ * Stop. We ship it as a string so `paris install` can write it verbatim into
4
+ * the workspace without needing the @paris/cli package to be installed in the
5
+ * student's node_modules.
6
+ *
7
+ * The script is pure stdlib: walks up to find the marker, reads credentials,
8
+ * calls the API, prints a system-reminder to stdout (which Claude Code feeds
9
+ * back to the model). Failures degrade gracefully — they print to stderr so
10
+ * the student can still get useful debug info, but never block the session.
11
+ */
12
+ export declare function renderHookScript(): string;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The hook script that Claude Code invokes on SessionStart / UserPromptSubmit /
3
+ * Stop. We ship it as a string so `paris install` can write it verbatim into
4
+ * the workspace without needing the @paris/cli package to be installed in the
5
+ * student's node_modules.
6
+ *
7
+ * The script is pure stdlib: walks up to find the marker, reads credentials,
8
+ * calls the API, prints a system-reminder to stdout (which Claude Code feeds
9
+ * back to the model). Failures degrade gracefully — they print to stderr so
10
+ * the student can still get useful debug info, but never block the session.
11
+ */
12
+ export function renderHookScript() {
13
+ return `#!/usr/bin/env node
14
+ // Auto-generated by \`paris install\`. Do not edit — regenerate via \`paris install\`.
15
+
16
+ import { promises as fs } from 'node:fs'
17
+ import { homedir } from 'node:os'
18
+ import path from 'node:path'
19
+
20
+ const PHASE = process.argv[2] // session-start | user-prompt-submit | stop
21
+
22
+ async function loadCredentials() {
23
+ try {
24
+ const file = process.env.PARIS_CREDENTIALS_FILE || path.join(homedir(), '.paris', 'credentials')
25
+ const raw = await fs.readFile(file, 'utf-8')
26
+ return JSON.parse(raw)
27
+ } catch { return null }
28
+ }
29
+
30
+ async function loadMarker() {
31
+ let dir = process.cwd()
32
+ while (true) {
33
+ try {
34
+ const raw = await fs.readFile(path.join(dir, '.paris', 'workspace.json'), 'utf-8')
35
+ return { dir, marker: JSON.parse(raw) }
36
+ } catch {}
37
+ const parent = path.dirname(dir)
38
+ if (parent === dir) return null
39
+ dir = parent
40
+ }
41
+ }
42
+
43
+ async function apiGet(apiUrl, pathname, token) {
44
+ const res = await fetch(new URL(pathname, apiUrl), {
45
+ headers: { authorization: 'Bearer ' + token, accept: 'application/json' },
46
+ })
47
+ if (!res.ok) return null
48
+ return res.json()
49
+ }
50
+
51
+ async function apiPost(apiUrl, pathname, token, body) {
52
+ const res = await fetch(new URL(pathname, apiUrl), {
53
+ method: 'POST',
54
+ headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
55
+ body: JSON.stringify(body),
56
+ })
57
+ if (!res.ok) return null
58
+ return res.json().catch(() => null)
59
+ }
60
+
61
+ async function main() {
62
+ const marker = await loadMarker()
63
+ if (!marker) return // not a paris workspace — silent no-op
64
+ if (marker.marker.status === 'graduated') return // graduated — silent
65
+
66
+ const creds = await loadCredentials()
67
+ if (!creds) {
68
+ process.stderr.write('[paris-hook] sem credenciais; rode \`paris login\` na sua shell.\\n')
69
+ return
70
+ }
71
+
72
+ const apiUrl = marker.marker.api_url || creds.api_url
73
+
74
+ if (PHASE === 'stop') {
75
+ await apiPost(apiUrl, '/immersion-app/v1/events', creds.token, { type: 'turn_ended' })
76
+ return
77
+ }
78
+
79
+ const [workspace, current] = await Promise.all([
80
+ apiGet(apiUrl, '/immersion-app/v1/workspace', creds.token),
81
+ apiGet(apiUrl, '/immersion-app/v1/missions/current', creds.token),
82
+ ])
83
+
84
+ if (!workspace) {
85
+ process.stderr.write('[paris-hook] não foi possível buscar contexto. Está offline?\\n')
86
+ return
87
+ }
88
+ if (workspace.status === 'graduated') return
89
+
90
+ const lines = []
91
+ if (PHASE === 'session-start') {
92
+ lines.push('# Paris Immersion — contexto da sessão')
93
+ lines.push('')
94
+ lines.push('Imersão: **' + workspace.immersion.name + '** (\\'' + workspace.immersion.slug + '\\')')
95
+ lines.push('Dia: **' + workspace.day_number + '** · fase: **' + workspace.phase + '**')
96
+ lines.push('')
97
+ lines.push('Você (Claude) é o coach técnico desse aluno. Siga as regras do CLAUDE.md e da skill paris-coach. **Comece falando primeiro** com um resumo do dia e a tarefa atual — o aluno NÃO digitou nada ainda.')
98
+ lines.push('')
99
+ } else {
100
+ lines.push('# Paris — refresh de contexto')
101
+ lines.push('')
102
+ }
103
+
104
+ if (current && current.current) {
105
+ lines.push('## Tarefa atual')
106
+ lines.push('- **id:** \\'' + current.current.id + '\\'')
107
+ lines.push('- **título:** ' + current.current.title)
108
+ lines.push('- **tipo:** ' + current.current.deliverable_type)
109
+ lines.push('- **status:** ' + current.current.status)
110
+ if (current.current.description) lines.push('- **descrição:** ' + current.current.description)
111
+ if (current.current.socio_prompt) {
112
+ lines.push('')
113
+ lines.push('### Sócio prompt')
114
+ lines.push(current.current.socio_prompt)
115
+ }
116
+ } else {
117
+ lines.push('## Sem tarefa em foco')
118
+ lines.push('Pergunte ao aluno em que ele quer trabalhar.')
119
+ }
120
+
121
+ process.stdout.write(lines.join('\\n') + '\\n')
122
+
123
+ if (PHASE === 'session-start') {
124
+ await apiPost(apiUrl, '/immersion-app/v1/events', creds.token, { type: 'session_started' })
125
+ }
126
+ }
127
+
128
+ main().catch((err) => {
129
+ process.stderr.write('[paris-hook] erro: ' + (err && err.message || err) + '\\n')
130
+ })
131
+ `;
132
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Renders the .claude/settings.json that `paris install` drops into the
3
+ * workspace. Both the hooks (.paris/hooks/paris-hook.mjs) and the MCP server
4
+ * (.paris/mcp/index.mjs) are vendored into the workspace by `paris install` —
5
+ * no npm/npx fetch happens at session start. The MCP bundle is self-contained
6
+ * (all deps inlined), so the only runtime requirement is `node`, which is
7
+ * already a prerequisite. Paths are relative to the workspace root, which is
8
+ * the cwd Claude Code uses when launching hooks and MCP servers.
9
+ */
10
+ export declare function renderSettingsJson(opts: {
11
+ apiUrl: string;
12
+ }): Record<string, unknown>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Renders the .claude/settings.json that `paris install` drops into the
3
+ * workspace. Both the hooks (.paris/hooks/paris-hook.mjs) and the MCP server
4
+ * (.paris/mcp/index.mjs) are vendored into the workspace by `paris install` —
5
+ * no npm/npx fetch happens at session start. The MCP bundle is self-contained
6
+ * (all deps inlined), so the only runtime requirement is `node`, which is
7
+ * already a prerequisite. Paths are relative to the workspace root, which is
8
+ * the cwd Claude Code uses when launching hooks and MCP servers.
9
+ */
10
+ export function renderSettingsJson(opts) {
11
+ return {
12
+ $schema: 'https://json.schemastore.org/claude-code-settings',
13
+ mcpServers: {
14
+ 'paris-immersion': {
15
+ command: 'node',
16
+ args: ['.paris/mcp/index.mjs'],
17
+ env: {
18
+ PARIS_API_URL: opts.apiUrl,
19
+ },
20
+ },
21
+ },
22
+ hooks: {
23
+ SessionStart: [
24
+ {
25
+ matcher: '*',
26
+ hooks: [
27
+ {
28
+ type: 'command',
29
+ command: 'node .paris/hooks/paris-hook.mjs session-start',
30
+ },
31
+ ],
32
+ },
33
+ ],
34
+ UserPromptSubmit: [
35
+ {
36
+ matcher: '*',
37
+ hooks: [
38
+ {
39
+ type: 'command',
40
+ command: 'node .paris/hooks/paris-hook.mjs user-prompt-submit',
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ Stop: [
46
+ {
47
+ matcher: '*',
48
+ hooks: [
49
+ {
50
+ type: 'command',
51
+ command: 'node .paris/hooks/paris-hook.mjs stop',
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ }
@@ -0,0 +1 @@
1
+ export declare function renderParisCoachSkill(): string;
@@ -0,0 +1,61 @@
1
+ export function renderParisCoachSkill() {
2
+ return `---
3
+ name: paris-coach
4
+ description: Ative quando entrar em uma tarefa da Imersão Paris. Aplica checklist específico por tipo de deliverable antes de marcar a tarefa como concluída.
5
+ ---
6
+
7
+ # Paris Coach — checklist por deliverable
8
+
9
+ Você é o coach técnico. Antes de chamar \`mark_task_complete\` via MCP, garanta o checklist correspondente ao \`deliverable_type\` da tarefa.
10
+
11
+ ## deliverable_type: text
12
+
13
+ Tarefa exige uma resposta escrita (validação, análise, plano, reflexão).
14
+
15
+ Checklist:
16
+ 1. O aluno produziu pelo menos 1 parágrafo concreto, não 1 frase genérica.
17
+ 2. Inclui exemplos ou evidência (números, citação, comparação).
18
+ 3. Responde diretamente ao \`socio_prompt\` da tarefa (releia antes de marcar).
19
+ 4. Aluno releu antes de você marcar.
20
+
21
+ Se algo falta: explique o que e ofereça pra ajudar a estruturar.
22
+
23
+ ## deliverable_type: artifact
24
+
25
+ Tarefa exige um artefato (deck, doc, planilha, link, código).
26
+
27
+ Checklist:
28
+ 1. Existe um link/arquivo concreto (não "vou fazer depois").
29
+ 2. O artefato abre sem erro / não está vazio.
30
+ 3. Você (Claude) leu o artefato ou pelo menos viu o título + 1 seção.
31
+ 4. Conecta com o objetivo da tarefa (não é um arquivo solto).
32
+
33
+ Se for código: rode \`pnpm test\` / \`pnpm type-check\` ou equivalente. Sem teste passando, peça pro aluno rodar antes de marcar.
34
+
35
+ ## deliverable_type: checklist
36
+
37
+ Tarefa é uma lista de sub-itens a executar.
38
+
39
+ Checklist:
40
+ 1. Cada sub-item tem um "sim" explícito do aluno.
41
+ 2. Você listou o que ele disse pra cada item antes de fechar.
42
+ 3. Se algum item ficou "parcial", chame \`comment_on_task\` registrando isso antes de marcar — não esconda do instrutor.
43
+
44
+ ## deliverable_type: pitch
45
+
46
+ Tarefa exige uma fala (pitch, apresentação, gravação).
47
+
48
+ Checklist:
49
+ 1. Aluno deu o pitch em voz alta (você não consegue ouvir, mas peça pra ele gravar e te resumir).
50
+ 2. Resumo em 3 frases: problema, solução, ask.
51
+ 3. Aluno se sentiu confortável com a estrutura (pergunte explicitamente).
52
+ 4. Tempo estimado ≤ tempo da tarefa.
53
+
54
+ ## Sempre
55
+
56
+ - Antes de marcar, mostre ao aluno: "vou marcar com esse resumo — confere?" e use o resumo como \`summary\` do \`mark_task_complete\`.
57
+ - Anexe um \`transcript_excerpt\` curto (3-5 linhas das mensagens mais relevantes da tarefa) se possível.
58
+ - Se o aluno disser "acabei" mas faltar item do checklist, NÃO marque. Mostre o que falta. Se ele insistir após ver, marque — coaching, não tribunal.
59
+ - Se a tarefa foi reaberta pelo instrutor, releia o \`reason\` (vem via UserPromptSubmit hook) antes de pedir nova evidência.
60
+ `;
61
+ }