paris-immersion 0.1.0 → 0.1.2
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.d.ts +13 -0
- package/dist/commands/install.js +46 -18
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +117 -6
- package/dist/templates/hook.js +33 -15
- package/dist/templates/settings.js +5 -1
- package/package.json +1 -1
- package/templates/mcp/index.mjs +12 -0
|
@@ -2,3 +2,16 @@ export declare function installCommand(opts: {
|
|
|
2
2
|
dir?: string;
|
|
3
3
|
immersion?: string;
|
|
4
4
|
}): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Locate the student's workspace, or create one if none exists. Used by
|
|
7
|
+
* `paris serve` so the onboarding embedded terminal always opens a *coached*
|
|
8
|
+
* Claude session — the student never has to know `paris install` exists. This
|
|
9
|
+
* is what guarantees the "magic moment": open the terminal, Claude greets you
|
|
10
|
+
* with today's mission. Returns the workspace dir + whether it was just created.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureWorkspace(opts?: {
|
|
13
|
+
immersion?: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
dir: string;
|
|
16
|
+
created: boolean;
|
|
17
|
+
}>;
|
package/dist/commands/install.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { apiAuthFetch } from '../lib/api.js';
|
|
@@ -22,14 +23,50 @@ export async function installCommand(opts) {
|
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
25
|
process.stdout.write(c.dim('Buscando contexto da imersão...\n'));
|
|
25
|
-
const
|
|
26
|
-
const
|
|
26
|
+
const boot = await bootstrapWorkspace(opts.immersion);
|
|
27
|
+
const targetDir = path.resolve(opts.dir ?? `paris-${boot.immersion.slug}`);
|
|
28
|
+
await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(c.green(`✓ Workspace pronto em ${targetDir}`));
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(c.bold('Próximo passo:'));
|
|
33
|
+
console.log(` cd ${path.relative(process.cwd(), targetDir) || '.'}`);
|
|
34
|
+
console.log(' claude');
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(c.dim(`Imersão: ${boot.immersion.name} · fase ${boot.phase} · dia ${boot.day_number}`));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Locate the student's workspace, or create one if none exists. Used by
|
|
40
|
+
* `paris serve` so the onboarding embedded terminal always opens a *coached*
|
|
41
|
+
* Claude session — the student never has to know `paris install` exists. This
|
|
42
|
+
* is what guarantees the "magic moment": open the terminal, Claude greets you
|
|
43
|
+
* with today's mission. Returns the workspace dir + whether it was just created.
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureWorkspace(opts = {}) {
|
|
46
|
+
const existing = await findMarker();
|
|
47
|
+
if (existing)
|
|
48
|
+
return { dir: existing.dir, created: false };
|
|
49
|
+
const creds = await readCredentials();
|
|
50
|
+
if (!creds) {
|
|
51
|
+
throw new Error('Você precisa rodar `paris login` antes.');
|
|
52
|
+
}
|
|
53
|
+
const boot = await bootstrapWorkspace(opts.immersion);
|
|
54
|
+
// Predictable, findable location regardless of where `paris serve` was run.
|
|
55
|
+
const targetDir = path.join(homedir(), `paris-${boot.immersion.slug}`);
|
|
56
|
+
await writeWorkspaceFiles(targetDir, boot, creds.api_url, creds.email);
|
|
57
|
+
return { dir: targetDir, created: true };
|
|
58
|
+
}
|
|
59
|
+
async function bootstrapWorkspace(immersion) {
|
|
60
|
+
const slugQuery = immersion ? `?immersion=${encodeURIComponent(immersion)}` : '';
|
|
61
|
+
return apiAuthFetch(`/immersion-app/v1/workspace/bootstrap${slugQuery}`, {
|
|
27
62
|
method: 'POST',
|
|
28
63
|
});
|
|
29
|
-
|
|
64
|
+
}
|
|
65
|
+
/** Write the full workspace layout (marker, hooks, vendored MCP, settings, skill, CLAUDE.md). */
|
|
66
|
+
async function writeWorkspaceFiles(targetDir, boot, apiUrl, email) {
|
|
30
67
|
await fs.mkdir(targetDir, { recursive: true });
|
|
31
68
|
// .paris/workspace.json — the marker.
|
|
32
|
-
await writeMarker(targetDir, { ...boot.marker, api_url:
|
|
69
|
+
await writeMarker(targetDir, { ...boot.marker, api_url: apiUrl });
|
|
33
70
|
// .paris/hooks/paris-hook.mjs — installed Claude Code hook.
|
|
34
71
|
const hooksDir = path.join(targetDir, '.paris', 'hooks');
|
|
35
72
|
await fs.mkdir(hooksDir, { recursive: true });
|
|
@@ -40,13 +77,12 @@ export async function installCommand(opts) {
|
|
|
40
77
|
const mcpDir = path.join(targetDir, '.paris', 'mcp');
|
|
41
78
|
await fs.mkdir(mcpDir, { recursive: true });
|
|
42
79
|
await fs.writeFile(path.join(mcpDir, 'index.mjs'), await readVendoredMcpBundle(), { mode: 0o755 });
|
|
43
|
-
// .claude/settings.json — wires hooks + MCP server.
|
|
80
|
+
// .claude/settings.json — wires hooks + MCP server. Both settings.json (for
|
|
81
|
+
// interactive `claude`) and settings.local.json (the source `claude -p`
|
|
82
|
+
// reliably loads hooks from).
|
|
44
83
|
const claudeDir = path.join(targetDir, '.claude');
|
|
45
84
|
await fs.mkdir(claudeDir, { recursive: true });
|
|
46
|
-
|
|
47
|
-
// and settings.local.json (so `claude -p` does too — in -p mode, local
|
|
48
|
-
// settings are the reliably-loaded source for hooks).
|
|
49
|
-
const settingsPayload = renderSettingsJson({ apiUrl: creds.api_url });
|
|
85
|
+
const settingsPayload = renderSettingsJson({ apiUrl });
|
|
50
86
|
await fs.writeFile(path.join(claudeDir, 'settings.json'), JSON.stringify(settingsPayload, null, 2));
|
|
51
87
|
await fs.writeFile(path.join(claudeDir, 'settings.local.json'), JSON.stringify(settingsPayload, null, 2));
|
|
52
88
|
// .claude/skills/paris-coach/SKILL.md
|
|
@@ -57,16 +93,8 @@ export async function installCommand(opts) {
|
|
|
57
93
|
await fs.writeFile(path.join(targetDir, 'CLAUDE.md'), renderClaudeMd({
|
|
58
94
|
immersionName: boot.immersion.name,
|
|
59
95
|
immersionSlug: boot.immersion.slug,
|
|
60
|
-
participantEmail:
|
|
96
|
+
participantEmail: email,
|
|
61
97
|
}));
|
|
62
|
-
console.log();
|
|
63
|
-
console.log(c.green(`✓ Workspace pronto em ${targetDir}`));
|
|
64
|
-
console.log();
|
|
65
|
-
console.log(c.bold('Próximo passo:'));
|
|
66
|
-
console.log(` cd ${path.relative(process.cwd(), targetDir) || '.'}`);
|
|
67
|
-
console.log(' claude');
|
|
68
|
-
console.log();
|
|
69
|
-
console.log(c.dim(`Imersão: ${boot.immersion.name} · fase ${boot.phase} · dia ${boot.day_number}`));
|
|
70
98
|
}
|
|
71
99
|
/**
|
|
72
100
|
* Reads the bundled MCP server shipped with this package. Lives at
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export interface ServeOptions {
|
|
2
2
|
port: number;
|
|
3
3
|
allowedOrigins: string[];
|
|
4
|
+
/** The login token from ~/.paris/credentials. Accepted on the WS handshake for
|
|
5
|
+
* backward compatibility (legacy: web sent the login token directly). */
|
|
4
6
|
token: string;
|
|
7
|
+
/** API base URL — used to verify serve-tokens the browser sends (the CLI has no
|
|
8
|
+
* JWT_SECRET, so it delegates verification to POST /api/onboarding/v2/serve-verify). */
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
/** When set, a verified serve-token must resolve to this user/participant —
|
|
11
|
+
* read from the workspace marker so one machine's serve can't be driven by a
|
|
12
|
+
* token minted for a different student. */
|
|
13
|
+
expectedUserId?: string;
|
|
14
|
+
expectedParticipantId?: string;
|
|
15
|
+
/** Working directory the embedded `claude` runs in. Defaults to process.cwd().
|
|
16
|
+
* serveCommand sets this to the provisioned workspace so the session is coached. */
|
|
17
|
+
cwd?: string;
|
|
5
18
|
/** When true, accept connections without spawning a pty — used by tests + dry-run handshake checks. */
|
|
6
19
|
dryRun?: boolean;
|
|
7
20
|
}
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,12 +1,74 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { promises as fs, chmodSync, existsSync, statSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { readCredentials } from '../lib/credentials.js';
|
|
6
|
+
import { ensureWorkspace } from './install.js';
|
|
4
7
|
import { c } from '../lib/colors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Restore the executable bit on node-pty's `spawn-helper`. Some install paths
|
|
10
|
+
* (pnpm store hardlinks, tarball extraction without same-permissions, certain
|
|
11
|
+
* CI caches) drop the +x bit on the prebuilt helper — and without it node-pty's
|
|
12
|
+
* posix_spawnp fails at runtime with "posix_spawnp failed", killing the embedded
|
|
13
|
+
* terminal on EVERY launch. We can't depend on the package shipping it correctly,
|
|
14
|
+
* so we fix it best-effort at startup. No-op on Windows (no spawn-helper there).
|
|
15
|
+
*/
|
|
16
|
+
function ensureSpawnHelperExecutable(req) {
|
|
17
|
+
if (process.platform === 'win32')
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
const main = req.resolve('node-pty'); // <pkg>/lib/index.js
|
|
21
|
+
const helper = path.resolve(path.dirname(main), '..', 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper');
|
|
22
|
+
if (existsSync(helper) && !(statSync(helper).mode & 0o111)) {
|
|
23
|
+
chmodSync(helper, 0o755);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* best-effort — if we can't locate it, the spawn error path still reports cleanly */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Verify a serve-token the browser sent on the WS handshake. The CLI can't check
|
|
32
|
+
* the JWT signature locally (no JWT_SECRET), so it asks the API. Returns true only
|
|
33
|
+
* if the API confirms the token AND it resolves to the workspace's own student.
|
|
34
|
+
*/
|
|
35
|
+
async function verifyServeToken(token, opts) {
|
|
36
|
+
if (!token || !opts.apiUrl)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${opts.apiUrl}/api/onboarding/v2/serve-verify`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ token }),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return false;
|
|
46
|
+
const data = (await res.json());
|
|
47
|
+
if (!data.valid)
|
|
48
|
+
return false;
|
|
49
|
+
if (opts.expectedUserId && data.userId && data.userId !== opts.expectedUserId)
|
|
50
|
+
return false;
|
|
51
|
+
if (opts.expectedParticipantId && data.participantId && data.participantId !== opts.expectedParticipantId)
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
5
59
|
const DEFAULT_ALLOWED_ORIGINS = [
|
|
6
60
|
'https://app.paris-group.com',
|
|
7
61
|
'https://paris-immersion.vercel.app',
|
|
8
62
|
'http://localhost:3010',
|
|
9
63
|
];
|
|
64
|
+
// Initial prompt fed to `claude` when the embedded terminal opens, so the course
|
|
65
|
+
// starts immediately instead of sitting at an idle prompt. Invoca a skill do
|
|
66
|
+
// tutor ToStudy (`/tostudy`, do @tostudy-ai/cli) — o aluno chega aqui já logado
|
|
67
|
+
// na conta dele da ToStudy (auto-login no install via cli-auth-code), então o
|
|
68
|
+
// `/tostudy` roda o curso real direto. Override com PARIS_COACH_KICKOFF se uma
|
|
69
|
+
// turma precisar de outro entrypoint (ex.: o tutor-paris `get_course` como
|
|
70
|
+
// fallback quando não há curso ToStudy configurado).
|
|
71
|
+
const COACH_KICKOFF = process.env.PARIS_COACH_KICKOFF || '/tostudy';
|
|
10
72
|
/**
|
|
11
73
|
* Start a localhost WebSocket server that brokers a pty (running `claude`) to a
|
|
12
74
|
* connected browser-side xterm.js. Two gates protect the channel:
|
|
@@ -32,11 +94,18 @@ export async function startServeServer(opts) {
|
|
|
32
94
|
return;
|
|
33
95
|
}
|
|
34
96
|
const url = new URL(info.req.url ?? '/', 'http://localhost');
|
|
35
|
-
|
|
36
|
-
|
|
97
|
+
const qToken = url.searchParams.get('token') ?? '';
|
|
98
|
+
// Fast path: the login token (creds.token) — backward compatible with
|
|
99
|
+
// older web clients that sent it directly.
|
|
100
|
+
if (qToken && qToken === opts.token) {
|
|
101
|
+
cb(true);
|
|
37
102
|
return;
|
|
38
103
|
}
|
|
39
|
-
|
|
104
|
+
// Otherwise treat it as a serve-token (purpose 'onboarding-serve', minted by
|
|
105
|
+
// GET /api/onboarding/v2/serve-token) and let the API verify it.
|
|
106
|
+
void verifyServeToken(qToken, opts).then((ok) => {
|
|
107
|
+
cb(ok, ok ? undefined : 401, ok ? undefined : 'invalid token');
|
|
108
|
+
});
|
|
40
109
|
},
|
|
41
110
|
});
|
|
42
111
|
// Wait for the listening event so handle.port is final (random port if 0).
|
|
@@ -55,14 +124,22 @@ export async function startServeServer(opts) {
|
|
|
55
124
|
// Lazy require — keeps `paris serve --help` working even if the native
|
|
56
125
|
// node-pty binary failed to compile/install on this platform.
|
|
57
126
|
const req = createRequire(import.meta.url);
|
|
127
|
+
ensureSpawnHelperExecutable(req);
|
|
58
128
|
const ptyMod = req('node-pty');
|
|
59
129
|
const shell = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : 'bash');
|
|
60
|
-
|
|
130
|
+
// Auto-start the coach: launch `claude` with an initial prompt so the
|
|
131
|
+
// student opens the terminal to Claude ALREADY running the course (it
|
|
132
|
+
// speaks first), instead of an idle prompt. The SessionStart hook has
|
|
133
|
+
// already injected the immersion/day/task context; the kickoff just tells
|
|
134
|
+
// Claude to begin now without waiting for input.
|
|
135
|
+
const shellArgs = process.platform === 'win32'
|
|
136
|
+
? ['-NoLogo', '-Command', `claude "${COACH_KICKOFF.replace(/"/g, '`"')}"`]
|
|
137
|
+
: ['-l', '-c', `claude '${COACH_KICKOFF.replace(/'/g, `'\\''`)}'`];
|
|
61
138
|
pty = ptyMod.spawn(shell, shellArgs, {
|
|
62
139
|
name: 'xterm-color',
|
|
63
140
|
cols: 80,
|
|
64
141
|
rows: 24,
|
|
65
|
-
cwd: process.cwd(),
|
|
142
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
66
143
|
env: {
|
|
67
144
|
...process.env,
|
|
68
145
|
TOSTUDY_MCP_ENABLED: '1',
|
|
@@ -156,9 +233,43 @@ export async function serveCommand(opts) {
|
|
|
156
233
|
process.exit(1);
|
|
157
234
|
}
|
|
158
235
|
const allowedOrigins = (process.env.PARIS_SERVE_ORIGINS?.split(',').map((s) => s.trim()).filter(Boolean)) ?? DEFAULT_ALLOWED_ORIGINS;
|
|
236
|
+
// Guarantee the embedded `claude` opens inside a provisioned (coached)
|
|
237
|
+
// workspace. If the student already ran `paris install`, we reuse it; else we
|
|
238
|
+
// bootstrap one now so the onboarding terminal is never vanilla Claude. A
|
|
239
|
+
// failure here is non-fatal — we fall back to cwd so the terminal still works.
|
|
240
|
+
let cwd = process.cwd();
|
|
241
|
+
try {
|
|
242
|
+
const { dir, created } = await ensureWorkspace({});
|
|
243
|
+
cwd = dir;
|
|
244
|
+
console.log(created
|
|
245
|
+
? ` ${c.green('✓')} workspace criado em ${c.bold(dir)}`
|
|
246
|
+
: ` ${c.dim('workspace:')} ${dir}`);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
console.error(c.yellow(` ! sem workspace coachado (${msg}). Abrindo Claude em ${cwd}.`));
|
|
251
|
+
}
|
|
252
|
+
// Read api_url + the workspace's owner so we can verify serve-tokens (and bind
|
|
253
|
+
// them to this student). Best-effort — falls back to login-token-only auth.
|
|
254
|
+
let apiUrl = '';
|
|
255
|
+
let expectedUserId;
|
|
256
|
+
let expectedParticipantId;
|
|
257
|
+
try {
|
|
258
|
+
const creds = await readCredentials();
|
|
259
|
+
apiUrl = creds?.api_url ?? '';
|
|
260
|
+
const markerRaw = await fs.readFile(path.join(cwd, '.paris', 'workspace.json'), 'utf8');
|
|
261
|
+
const marker = JSON.parse(markerRaw);
|
|
262
|
+
expectedUserId = marker.student_user_id;
|
|
263
|
+
expectedParticipantId = marker.participant_id;
|
|
264
|
+
if (!apiUrl && marker.api_url)
|
|
265
|
+
apiUrl = marker.api_url;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* best-effort — verifyServeToken just no-ops without apiUrl */
|
|
269
|
+
}
|
|
159
270
|
let handle;
|
|
160
271
|
try {
|
|
161
|
-
handle = await startServeServer({ port: portNum, allowedOrigins, token });
|
|
272
|
+
handle = await startServeServer({ port: portNum, allowedOrigins, token, cwd, apiUrl, expectedUserId, expectedParticipantId });
|
|
162
273
|
}
|
|
163
274
|
catch (err) {
|
|
164
275
|
const msg = err instanceof Error ? err.message : String(err);
|
package/dist/templates/hook.js
CHANGED
|
@@ -76,9 +76,10 @@ async function main() {
|
|
|
76
76
|
return
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const [workspace, current] = await Promise.all([
|
|
79
|
+
const [workspace, current, course] = await Promise.all([
|
|
80
80
|
apiGet(apiUrl, '/immersion-app/v1/workspace', creds.token),
|
|
81
81
|
apiGet(apiUrl, '/immersion-app/v1/missions/current', creds.token),
|
|
82
|
+
apiGet(apiUrl, '/immersion-app/v1/course', creds.token),
|
|
82
83
|
])
|
|
83
84
|
|
|
84
85
|
if (!workspace) {
|
|
@@ -87,6 +88,11 @@ async function main() {
|
|
|
87
88
|
}
|
|
88
89
|
if (workspace.status === 'graduated') return
|
|
89
90
|
|
|
91
|
+
// Onboarding: enquanto o curso não termina, o terminal roda o tutor do curso
|
|
92
|
+
// (/tostudy) — isso tem prioridade sobre as missões da imersão (que nem existem
|
|
93
|
+
// na fase pre).
|
|
94
|
+
const inCourse = !!(course && course.courseCompleted === false)
|
|
95
|
+
|
|
90
96
|
const lines = []
|
|
91
97
|
if (PHASE === 'session-start') {
|
|
92
98
|
lines.push('# Paris Immersion — contexto da sessão')
|
|
@@ -94,28 +100,40 @@ async function main() {
|
|
|
94
100
|
lines.push('Imersão: **' + workspace.immersion.name + '** (\\'' + workspace.immersion.slug + '\\')')
|
|
95
101
|
lines.push('Dia: **' + workspace.day_number + '** · fase: **' + workspace.phase + '**')
|
|
96
102
|
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
103
|
} else {
|
|
100
104
|
lines.push('# Paris — refresh de contexto')
|
|
101
105
|
lines.push('')
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
if (
|
|
105
|
-
lines.push('##
|
|
106
|
-
lines.push('
|
|
107
|
-
lines.push('
|
|
108
|
-
|
|
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) {
|
|
108
|
+
if (inCourse) {
|
|
109
|
+
lines.push('## FASE DO CURSO DE ONBOARDING')
|
|
110
|
+
lines.push('O aluno está na etapa do curso **' + course.title + '** (progresso: **' + course.lessonsCompleted + '/' + course.totalLessons + '**).')
|
|
111
|
+
lines.push('Rode a skill **/tostudy** (o tutor da ToStudy) pra conduzir o curso — o aluno já chega logado na conta dele da ToStudy (auto-login no install). NÃO trabalhe em missões da imersão enquanto o curso não terminar. (Fallback: se a ToStudy não estiver disponível, use as ferramentas MCP **get_course**/**complete_lesson** do paris.)')
|
|
112
|
+
if (PHASE === 'session-start') {
|
|
112
113
|
lines.push('')
|
|
113
|
-
lines.push('
|
|
114
|
-
lines.push(current.current.socio_prompt)
|
|
114
|
+
lines.push('**Comece falando primeiro**: rode /tostudy e dê as boas-vindas. O aluno ainda não digitou nada.')
|
|
115
115
|
}
|
|
116
116
|
} else {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
if (PHASE === 'session-start') {
|
|
118
|
+
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.')
|
|
119
|
+
lines.push('')
|
|
120
|
+
}
|
|
121
|
+
if (current && current.current) {
|
|
122
|
+
lines.push('## Tarefa atual')
|
|
123
|
+
lines.push('- **id:** \\'' + current.current.id + '\\'')
|
|
124
|
+
lines.push('- **título:** ' + current.current.title)
|
|
125
|
+
lines.push('- **tipo:** ' + current.current.deliverable_type)
|
|
126
|
+
lines.push('- **status:** ' + current.current.status)
|
|
127
|
+
if (current.current.description) lines.push('- **descrição:** ' + current.current.description)
|
|
128
|
+
if (current.current.socio_prompt) {
|
|
129
|
+
lines.push('')
|
|
130
|
+
lines.push('### Sócio prompt')
|
|
131
|
+
lines.push(current.current.socio_prompt)
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
lines.push('## Sem tarefa em foco')
|
|
135
|
+
lines.push('Pergunte ao aluno em que ele quer trabalhar.')
|
|
136
|
+
}
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
process.stdout.write(lines.join('\\n') + '\\n')
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function renderSettingsJson(opts) {
|
|
11
11
|
return {
|
|
12
|
-
$schema: 'https://json.schemastore.org/claude-code-settings',
|
|
12
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
13
|
+
// Auto-confia no MCP do projeto (paris-immersion) — sem isso o Claude Code
|
|
14
|
+
// pode não conectar o server numa sessão não-interativa (terminal embutido),
|
|
15
|
+
// e o tutor abre sem as tools get_course/complete_lesson.
|
|
16
|
+
enableAllProjectMcpServers: true,
|
|
13
17
|
mcpServers: {
|
|
14
18
|
'paris-immersion': {
|
|
15
19
|
command: 'node',
|
package/package.json
CHANGED
package/templates/mcp/index.mjs
CHANGED
|
@@ -18024,6 +18024,18 @@ function buildTools() {
|
|
|
18024
18024
|
transcript_excerpt: input.transcript_excerpt
|
|
18025
18025
|
})
|
|
18026
18026
|
},
|
|
18027
|
+
{
|
|
18028
|
+
name: "get_course",
|
|
18029
|
+
description: "Retorna o curso de onboarding (Terminal + Claude Code): li\xE7\xF5es, objetivos e o progresso do aluno. Use NO IN\xCDCIO pra saber o que ensinar e de onde retomar.",
|
|
18030
|
+
inputSchema: external_exports.object({}),
|
|
18031
|
+
handler: async (_, { client }) => client.get("/immersion-app/v1/course")
|
|
18032
|
+
},
|
|
18033
|
+
{
|
|
18034
|
+
name: "complete_lesson",
|
|
18035
|
+
description: "Marca uma li\xE7\xE3o do curso de onboarding como conclu\xEDda. Progresso \xE9 MONOT\xD4NICO (nunca regride) e reconcilia com a conta ToStudy do aluno pelo e-mail. S\xD3 chame quando o aluno realmente cumprir o objetivo da li\xE7\xE3o. Quando a \xFAltima fecha, o curso conclui e a fase avan\xE7a.",
|
|
18036
|
+
inputSchema: external_exports.object({ lesson_id: external_exports.string().min(1) }),
|
|
18037
|
+
handler: async (input, { client }) => client.post(`/immersion-app/v1/course/lessons/${input.lesson_id}/complete`, {})
|
|
18038
|
+
},
|
|
18027
18039
|
{
|
|
18028
18040
|
name: "reopen_task",
|
|
18029
18041
|
description: "Reabrir uma tarefa marcada como conclu\xEDda. O aluno (voc\xEA) precisa de uma raz\xE3o.",
|