@keeroklab/cli 0.5.1 → 0.6.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.
package/README.md CHANGED
@@ -6,16 +6,32 @@ Students connect to a supervised session where the instructor can monitor termin
6
6
 
7
7
  ## Quick Start
8
8
 
9
+ Your instructor will provide the `<token>` via email or the session page.
10
+
11
+ ### macOS / Linux (no install needed)
12
+
9
13
  ```bash
10
- npx @keeroklab/cli connect <token> --server https://keerok.tech
14
+ curl -fsSL https://keerok.tech/lab/install.sh | bash -s -- <TOKEN>
11
15
  ```
12
16
 
13
- Your instructor will provide the `<token>` via email or the session page.
17
+ This will automatically download Node.js if it's not installed on your machine.
18
+
19
+ ### Windows (PowerShell)
20
+
21
+ ```powershell
22
+ irm https://keerok.tech/lab/install.ps1 -OutFile keerok.ps1; .\keerok.ps1 <TOKEN>
23
+ ```
24
+
25
+ ### If you already have Node.js
26
+
27
+ ```bash
28
+ npx @keeroklab/cli connect <token> --server https://keerok.tech
29
+ ```
14
30
 
15
31
  ## Requirements
16
32
 
17
- - **Node.js 18+** — [Download](https://nodejs.org)
18
33
  - A terminal (macOS Terminal, iTerm2, Windows Terminal, etc.)
34
+ - Node.js 18+ is downloaded automatically if not present
19
35
 
20
36
  ## What happens when you connect
21
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keeroklab/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "CLI wrapper for Keerok Lab — live Claude Code supervision",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -12,6 +12,19 @@ program
12
12
  .description('CLI wrapper for Keerok Lab — live Claude Code supervision')
13
13
  .version('0.1.0');
14
14
 
15
+ /**
16
+ * Extract a Claude Code resume session ID from terminal output.
17
+ * Claude prints: "Resume this session with:\n claude --resume <uuid>"
18
+ * Output may contain ANSI escape codes from the PTY.
19
+ */
20
+ function extractResumeId(output) {
21
+ const clean = output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
22
+ const match = clean.match(
23
+ /--resume\s+([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/
24
+ );
25
+ return match ? match[1] : null;
26
+ }
27
+
15
28
  program
16
29
  .command('connect')
17
30
  .description('Connect to a lab session')
@@ -87,24 +100,50 @@ program
87
100
  console.log(`Starting: ${options.command}`);
88
101
  console.log('─'.repeat(50) + '\n');
89
102
 
90
- // Step 3: Spawn PTY with the command
91
- const pty = spawnPty(options.command, {
92
- onData: (data) => {
93
- // Forward terminal output to server
94
- ws.send({
95
- type: 'terminal_output',
96
- data: Buffer.from(data).toString('base64'),
97
- });
98
- },
99
- onResize: (cols, rows) => {
100
- ws.send({ type: 'terminal_resize', cols, rows });
101
- },
102
- onExit: (code) => {
103
- console.log(`\n\nProcess exited with code ${code}`);
104
- ws.close();
105
- process.exit(code);
106
- },
107
- });
103
+ // Step 3: Spawn PTY with resume support
104
+ let outputBuffer = '';
105
+ const MAX_BUFFER = 4096;
106
+ let currentPty = null;
107
+
108
+ function spawnClaude(resumeId = null) {
109
+ const cmd = resumeId
110
+ ? `${options.command} --resume ${resumeId}`
111
+ : options.command;
112
+ outputBuffer = '';
113
+
114
+ currentPty = spawnPty(cmd, {
115
+ onData: (data) => {
116
+ // Maintain rolling buffer for resume ID detection
117
+ outputBuffer += data;
118
+ if (outputBuffer.length > MAX_BUFFER) {
119
+ outputBuffer = outputBuffer.slice(-MAX_BUFFER);
120
+ }
121
+ // Forward terminal output to server
122
+ ws.send({
123
+ type: 'terminal_output',
124
+ data: Buffer.from(data).toString('base64'),
125
+ });
126
+ },
127
+ onResize: (cols, rows) => {
128
+ ws.send({ type: 'terminal_resize', cols, rows });
129
+ },
130
+ onExit: (code) => {
131
+ const detectedResumeId = extractResumeId(outputBuffer);
132
+
133
+ if (detectedResumeId) {
134
+ console.log('\n🔄 Resuming Claude Code session...\n');
135
+ ws.send({ type: 'session_resumed', resume_id: detectedResumeId });
136
+ setTimeout(() => spawnClaude(detectedResumeId), 1000);
137
+ } else {
138
+ console.log(`\n\nProcess exited with code ${code}`);
139
+ ws.close();
140
+ process.exit(code);
141
+ }
142
+ },
143
+ });
144
+ }
145
+
146
+ spawnClaude();
108
147
 
109
148
  // Heartbeat every 30s
110
149
  const heartbeat = setInterval(() => {
@@ -114,7 +153,7 @@ program
114
153
  // Handle SIGINT gracefully
115
154
  process.on('SIGINT', () => {
116
155
  clearInterval(heartbeat);
117
- pty.kill();
156
+ if (currentPty) currentPty.kill();
118
157
  ws.close();
119
158
  process.exit(0);
120
159
  });
@@ -31,30 +31,36 @@ export function spawnPty(command, handlers) {
31
31
  handlers.onData?.(data);
32
32
  });
33
33
 
34
- child.onExit(({ exitCode }) => {
35
- handlers.onExit?.(exitCode ?? 0);
36
- });
37
-
38
34
  // Forward stdin to child
35
+ const stdinHandler = (data) => {
36
+ child.write(data.toString());
37
+ };
38
+
39
39
  if (process.stdin.isTTY) {
40
40
  process.stdin.setRawMode(true);
41
41
  }
42
42
  process.stdin.resume();
43
- process.stdin.on('data', (data) => {
44
- child.write(data.toString());
45
- });
43
+ process.stdin.on('data', stdinHandler);
46
44
 
47
45
  // Handle terminal resize
48
- process.stdout.on('resize', () => {
46
+ const resizeHandler = () => {
49
47
  const newCols = process.stdout.columns;
50
48
  const newRows = process.stdout.rows;
51
49
  child.resize(newCols, newRows);
52
50
  handlers.onResize?.(newCols, newRows);
53
- });
51
+ };
52
+ process.stdout.on('resize', resizeHandler);
54
53
 
55
54
  // Send initial size
56
55
  handlers.onResize?.(cols, rows);
57
56
 
57
+ child.onExit(({ exitCode }) => {
58
+ // Clean up listeners to prevent leaks on respawn
59
+ process.stdin.removeListener('data', stdinHandler);
60
+ process.stdout.removeListener('resize', resizeHandler);
61
+ handlers.onExit?.(exitCode ?? 0);
62
+ });
63
+
58
64
  return {
59
65
  kill() {
60
66
  child.kill();