paris-immersion 0.1.8 → 0.1.10

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.
@@ -38,9 +38,19 @@ export interface ServeHandle {
38
38
  close: () => void;
39
39
  }
40
40
  /**
41
- * Spawn the coached pty (`claude '/tostudy'` inside the provisioned workspace).
42
- * Shared by the local ws server and the relay agent. Throws if node-pty can't
43
- * load/spawn — callers report it on their channel.
41
+ * Descobre o slash-command do tutor pra injetar no kickoff. O `@tostudy-ai/cli`
42
+ * instala o comando com nome ESPECÍFICO do curso (`.claude/commands/tostudy-<slug>.md`),
43
+ * NÃO `/tostudy` — então cravar `/tostudy` dava "Unknown command: /tostudy" e o
44
+ * curso não iniciava. Aqui varremos `.claude/commands/tostudy*.md` e usamos o
45
+ * comando real; com vários cursos, casamos com o `slug` ativo do
46
+ * `.tostudy/workspace.json`. Override explícito via PARIS_COACH_KICKOFF.
47
+ * Fallback `/tostudy` (legado / antes do `tostudy` instalar o comando).
48
+ */
49
+ export declare function resolveKickoff(cwd: string): string;
50
+ /**
51
+ * Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
52
+ * workspace). Shared by the local ws server and the relay agent. Throws if
53
+ * node-pty can't load/spawn — callers report it on their channel.
44
54
  */
45
55
  export declare function spawnCoachPty(cwd: string): IPty;
46
56
  /**
@@ -1,6 +1,7 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
  import { createRequire } from 'node:module';
3
- import { promises as fs, chmodSync, existsSync, statSync } from 'node:fs';
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';
@@ -69,20 +70,78 @@ const DEFAULT_ALLOWED_ORIGINS = [
69
70
  // `/tostudy` roda o curso real direto. Override com PARIS_COACH_KICKOFF se uma
70
71
  // turma precisar de outro entrypoint (ex.: o tutor-paris `get_course` como
71
72
  // fallback quando não há curso ToStudy configurado).
72
- const COACH_KICKOFF = process.env.PARIS_COACH_KICKOFF || '/tostudy';
73
73
  /**
74
- * Spawn the coached pty (`claude '/tostudy'` inside the provisioned workspace).
75
- * Shared by the local ws server and the relay agent. Throws if node-pty can't
76
- * load/spawn — callers report it on their channel.
74
+ * Descobre o slash-command do tutor pra injetar no kickoff. O `@tostudy-ai/cli`
75
+ * instala o comando com nome ESPECÍFICO do curso (`.claude/commands/tostudy-<slug>.md`),
76
+ * NÃO `/tostudy` — então cravar `/tostudy` dava "Unknown command: /tostudy" e o
77
+ * curso não iniciava. Aqui varremos `.claude/commands/tostudy*.md` e usamos o
78
+ * comando real; com vários cursos, casamos com o `slug` ativo do
79
+ * `.tostudy/workspace.json`. Override explícito via PARIS_COACH_KICKOFF.
80
+ * Fallback `/tostudy` (legado / antes do `tostudy` instalar o comando).
81
+ */
82
+ export function resolveKickoff(cwd) {
83
+ const override = process.env.PARIS_COACH_KICKOFF;
84
+ if (override)
85
+ return override;
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, ''));
98
+ }
99
+ }
100
+ catch {
101
+ /* diretório não existe — ok */
102
+ }
103
+ }
104
+ if (names.size === 0)
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;
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';
130
+ }
131
+ /**
132
+ * Spawn the coached pty (`claude '/tostudy-<curso>'` inside the provisioned
133
+ * workspace). Shared by the local ws server and the relay agent. Throws if
134
+ * node-pty can't load/spawn — callers report it on their channel.
77
135
  */
78
136
  export function spawnCoachPty(cwd) {
79
137
  const req = createRequire(import.meta.url);
80
138
  ensureSpawnHelperExecutable(req);
81
139
  const ptyMod = req('node-pty');
140
+ const kickoff = resolveKickoff(cwd);
82
141
  const shell = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : 'bash');
83
142
  const shellArgs = process.platform === 'win32'
84
- ? ['-NoLogo', '-Command', `claude "${COACH_KICKOFF.replace(/"/g, '`"')}"`]
85
- : ['-l', '-c', `claude '${COACH_KICKOFF.replace(/'/g, `'\\''`)}'`];
143
+ ? ['-NoLogo', '-Command', `claude "${kickoff.replace(/"/g, '`"')}"`]
144
+ : ['-l', '-c', `claude '${kickoff.replace(/'/g, `'\\''`)}'`];
86
145
  return ptyMod.spawn(shell, shellArgs, {
87
146
  name: 'xterm-color',
88
147
  cols: 80,
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.8');
16
+ .version('0.1.10');
17
17
  program
18
18
  .command('login')
19
19
  .description('Autenticar com sua conta Paris (device-code OR --token direto)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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": {