myrlin-workbook 0.9.24 → 0.9.26

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/logs/server.pid CHANGED
@@ -1 +1 @@
1
- 68900
1
+ 44496
package/package.json CHANGED
@@ -1,66 +1,67 @@
1
- {
2
- "name": "myrlin-workbook",
3
- "version": "0.9.24",
4
- "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
- "main": "src/index.js",
6
- "bin": {
7
- "myrlin-workbook": "./src/gui.js",
8
- "myrlin": "./src/gui.js",
9
- "myrlin-tui": "./src/index.js",
10
- "cwm": "./src/index.js"
11
- },
12
- "scripts": {
13
- "start": "node src/index.js",
14
- "demo": "node src/demo.js",
15
- "gui": "node src/supervisor.js",
16
- "gui:daemon": "node src/supervisor.js --daemon",
17
- "gui:bare": "node src/gui.js",
18
- "gui:demo": "node src/supervisor.js --demo",
19
- "test": "node test/run.js",
20
- "mcp:visual-qa": "node src/mcp/visual-qa.js",
21
- "gui:cdp": "node src/supervisor.js --cdp",
22
- "postinstall": "node scripts/postinstall.js",
23
- "restart": "bash scripts/restart-gui.sh"
24
- },
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/therealarthur/myrlin-workbook.git"
28
- },
29
- "homepage": "https://github.com/therealarthur/myrlin-workbook",
30
- "engines": {
31
- "node": ">=18.0.0"
32
- },
33
- "keywords": [
34
- "claude",
35
- "workspace",
36
- "manager",
37
- "terminal",
38
- "tui",
39
- "ai",
40
- "coding-assistant",
41
- "session-manager",
42
- "developer-tools",
43
- "xterm",
44
- "myrlin"
45
- ],
46
- "author": "Arthur",
47
- "license": "AGPL-3.0-only",
48
- "dependencies": {
49
- "blessed": "^0.1.81",
50
- "blessed-contrib": "^4.11.0",
51
- "chalk": "^5.6.2",
52
- "chrome-remote-interface": "^0.34.0",
53
- "express": "^5.2.1",
54
- "node-pty": "^1.1.0",
55
- "qrcode": "^1.5.4",
56
- "ws": "^8.19.0"
57
- },
58
- "devDependencies": {
59
- "@playwright/test": "^1.58.2",
60
- "@xterm/addon-fit": "^0.11.0",
61
- "@xterm/addon-web-links": "^0.12.0",
62
- "@xterm/xterm": "^6.0.0",
63
- "ffmpeg-static": "^5.3.0",
64
- "sharp": "^0.34.5"
65
- }
66
- }
1
+ {
2
+ "name": "myrlin-workbook",
3
+ "version": "0.9.26",
4
+ "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "myrlin-workbook": "./src/gui.js",
8
+ "myrlin": "./src/gui.js",
9
+ "myrlin-tui": "./src/index.js",
10
+ "cwm": "./src/index.js"
11
+ },
12
+ "scripts": {
13
+ "start": "node src/index.js",
14
+ "demo": "node src/demo.js",
15
+ "gui": "node src/supervisor.js",
16
+ "gui:daemon": "node src/supervisor.js --daemon",
17
+ "gui:bare": "node src/gui.js",
18
+ "gui:demo": "node src/supervisor.js --demo",
19
+ "test": "node test/run.js",
20
+ "mcp:visual-qa": "node src/mcp/visual-qa.js",
21
+ "gui:cdp": "node src/supervisor.js --cdp",
22
+ "postinstall": "node scripts/postinstall.js",
23
+ "restart": "bash scripts/restart-gui.sh"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/therealarthur/myrlin-workbook.git"
28
+ },
29
+ "homepage": "https://github.com/therealarthur/myrlin-workbook",
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "keywords": [
34
+ "claude",
35
+ "workspace",
36
+ "manager",
37
+ "terminal",
38
+ "tui",
39
+ "ai",
40
+ "coding-assistant",
41
+ "session-manager",
42
+ "developer-tools",
43
+ "xterm",
44
+ "myrlin"
45
+ ],
46
+ "author": "Arthur",
47
+ "license": "AGPL-3.0-only",
48
+ "dependencies": {
49
+ "blessed": "^0.1.81",
50
+ "blessed-contrib": "^4.11.0",
51
+ "chalk": "^5.6.2",
52
+ "chrome-remote-interface": "^0.34.0",
53
+ "express": "^5.2.1",
54
+ "node-pty": "^1.1.0",
55
+ "qrcode": "^1.5.4",
56
+ "simple-git": "^3.33.0",
57
+ "ws": "^8.19.0"
58
+ },
59
+ "devDependencies": {
60
+ "@playwright/test": "^1.58.2",
61
+ "@xterm/addon-fit": "^0.11.0",
62
+ "@xterm/addon-web-links": "^0.12.0",
63
+ "@xterm/xterm": "^6.0.0",
64
+ "ffmpeg-static": "^5.3.0",
65
+ "sharp": "^0.34.5"
66
+ }
67
+ }
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Map of file extensions to CodeMirror language names.
8
+ */
9
+ const EXT_TO_LANG = {
10
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript',
11
+ ts: 'typescript', tsx: 'typescript',
12
+ jsx: 'javascript',
13
+ json: 'json',
14
+ html: 'html', htm: 'html',
15
+ css: 'css', scss: 'css', less: 'css',
16
+ md: 'markdown', markdown: 'markdown',
17
+ py: 'python',
18
+ sh: 'shell', bash: 'shell',
19
+ yaml: 'yaml', yml: 'yaml',
20
+ toml: 'toml',
21
+ rs: 'rust',
22
+ go: 'go',
23
+ java: 'java',
24
+ rb: 'ruby',
25
+ php: 'php',
26
+ sql: 'sql',
27
+ xml: 'xml',
28
+ txt: 'text',
29
+ };
30
+
31
+ /**
32
+ * Directory names to skip when building the file tree.
33
+ * Prevents traversal into version control internals and build artifacts.
34
+ */
35
+ const SKIP_DIRS = new Set([
36
+ '.git', 'node_modules', '.DS_Store', '__pycache__',
37
+ '.next', 'dist', 'build', '.cache',
38
+ ]);
39
+
40
+ /**
41
+ * Validate that the resolved target path stays within root.
42
+ * Throws if path traversal is detected.
43
+ *
44
+ * @param {string} root - Absolute workspace root path
45
+ * @param {string} relPath - Relative path from client
46
+ * @returns {string} Resolved absolute path
47
+ */
48
+ function validatePath(root, relPath) {
49
+ if (!root || typeof root !== 'string') throw new Error('root is required');
50
+ const resolved = path.resolve(root, relPath || '');
51
+ if (!resolved.startsWith(path.resolve(root) + path.sep) && resolved !== path.resolve(root)) {
52
+ throw new Error('Path traversal detected');
53
+ }
54
+ return resolved;
55
+ }
56
+
57
+ /**
58
+ * GET /api/files/tree
59
+ * Returns directory entries (dirs and files) for a given subpath within the
60
+ * workspace root. Skips .git, node_modules, and other build artifacts.
61
+ *
62
+ * @param {string} workingDir - Workspace root directory
63
+ * @param {string} [subpath=''] - Relative subpath to list
64
+ * @returns {Promise<{path: string, entries: Array}>}
65
+ */
66
+ async function getTree(workingDir, subpath = '') {
67
+ const root = path.resolve(workingDir);
68
+ const target = validatePath(root, subpath);
69
+
70
+ const entries = await fs.promises.readdir(target, { withFileTypes: true });
71
+ const result = [];
72
+
73
+ for (const entry of entries) {
74
+ if (SKIP_DIRS.has(entry.name)) continue;
75
+ // Skip hidden files/dirs (dot-files) at root level
76
+ if (subpath === '' && entry.name.startsWith('.')) continue;
77
+ const relPath = subpath ? path.join(subpath, entry.name) : entry.name;
78
+ if (entry.isDirectory()) {
79
+ result.push({ name: entry.name, path: relPath, type: 'dir' });
80
+ } else if (entry.isFile()) {
81
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
82
+ result.push({ name: entry.name, path: relPath, type: 'file', ext });
83
+ }
84
+ }
85
+
86
+ // Sort: directories first, then files, each group sorted alphabetically
87
+ result.sort((a, b) => {
88
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
89
+ return a.name.localeCompare(b.name);
90
+ });
91
+
92
+ return { path: subpath || '/', entries: result };
93
+ }
94
+
95
+ /**
96
+ * GET /api/files/content
97
+ * Returns file content as UTF-8 text with a language hint for CodeMirror.
98
+ * Rejects files larger than 5MB.
99
+ *
100
+ * @param {string} workingDir - Workspace root directory
101
+ * @param {string} file - Relative path to the file
102
+ * @returns {Promise<{content: string, language: string, size: number, path: string}>}
103
+ */
104
+ async function getContent(workingDir, file) {
105
+ if (!file) throw new Error('file is required');
106
+ const root = path.resolve(workingDir);
107
+ const target = validatePath(root, file);
108
+
109
+ const stat = await fs.promises.stat(target);
110
+ if (stat.size > 5 * 1024 * 1024) throw new Error('File too large (> 5MB)');
111
+
112
+ const content = await fs.promises.readFile(target, 'utf8');
113
+ const ext = path.extname(file).slice(1).toLowerCase();
114
+ return {
115
+ content,
116
+ language: EXT_TO_LANG[ext] || 'text',
117
+ size: stat.size,
118
+ path: file,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * POST /api/files/save
124
+ * Atomically writes file content by writing to a temp file then renaming.
125
+ * Creates parent directories if they don't exist.
126
+ *
127
+ * @param {string} workingDir - Workspace root directory
128
+ * @param {string} file - Relative path to the file
129
+ * @param {string} content - UTF-8 text content to write
130
+ * @returns {Promise<{ok: boolean}>}
131
+ */
132
+ async function saveContent(workingDir, file, content) {
133
+ if (!file) throw new Error('file is required');
134
+ if (typeof content !== 'string') throw new Error('content must be a string');
135
+ const root = path.resolve(workingDir);
136
+ const target = validatePath(root, file);
137
+
138
+ // Ensure parent directory exists
139
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
140
+
141
+ // Atomic write: write to temp file then rename into place
142
+ const tmp = target + '.tmp.' + Date.now();
143
+ await fs.promises.writeFile(tmp, content, 'utf8');
144
+ await fs.promises.rename(tmp, target);
145
+
146
+ return { ok: true };
147
+ }
148
+
149
+ module.exports = { getTree, getContent, saveContent };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Git Manager - wraps simple-git for use in API endpoints.
5
+ *
6
+ * All functions accept a workingDir string and perform git operations
7
+ * scoped to that directory. path.resolve() is used to prevent traversal.
8
+ */
9
+
10
+ const { simpleGit } = require('simple-git');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Resolve and validate the working directory for git operations.
15
+ * @param {string} workingDir - The workspace working directory
16
+ * @returns {string} The resolved absolute path
17
+ */
18
+ function resolveGitDir(workingDir) {
19
+ if (!workingDir || typeof workingDir !== 'string') {
20
+ throw new Error('workingDir is required');
21
+ }
22
+ return path.resolve(workingDir);
23
+ }
24
+
25
+ /**
26
+ * Get the current git status for a working directory.
27
+ * Returns branch name, staged/modified/untracked/deleted files.
28
+ * @param {string} workingDir - Workspace root directory
29
+ * @returns {Promise<object>} Status object with branch and file lists
30
+ */
31
+ async function getStatus(workingDir) {
32
+ const git = simpleGit(resolveGitDir(workingDir));
33
+ const status = await git.status();
34
+ return {
35
+ branch: status.current,
36
+ staged: status.staged.map(f => ({ file: f, state: 'staged' })),
37
+ modified: status.modified.map(f => ({ file: f, state: 'modified' })),
38
+ notAdded: status.not_added.map(f => ({ file: f, state: 'untracked' })),
39
+ deleted: status.deleted.map(f => ({ file: f, state: 'deleted' })),
40
+ isClean: status.isClean(),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Get the commit log for a working directory.
46
+ * @param {string} workingDir - Workspace root directory
47
+ * @param {number} [limit=20] - Maximum number of commits to return (capped at 100)
48
+ * @returns {Promise<Array>} Array of commit objects with hash, author, date, message
49
+ */
50
+ async function getLog(workingDir, limit = 20) {
51
+ const git = simpleGit(resolveGitDir(workingDir));
52
+ try {
53
+ const log = await git.log({ maxCount: Math.min(parseInt(limit, 10) || 20, 100) });
54
+ return (log.all || []).map(c => ({
55
+ hash: c.hash,
56
+ shortHash: c.hash.slice(0, 7),
57
+ author: c.author_name,
58
+ date: c.date,
59
+ message: c.message,
60
+ }));
61
+ } catch (e) {
62
+ // Fresh repo with no commits yet
63
+ if (e.message && e.message.includes('does not have any commits')) return [];
64
+ throw e;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the diff for a specific file in the working directory.
70
+ * @param {string} workingDir - Workspace root directory
71
+ * @param {string} file - File path relative to workingDir
72
+ * @param {boolean} [staged=false] - Whether to show staged diff (vs unstaged)
73
+ * @returns {Promise<string>} The unified diff output
74
+ */
75
+ async function getDiff(workingDir, file, staged = false) {
76
+ const git = simpleGit(resolveGitDir(workingDir));
77
+ let diff;
78
+ if (staged) {
79
+ diff = await git.diff(['--cached', '--', file]);
80
+ } else {
81
+ diff = await git.diff(['--', file]);
82
+ }
83
+ return diff || '';
84
+ }
85
+
86
+ /**
87
+ * Get branch information for a working directory.
88
+ * @param {string} workingDir - Workspace root directory
89
+ * @returns {Promise<{current: string, all: string[]}>} Current branch and all branch names
90
+ */
91
+ async function getBranches(workingDir) {
92
+ const git = simpleGit(resolveGitDir(workingDir));
93
+ const result = await git.branch(['-a']);
94
+ return {
95
+ current: result.current,
96
+ all: result.all,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Get the full diff output for a specific commit (git show).
102
+ * @param {string} workingDir - Workspace root directory
103
+ * @param {string} hash - Full or short commit hash
104
+ * @returns {Promise<string>} The unified diff/show output
105
+ */
106
+ async function getCommitDiff(workingDir, hash) {
107
+ if (!hash || !/^[0-9a-f]{4,40}$/i.test(hash)) {
108
+ throw new Error('Invalid commit hash');
109
+ }
110
+ const git = simpleGit(resolveGitDir(workingDir));
111
+ const output = await git.show([hash, '--stat', '--patch']);
112
+ return output || '';
113
+ }
114
+
115
+ module.exports = { getStatus, getLog, getDiff, getBranches, getCommitDiff };