myrlin-workbook 0.9.6 → 0.9.8
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/package.json +1 -1
- package/scripts/seed-sessions.js +121 -0
- package/src/core/session-manager.js +184 -183
- package/src/index.js +176 -176
- package/src/state/docs-manager.js +419 -419
- package/src/state/store.js +2 -1
- package/src/utils/data-dir.js +116 -0
- package/src/utils/path-utils.js +23 -0
- package/src/web/cost-worker.js +134 -0
- package/src/web/public/app.js +6 -2
- package/src/web/server.js +139 -6
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-time script to rebuild sessions from layout.json data.
|
|
4
|
+
* Reads layout.json, maps tab groups to workspace IDs, creates sessions via API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const http = require('http');
|
|
8
|
+
|
|
9
|
+
const BASE = 'http://127.0.0.1:3456';
|
|
10
|
+
const PASSWORD = 'Sparktech123!';
|
|
11
|
+
|
|
12
|
+
function apiCall(method, path, body) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const url = new URL(path, BASE);
|
|
15
|
+
const data = body ? JSON.stringify(body) : null;
|
|
16
|
+
const opts = {
|
|
17
|
+
hostname: url.hostname,
|
|
18
|
+
port: url.port,
|
|
19
|
+
path: url.pathname + url.search,
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
...(apiCall._token ? { 'Authorization': 'Bearer ' + apiCall._token } : {}),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const req = http.request(opts, (res) => {
|
|
27
|
+
let chunks = '';
|
|
28
|
+
res.on('data', c => chunks += c);
|
|
29
|
+
res.on('end', () => {
|
|
30
|
+
try { resolve(JSON.parse(chunks)); } catch { resolve(chunks); }
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
req.on('error', reject);
|
|
34
|
+
if (data) req.write(data);
|
|
35
|
+
req.end();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
// Auth
|
|
41
|
+
const auth = await apiCall('POST', '/api/auth/login', { password: PASSWORD });
|
|
42
|
+
apiCall._token = auth.token;
|
|
43
|
+
console.log('Authenticated');
|
|
44
|
+
|
|
45
|
+
// Get workspace map
|
|
46
|
+
const wsResp = await apiCall('GET', '/api/workspaces');
|
|
47
|
+
const wsMap = {};
|
|
48
|
+
for (const ws of wsResp.workspaces) {
|
|
49
|
+
wsMap[ws.name] = ws.id;
|
|
50
|
+
}
|
|
51
|
+
console.log('Workspaces:', Object.keys(wsMap).join(', '));
|
|
52
|
+
|
|
53
|
+
// Session definitions from layout.json
|
|
54
|
+
const sessions = [
|
|
55
|
+
// Pantex
|
|
56
|
+
{ name: 'pantex main', ws: 'Pantex', cwd: 'C:\\Users\\Arthur\\Desktop\\Desktop\\work\\pantex', resume: '01ec027f-d5ba-42db-ae3e-1337d204da7b' },
|
|
57
|
+
{ name: 'pantex', ws: 'Pantex', cwd: 'C:\\Users\\Arthur\\Desktop\\Desktop\\work\\pantex' },
|
|
58
|
+
{ name: 'pantex 3', ws: 'Pantex', cwd: 'C:\\Users\\Arthur\\Desktop\\Desktop\\work\\pantex' },
|
|
59
|
+
{ name: 'pantex (fbc77065)', ws: 'Pantex', cwd: 'C:\\Users\\Arthur\\Desktop\\Desktop\\work\\pantex', resume: 'fbc77065-760d-4834-876c-57aef851f51b' },
|
|
60
|
+
|
|
61
|
+
// Myrlin Research
|
|
62
|
+
{ name: 'Big ML Study', ws: 'Myrlin Research', cwd: 'C:\\Users\\Arthur\\Desktop\\claude-workspace-manager' },
|
|
63
|
+
{ name: 'Modding code focus', ws: 'Myrlin Research', cwd: 'C:\\Users\\Arthur\\hytale-asset-factory', resume: '2d1047c7-42ea-4bf8-a17e-1e4d3e448582' },
|
|
64
|
+
{ name: 'hytale-asset-factory - new', ws: 'Myrlin Research', cwd: 'C:\\Users\\Arthur\\hytale-asset-factory' },
|
|
65
|
+
{ name: 'Main Myrlin Debug', ws: 'Myrlin Research', cwd: 'C:\\Users\\Arthur\\hytale-asset-factory', resume: 'c0d5a3a3-9963-49c6-9de3-185b47034260' },
|
|
66
|
+
|
|
67
|
+
// Workbook
|
|
68
|
+
{ name: 'Workbook main', ws: 'Workbook', cwd: 'C:\\Users\\Arthur\\Desktop\\claude-workspace-manager', resume: '4271e03d-7295-40e4-bb35-0fd887c39461' },
|
|
69
|
+
{ name: 'refactor/optimization', ws: 'Workbook', cwd: 'C:\\Users\\Arthur\\Desktop\\claude-workspace-manager', resume: '4ece62ba-44db-49a2-8af5-4c44f85a1514' },
|
|
70
|
+
|
|
71
|
+
// Myrlin Debug/General
|
|
72
|
+
{ name: 'Main Myrlin Debug', ws: 'Myrlin Debug/General', cwd: 'C:\\Users\\Arthur\\hytale-asset-factory', resume: 'c0d5a3a3-9963-49c6-9de3-185b47034260' },
|
|
73
|
+
{ name: 'reddit conversation', ws: 'Myrlin Debug/General', cwd: 'C:\\Users\\Arthur\\hytale-asset-factory', resume: '9af8b4ef-b5d4-4a69-a004-62755e612e39' },
|
|
74
|
+
|
|
75
|
+
// Loussine
|
|
76
|
+
{ name: 'Loussines stuff', ws: 'Loussine', cwd: 'C:\\Users\\Arthur' },
|
|
77
|
+
|
|
78
|
+
// Onnik
|
|
79
|
+
{ name: 'filepro_ai (36b04d1d)', ws: 'Onnik', cwd: 'C:\\Users\\Arthur\\Desktop\\filepro_ai', resume: '36b04d1d-4260-4630-aa1c-283f822dba0e' },
|
|
80
|
+
{ name: 'filepro_ai (4d9d0d40)', ws: 'Onnik', cwd: 'C:\\Users\\Arthur\\Desktop\\filepro_ai', resume: '4d9d0d40-df32-4fb3-bd45-b755ca5618fc' },
|
|
81
|
+
|
|
82
|
+
// Myrlin Platform
|
|
83
|
+
{ name: 'Adidas bot', ws: 'Myrlin Platform', cwd: 'C:\\Users\\Arthur' },
|
|
84
|
+
{ name: 'myrlin portal', ws: 'Myrlin Platform', cwd: 'C:\\Users\\Arthur', resume: 'c0df573d-78ce-40a7-9033-eb392e87395c' },
|
|
85
|
+
|
|
86
|
+
// AI incubator
|
|
87
|
+
{ name: 'M&H Billing Dev', ws: 'AI incubator', cwd: 'C:\\Users\\Arthur\\Desktop\\Work AI Project' },
|
|
88
|
+
{ name: 'Q&A', ws: 'AI incubator', cwd: 'C:\\Users\\Arthur\\Desktop\\Work AI Project', resume: '20553635-833e-4a24-af3b-8cbee03c2d65' },
|
|
89
|
+
{ name: 'random tasking 1', ws: 'AI incubator', cwd: 'C:\\Users\\Arthur\\Desktop\\Work AI Project', resume: 'e964d01e-031d-460a-8988-e9bff6efb949' },
|
|
90
|
+
{ name: 'random tasking 2', ws: 'AI incubator', cwd: 'C:\\Users\\Arthur\\Desktop\\Work AI Project', resume: '00964339-f945-4ba0-aa7f-e1bcb47be118' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
let created = 0;
|
|
94
|
+
for (const s of sessions) {
|
|
95
|
+
const wsId = wsMap[s.ws];
|
|
96
|
+
if (!wsId) {
|
|
97
|
+
console.error(' SKIP: no workspace found for', s.ws);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const body = {
|
|
101
|
+
name: s.name,
|
|
102
|
+
workspaceId: wsId,
|
|
103
|
+
workingDir: s.cwd,
|
|
104
|
+
command: 'claude',
|
|
105
|
+
topic: s.name,
|
|
106
|
+
};
|
|
107
|
+
if (s.resume) body.resumeSessionId = s.resume;
|
|
108
|
+
|
|
109
|
+
const result = await apiCall('POST', '/api/sessions', body);
|
|
110
|
+
if (result.session) {
|
|
111
|
+
console.log(' Created:', s.name, '->', s.ws);
|
|
112
|
+
created++;
|
|
113
|
+
} else {
|
|
114
|
+
console.error(' FAILED:', s.name, result.error || JSON.stringify(result));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('\nDone:', created, 'sessions created');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch(err => { console.error(err); process.exit(1); });
|
|
@@ -1,183 +1,184 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Manager - Manages Claude Code session lifecycle
|
|
3
|
-
* Handles launching, stopping, and restarting session processes.
|
|
4
|
-
* Supports Windows (cmd.exe), macOS, and Linux/WSL.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { spawn } = require('child_process');
|
|
8
|
-
const { getStore } = require('../state/store');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'/bin/
|
|
18
|
-
'/bin/
|
|
19
|
-
'/bin/
|
|
20
|
-
'/bin/
|
|
21
|
-
'/bin/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
* On
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
store.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
store.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
store.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
store.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
* @
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
* @
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager - Manages Claude Code session lifecycle
|
|
3
|
+
* Handles launching, stopping, and restarting session processes.
|
|
4
|
+
* Supports Windows (cmd.exe), macOS, and Linux/WSL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const { getStore } = require('../state/store');
|
|
9
|
+
const { expandHome } = require('../utils/path-utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Allowlist of known-safe login shells.
|
|
13
|
+
* Used to validate process.env.SHELL on non-Windows platforms to prevent
|
|
14
|
+
* arbitrary binary execution if the environment is compromised.
|
|
15
|
+
*/
|
|
16
|
+
const ALLOWED_SHELLS = [
|
|
17
|
+
'/bin/bash', '/usr/bin/bash',
|
|
18
|
+
'/bin/sh', '/usr/bin/sh',
|
|
19
|
+
'/bin/zsh', '/usr/bin/zsh',
|
|
20
|
+
'/bin/fish', '/usr/bin/fish',
|
|
21
|
+
'/bin/dash', '/usr/bin/dash',
|
|
22
|
+
'/bin/ash',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get a safe shell path for the current platform.
|
|
27
|
+
* Validates process.env.SHELL against an allowlist; falls back to /bin/bash.
|
|
28
|
+
* @returns {string} Absolute path to a safe shell binary
|
|
29
|
+
*/
|
|
30
|
+
function getSafeShell() {
|
|
31
|
+
const envShell = process.env.SHELL;
|
|
32
|
+
if (envShell && ALLOWED_SHELLS.includes(envShell)) {
|
|
33
|
+
return envShell;
|
|
34
|
+
}
|
|
35
|
+
return '/bin/bash';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Launch a Claude Code session by spawning a new detached process.
|
|
40
|
+
* On Windows, opens a new cmd.exe console window.
|
|
41
|
+
* On Linux/macOS, spawns a detached shell process (primarily used by TUI;
|
|
42
|
+
* the web GUI uses PTY terminals via pty-manager.js instead).
|
|
43
|
+
* Updates the store with the new PID and sets status to 'running'.
|
|
44
|
+
* @param {string} sessionId - The session ID to launch
|
|
45
|
+
* @returns {{ success: boolean, pid?: number, error?: string }}
|
|
46
|
+
*/
|
|
47
|
+
function launchSession(sessionId) {
|
|
48
|
+
const store = getStore();
|
|
49
|
+
const session = store.getSession(sessionId);
|
|
50
|
+
|
|
51
|
+
if (!session) {
|
|
52
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (session.status === 'running' && session.pid) {
|
|
56
|
+
return { success: false, error: `Session ${sessionId} is already running (PID: ${session.pid})` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const baseCommand = session.command || 'claude';
|
|
61
|
+
const bypassFlag = session.bypassPermissions ? ' --dangerously-skip-permissions' : '';
|
|
62
|
+
const command = baseCommand + bypassFlag;
|
|
63
|
+
const workingDir = expandHome(session.workingDir) || process.cwd();
|
|
64
|
+
|
|
65
|
+
let child;
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
// Windows: open a new console window via `cmd /c start cmd /k`
|
|
68
|
+
child = spawn('cmd', ['/c', 'start', 'cmd', '/k', command], {
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
cwd: workingDir,
|
|
72
|
+
shell: false,
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// Linux/macOS/WSL: spawn a detached login shell
|
|
76
|
+
// Note: without a TTY, interactive CLI tools will exit immediately.
|
|
77
|
+
// The web GUI uses pty-manager.js (with a real PTY) for terminal sessions.
|
|
78
|
+
// This code path is kept for TUI compatibility and headless/scripted launches.
|
|
79
|
+
const shell = getSafeShell();
|
|
80
|
+
child = spawn(shell, ['-l', '-c', command], {
|
|
81
|
+
detached: true,
|
|
82
|
+
stdio: 'ignore',
|
|
83
|
+
cwd: workingDir,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pid = child.pid;
|
|
88
|
+
|
|
89
|
+
// Unref so the parent process can exit independently
|
|
90
|
+
child.unref();
|
|
91
|
+
|
|
92
|
+
store.updateSessionStatus(sessionId, 'running', pid);
|
|
93
|
+
store.addSessionLog(sessionId, `Session launched with PID ${pid} (command: ${command})`);
|
|
94
|
+
|
|
95
|
+
return { success: true, pid, process: child };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
store.updateSessionStatus(sessionId, 'error', null);
|
|
98
|
+
store.addSessionLog(sessionId, `Failed to launch session: ${err.message}`);
|
|
99
|
+
return { success: false, error: err.message };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stop a running session by killing its process.
|
|
105
|
+
* Updates the store status to 'stopped'.
|
|
106
|
+
* @param {string} sessionId - The session ID to stop
|
|
107
|
+
* @returns {{ success: boolean, error?: string }}
|
|
108
|
+
*/
|
|
109
|
+
function stopSession(sessionId) {
|
|
110
|
+
const store = getStore();
|
|
111
|
+
const session = store.getSession(sessionId);
|
|
112
|
+
|
|
113
|
+
if (!session) {
|
|
114
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (session.status !== 'running' || !session.pid) {
|
|
118
|
+
store.updateSessionStatus(sessionId, 'stopped', null);
|
|
119
|
+
return { success: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
process.kill(session.pid);
|
|
124
|
+
store.updateSessionStatus(sessionId, 'stopped', null);
|
|
125
|
+
store.addSessionLog(sessionId, `Session stopped (PID ${session.pid} killed)`);
|
|
126
|
+
return { success: true };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// Process may already be dead - that's fine, mark as stopped anyway
|
|
129
|
+
store.updateSessionStatus(sessionId, 'stopped', null);
|
|
130
|
+
store.addSessionLog(sessionId, `Session stop - process already exited (${err.message})`);
|
|
131
|
+
return { success: true };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Restart a session by stopping it first, then relaunching.
|
|
137
|
+
* @param {string} sessionId - The session ID to restart
|
|
138
|
+
* @returns {{ success: boolean, pid?: number, error?: string }}
|
|
139
|
+
*/
|
|
140
|
+
function restartSession(sessionId) {
|
|
141
|
+
const store = getStore();
|
|
142
|
+
const session = store.getSession(sessionId);
|
|
143
|
+
|
|
144
|
+
if (!session) {
|
|
145
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
store.addSessionLog(sessionId, 'Restarting session...');
|
|
149
|
+
|
|
150
|
+
const stopResult = stopSession(sessionId);
|
|
151
|
+
if (!stopResult.success) {
|
|
152
|
+
return { success: false, error: `Failed to stop before restart: ${stopResult.error}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return launchSession(sessionId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get process info for a session.
|
|
160
|
+
* @param {string} sessionId - The session ID
|
|
161
|
+
* @returns {{ pid: number|null, status: string, command: string }|null}
|
|
162
|
+
*/
|
|
163
|
+
function getSessionProcess(sessionId) {
|
|
164
|
+
const store = getStore();
|
|
165
|
+
const session = store.getSession(sessionId);
|
|
166
|
+
|
|
167
|
+
if (!session) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
pid: session.pid,
|
|
173
|
+
status: session.status,
|
|
174
|
+
command: session.command || 'claude',
|
|
175
|
+
workingDir: session.workingDir || '',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
launchSession,
|
|
181
|
+
stopSession,
|
|
182
|
+
restartSession,
|
|
183
|
+
getSessionProcess,
|
|
184
|
+
};
|