paris-immersion 0.1.9 → 0.1.11
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/serve.js +40 -21
- package/dist/index.js +1 -1
- package/dist/templates/hook.js +37 -0
- package/package.json +1 -1
package/dist/commands/serve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { promises as fs, chmodSync, existsSync, statSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { readCredentials } from '../lib/credentials.js';
|
|
6
7
|
import { ensureWorkspace } from './install.js';
|
|
@@ -82,32 +83,50 @@ export function resolveKickoff(cwd) {
|
|
|
82
83
|
const override = process.env.PARIS_COACH_KICKOFF;
|
|
83
84
|
if (override)
|
|
84
85
|
return override;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
files.find((f) => f.toLowerCase().includes(slug));
|
|
98
|
-
if (match)
|
|
99
|
-
chosen = match;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
/* sem workspace.json legível — usa o primeiro */
|
|
86
|
+
// O Claude Code carrega slash-commands de DOIS lugares: o do projeto
|
|
87
|
+
// (workspace) e o global do usuário (~/.claude/commands). O `@tostudy-ai/cli`
|
|
88
|
+
// instala o comando do tutor em qualquer um deles (na prática, no global) —
|
|
89
|
+
// então varremos os DOIS. Sem isto, numa máquina onde o tostudy escreve só no
|
|
90
|
+
// global, o kickoff não acharia o comando e cairia em `/tostudy` (Unknown).
|
|
91
|
+
const dirs = [path.join(cwd, '.claude', 'commands'), path.join(homedir(), '.claude', 'commands')];
|
|
92
|
+
const names = new Set();
|
|
93
|
+
for (const d of dirs) {
|
|
94
|
+
try {
|
|
95
|
+
for (const f of readdirSync(d)) {
|
|
96
|
+
if (/^tostudy.*\.md$/i.test(f))
|
|
97
|
+
names.add(f.replace(/\.md$/i, ''));
|
|
104
98
|
}
|
|
105
99
|
}
|
|
106
|
-
|
|
100
|
+
catch {
|
|
101
|
+
/* diretório não existe — ok */
|
|
102
|
+
}
|
|
107
103
|
}
|
|
108
|
-
|
|
104
|
+
if (names.size === 0)
|
|
109
105
|
return '/tostudy';
|
|
106
|
+
const all = [...names];
|
|
107
|
+
// Curso ATIVO deste workspace = fonte da verdade pra escolher entre vários cursos.
|
|
108
|
+
let slug = '';
|
|
109
|
+
try {
|
|
110
|
+
const ws = JSON.parse(readFileSync(path.join(cwd, '.tostudy', 'workspace.json'), 'utf8'));
|
|
111
|
+
slug = (ws.slug ?? '').toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* sem workspace.json — segue sem slug */
|
|
115
|
+
}
|
|
116
|
+
if (slug) {
|
|
117
|
+
const exact = all.find((n) => n.toLowerCase() === `tostudy-${slug}`);
|
|
118
|
+
if (exact)
|
|
119
|
+
return '/' + exact;
|
|
120
|
+
const partial = all.find((n) => n.toLowerCase().includes(slug));
|
|
121
|
+
if (partial)
|
|
122
|
+
return '/' + partial;
|
|
110
123
|
}
|
|
124
|
+
// Sem match por slug: prefere um comando ESPECÍFICO de curso (`tostudy-<algo>`)
|
|
125
|
+
// ao genérico `tostudy`. Se só houver o genérico, usa `/tostudy`.
|
|
126
|
+
const courseSpecific = all.filter((n) => /^tostudy-.+/i.test(n)).sort();
|
|
127
|
+
if (courseSpecific.length)
|
|
128
|
+
return '/' + courseSpecific[0];
|
|
129
|
+
return '/tostudy';
|
|
111
130
|
}
|
|
112
131
|
/**
|
|
113
132
|
* Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { daemonCommand } from './commands/daemon.js';
|
|
|
13
13
|
const program = new Command()
|
|
14
14
|
.name('paris')
|
|
15
15
|
.description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
|
|
16
|
-
.version('0.1.
|
|
16
|
+
.version('0.1.11');
|
|
17
17
|
program
|
|
18
18
|
.command('login')
|
|
19
19
|
.description('Autenticar com sua conta Paris (device-code OR --token direto)')
|
package/dist/templates/hook.js
CHANGED
|
@@ -16,6 +16,7 @@ export function renderHookScript() {
|
|
|
16
16
|
import { promises as fs } from 'node:fs'
|
|
17
17
|
import { homedir } from 'node:os'
|
|
18
18
|
import path from 'node:path'
|
|
19
|
+
import { execFile } from 'node:child_process'
|
|
19
20
|
|
|
20
21
|
const PHASE = process.argv[2] // session-start | user-prompt-submit | stop
|
|
21
22
|
|
|
@@ -58,6 +59,33 @@ async function apiPost(apiUrl, pathname, token, body) {
|
|
|
58
59
|
return res.json().catch(() => null)
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function runCmd(cmd, args, cwd) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
try {
|
|
65
|
+
execFile(cmd, args, { cwd, timeout: 8000, windowsHide: true }, (err, stdout) => {
|
|
66
|
+
resolve(err ? null : stdout)
|
|
67
|
+
})
|
|
68
|
+
} catch { resolve(null) }
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Sincroniza o progresso REAL do curso da ToStudy pro nosso lado. O aluno faz o
|
|
73
|
+
// curso pela tostudy CLI no terminal embutido, e essa conclusão NÃO dispara o
|
|
74
|
+
// webhook do cursos — então é daqui que a gestão aprende o progresso. A ToStudy
|
|
75
|
+
// só expõe % e contagem concluída; o total do curso é derivado no servidor.
|
|
76
|
+
// Best-effort: se a tostudy não estiver logada / sem curso ativo, vira no-op.
|
|
77
|
+
async function syncTostudyProgress(apiUrl, token, cwd) {
|
|
78
|
+
const out = await runCmd('tostudy', ['progress', '--json'], cwd)
|
|
79
|
+
if (!out) return
|
|
80
|
+
let p
|
|
81
|
+
try { p = JSON.parse(out) } catch { return }
|
|
82
|
+
if (!p || typeof p.completedLessons !== 'number') return
|
|
83
|
+
await apiPost(apiUrl, '/immersion-app/v1/course/tostudy-progress', token, {
|
|
84
|
+
completedLessons: p.completedLessons,
|
|
85
|
+
coursePercent: typeof p.coursePercent === 'number' ? p.coursePercent : 0,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
async function main() {
|
|
62
90
|
const marker = await loadMarker()
|
|
63
91
|
if (!marker) return // not a paris workspace — silent no-op
|
|
@@ -73,6 +101,9 @@ async function main() {
|
|
|
73
101
|
|
|
74
102
|
if (PHASE === 'stop') {
|
|
75
103
|
await apiPost(apiUrl, '/immersion-app/v1/events', creds.token, { type: 'turn_ended' })
|
|
104
|
+
// Logo após cada interação é o melhor momento pra capturar uma lição que o
|
|
105
|
+
// aluno acabou de concluir no curso da ToStudy.
|
|
106
|
+
await syncTostudyProgress(apiUrl, creds.token, marker.dir)
|
|
76
107
|
return
|
|
77
108
|
}
|
|
78
109
|
|
|
@@ -141,6 +172,12 @@ async function main() {
|
|
|
141
172
|
if (PHASE === 'session-start') {
|
|
142
173
|
await apiPost(apiUrl, '/immersion-app/v1/events', creds.token, { type: 'session_started' })
|
|
143
174
|
}
|
|
175
|
+
|
|
176
|
+
// Na fase do curso, sincroniza o progresso da ToStudy também ao abrir/retomar
|
|
177
|
+
// a sessão (não só no stop), pra gestão refletir mesmo entre interações.
|
|
178
|
+
if (inCourse) {
|
|
179
|
+
await syncTostudyProgress(apiUrl, creds.token, marker.dir)
|
|
180
|
+
}
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
main().catch((err) => {
|
package/package.json
CHANGED