@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 +19 -3
- package/package.json +1 -1
- package/src/index.js +58 -19
- package/src/pty-capture.js +15 -9
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
|
-
|
|
14
|
+
curl -fsSL https://keerok.tech/lab/install.sh | bash -s -- <TOKEN>
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
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
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
156
|
+
if (currentPty) currentPty.kill();
|
|
118
157
|
ws.close();
|
|
119
158
|
process.exit(0);
|
|
120
159
|
});
|
package/src/pty-capture.js
CHANGED
|
@@ -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',
|
|
44
|
-
child.write(data.toString());
|
|
45
|
-
});
|
|
43
|
+
process.stdin.on('data', stdinHandler);
|
|
46
44
|
|
|
47
45
|
// Handle terminal resize
|
|
48
|
-
|
|
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();
|