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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- * Allowlist of known-safe login shells.
12
- * Used to validate process.env.SHELL on non-Windows platforms to prevent
13
- * arbitrary binary execution if the environment is compromised.
14
- */
15
- const ALLOWED_SHELLS = [
16
- '/bin/bash', '/usr/bin/bash',
17
- '/bin/sh', '/usr/bin/sh',
18
- '/bin/zsh', '/usr/bin/zsh',
19
- '/bin/fish', '/usr/bin/fish',
20
- '/bin/dash', '/usr/bin/dash',
21
- '/bin/ash',
22
- ];
23
-
24
- /**
25
- * Get a safe shell path for the current platform.
26
- * Validates process.env.SHELL against an allowlist; falls back to /bin/bash.
27
- * @returns {string} Absolute path to a safe shell binary
28
- */
29
- function getSafeShell() {
30
- const envShell = process.env.SHELL;
31
- if (envShell && ALLOWED_SHELLS.includes(envShell)) {
32
- return envShell;
33
- }
34
- return '/bin/bash';
35
- }
36
-
37
- /**
38
- * Launch a Claude Code session by spawning a new detached process.
39
- * On Windows, opens a new cmd.exe console window.
40
- * On Linux/macOS, spawns a detached shell process (primarily used by TUI;
41
- * the web GUI uses PTY terminals via pty-manager.js instead).
42
- * Updates the store with the new PID and sets status to 'running'.
43
- * @param {string} sessionId - The session ID to launch
44
- * @returns {{ success: boolean, pid?: number, error?: string }}
45
- */
46
- function launchSession(sessionId) {
47
- const store = getStore();
48
- const session = store.getSession(sessionId);
49
-
50
- if (!session) {
51
- return { success: false, error: `Session ${sessionId} not found` };
52
- }
53
-
54
- if (session.status === 'running' && session.pid) {
55
- return { success: false, error: `Session ${sessionId} is already running (PID: ${session.pid})` };
56
- }
57
-
58
- try {
59
- const baseCommand = session.command || 'claude';
60
- const bypassFlag = session.bypassPermissions ? ' --dangerously-skip-permissions' : '';
61
- const command = baseCommand + bypassFlag;
62
- const workingDir = session.workingDir || process.cwd();
63
-
64
- let child;
65
- if (process.platform === 'win32') {
66
- // Windows: open a new console window via `cmd /c start cmd /k`
67
- child = spawn('cmd', ['/c', 'start', 'cmd', '/k', command], {
68
- detached: true,
69
- stdio: 'ignore',
70
- cwd: workingDir,
71
- shell: false,
72
- });
73
- } else {
74
- // Linux/macOS/WSL: spawn a detached login shell
75
- // Note: without a TTY, interactive CLI tools will exit immediately.
76
- // The web GUI uses pty-manager.js (with a real PTY) for terminal sessions.
77
- // This code path is kept for TUI compatibility and headless/scripted launches.
78
- const shell = getSafeShell();
79
- child = spawn(shell, ['-l', '-c', command], {
80
- detached: true,
81
- stdio: 'ignore',
82
- cwd: workingDir,
83
- });
84
- }
85
-
86
- const pid = child.pid;
87
-
88
- // Unref so the parent process can exit independently
89
- child.unref();
90
-
91
- store.updateSessionStatus(sessionId, 'running', pid);
92
- store.addSessionLog(sessionId, `Session launched with PID ${pid} (command: ${command})`);
93
-
94
- return { success: true, pid, process: child };
95
- } catch (err) {
96
- store.updateSessionStatus(sessionId, 'error', null);
97
- store.addSessionLog(sessionId, `Failed to launch session: ${err.message}`);
98
- return { success: false, error: err.message };
99
- }
100
- }
101
-
102
- /**
103
- * Stop a running session by killing its process.
104
- * Updates the store status to 'stopped'.
105
- * @param {string} sessionId - The session ID to stop
106
- * @returns {{ success: boolean, error?: string }}
107
- */
108
- function stopSession(sessionId) {
109
- const store = getStore();
110
- const session = store.getSession(sessionId);
111
-
112
- if (!session) {
113
- return { success: false, error: `Session ${sessionId} not found` };
114
- }
115
-
116
- if (session.status !== 'running' || !session.pid) {
117
- store.updateSessionStatus(sessionId, 'stopped', null);
118
- return { success: true };
119
- }
120
-
121
- try {
122
- process.kill(session.pid);
123
- store.updateSessionStatus(sessionId, 'stopped', null);
124
- store.addSessionLog(sessionId, `Session stopped (PID ${session.pid} killed)`);
125
- return { success: true };
126
- } catch (err) {
127
- // Process may already be dead - that's fine, mark as stopped anyway
128
- store.updateSessionStatus(sessionId, 'stopped', null);
129
- store.addSessionLog(sessionId, `Session stop - process already exited (${err.message})`);
130
- return { success: true };
131
- }
132
- }
133
-
134
- /**
135
- * Restart a session by stopping it first, then relaunching.
136
- * @param {string} sessionId - The session ID to restart
137
- * @returns {{ success: boolean, pid?: number, error?: string }}
138
- */
139
- function restartSession(sessionId) {
140
- const store = getStore();
141
- const session = store.getSession(sessionId);
142
-
143
- if (!session) {
144
- return { success: false, error: `Session ${sessionId} not found` };
145
- }
146
-
147
- store.addSessionLog(sessionId, 'Restarting session...');
148
-
149
- const stopResult = stopSession(sessionId);
150
- if (!stopResult.success) {
151
- return { success: false, error: `Failed to stop before restart: ${stopResult.error}` };
152
- }
153
-
154
- return launchSession(sessionId);
155
- }
156
-
157
- /**
158
- * Get process info for a session.
159
- * @param {string} sessionId - The session ID
160
- * @returns {{ pid: number|null, status: string, command: string }|null}
161
- */
162
- function getSessionProcess(sessionId) {
163
- const store = getStore();
164
- const session = store.getSession(sessionId);
165
-
166
- if (!session) {
167
- return null;
168
- }
169
-
170
- return {
171
- pid: session.pid,
172
- status: session.status,
173
- command: session.command || 'claude',
174
- workingDir: session.workingDir || '',
175
- };
176
- }
177
-
178
- module.exports = {
179
- launchSession,
180
- stopSession,
181
- restartSession,
182
- getSessionProcess,
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
+ };