myrlin-workbook 0.9.26 → 0.9.28

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
- 44496
1
+ 55976
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
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": {
@@ -20,7 +20,8 @@
20
20
  "mcp:visual-qa": "node src/mcp/visual-qa.js",
21
21
  "gui:cdp": "node src/supervisor.js --cdp",
22
22
  "postinstall": "node scripts/postinstall.js",
23
- "restart": "bash scripts/restart-gui.sh"
23
+ "restart": "bash scripts/restart-gui.sh",
24
+ "build:icons": "node scripts/build-lucide-bundle.mjs && node scripts/build-material-bundle.mjs"
24
25
  },
25
26
  "repository": {
26
27
  "type": "git",
@@ -46,17 +47,20 @@
46
47
  "author": "Arthur",
47
48
  "license": "AGPL-3.0-only",
48
49
  "dependencies": {
50
+ "@lucide/lab": "^0.1.2",
49
51
  "blessed": "^0.1.81",
50
52
  "blessed-contrib": "^4.11.0",
51
53
  "chalk": "^5.6.2",
52
54
  "chrome-remote-interface": "^0.34.0",
53
55
  "express": "^5.2.1",
56
+ "lucide": "^1.8.0",
54
57
  "node-pty": "^1.1.0",
55
58
  "qrcode": "^1.5.4",
56
59
  "simple-git": "^3.33.0",
57
60
  "ws": "^8.19.0"
58
61
  },
59
62
  "devDependencies": {
63
+ "@material-icons/svg": "^1.0.33",
60
64
  "@playwright/test": "^1.58.2",
61
65
  "@xterm/addon-fit": "^0.11.0",
62
66
  "@xterm/addon-web-links": "^0.12.0",
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script: generates public/vendor/lucide.bundle.js
4
+ * Imports curated icons from `lucide` and `@lucide/lab`, converts to SVG strings,
5
+ * and outputs a plain JS bundle that sets window.__lucideIcons.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, resolve } from 'path';
11
+ import vm from 'vm';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const ROOT = resolve(__dirname, '..');
15
+ const OUT = resolve(ROOT, 'src/web/public/vendor/lucide.bundle.js');
16
+
17
+ // ── Curated icon list ────────────────────────────────────────────────────────
18
+ // Format: { category: [[kebab-name, 'lucide' | 'lab'], ...], ... }
19
+ const ICON_CATEGORIES = {
20
+ 'Folders & Files': [
21
+ ['folder', 'lucide'],
22
+ ['folder-open', 'lucide'],
23
+ ['folder-code', 'lucide'],
24
+ ['folder-git-2', 'lucide'],
25
+ ['folder-sync', 'lucide'],
26
+ ['folder-key', 'lucide'],
27
+ ['folder-search', 'lucide'],
28
+ ['folders', 'lucide'],
29
+ ['file', 'lucide'],
30
+ ['file-code', 'lucide'],
31
+ ['file-text', 'lucide'],
32
+ ['files', 'lucide'],
33
+ ['notebook', 'lucide'],
34
+ ['notebook-pen', 'lucide'],
35
+ ],
36
+ 'Code & Dev': [
37
+ ['code', 'lucide'],
38
+ ['code-xml', 'lucide'],
39
+ ['square-terminal', 'lucide'],
40
+ ['terminal', 'lucide'],
41
+ ['git-branch', 'lucide'],
42
+ ['git-merge', 'lucide'],
43
+ ['git-commit-vertical', 'lucide'],
44
+ ['git-fork', 'lucide'],
45
+ ['braces', 'lucide'],
46
+ ['brackets', 'lucide'],
47
+ ['hash', 'lucide'],
48
+ ['variable', 'lucide'],
49
+ ],
50
+ 'Infrastructure': [
51
+ ['database', 'lucide'],
52
+ ['database-zap', 'lucide'],
53
+ ['server', 'lucide'],
54
+ ['server-cog', 'lucide'],
55
+ ['cpu', 'lucide'],
56
+ ['hard-drive', 'lucide'],
57
+ ['network', 'lucide'],
58
+ ['wifi', 'lucide'],
59
+ ['cloud', 'lucide'],
60
+ ['cloud-upload', 'lucide'],
61
+ ['globe', 'lucide'],
62
+ ['layers', 'lucide'],
63
+ ],
64
+ 'Apps & Tools': [
65
+ ['box', 'lucide'],
66
+ ['package', 'lucide'],
67
+ ['package-2', 'lucide'],
68
+ ['workflow', 'lucide'],
69
+ ['settings', 'lucide'],
70
+ ['settings-2', 'lucide'],
71
+ ['wrench', 'lucide'],
72
+ ['hammer', 'lucide'],
73
+ ['cog', 'lucide'],
74
+ ['tool-case', 'lucide'],
75
+ ['puzzle', 'lucide'],
76
+ ['blocks', 'lucide'],
77
+ ['plug', 'lucide'],
78
+ ['cpu', 'lucide'],
79
+ ],
80
+ 'Design & Art': [
81
+ ['palette', 'lucide'],
82
+ ['brush', 'lucide'],
83
+ ['paintbrush', 'lucide'],
84
+ ['wand', 'lucide'],
85
+ ['wand-sparkles', 'lucide'],
86
+ ['sparkles', 'lucide'],
87
+ ['pen-tool', 'lucide'],
88
+ ['pencil', 'lucide'],
89
+ ['scissors', 'lucide'],
90
+ ['crop', 'lucide'],
91
+ ['image', 'lucide'],
92
+ ['camera', 'lucide'],
93
+ ],
94
+ 'Learning & Science': [
95
+ ['book-open', 'lucide'],
96
+ ['book', 'lucide'],
97
+ ['scroll', 'lucide'],
98
+ ['flask-conical', 'lucide'],
99
+ ['flask-round', 'lucide'],
100
+ ['microscope', 'lucide'],
101
+ ['test-tube', 'lucide'],
102
+ ['brain', 'lucide'],
103
+ ['brain-circuit', 'lucide'],
104
+ ['atom', 'lucide'],
105
+ ['dna', 'lucide'],
106
+ ['telescope', 'lucide'],
107
+ ],
108
+ 'People & Status': [
109
+ ['user', 'lucide'],
110
+ ['users', 'lucide'],
111
+ ['user-round', 'lucide'],
112
+ ['bot', 'lucide'],
113
+ ['rocket', 'lucide'],
114
+ ['star', 'lucide'],
115
+ ['heart', 'lucide'],
116
+ ['zap', 'lucide'],
117
+ ['shield', 'lucide'],
118
+ ['lock', 'lucide'],
119
+ ['key', 'lucide'],
120
+ ['flag', 'lucide'],
121
+ ['trophy', 'lucide'],
122
+ ['medal', 'lucide'],
123
+ ['crown', 'lucide'],
124
+ ],
125
+ 'Home & Work': [
126
+ ['house', 'lucide'],
127
+ ['briefcase', 'lucide'],
128
+ ['building', 'lucide'],
129
+ ['building-2', 'lucide'],
130
+ ['store', 'lucide'],
131
+ ['landmark', 'lucide'],
132
+ ['map-pin', 'lucide'],
133
+ ['compass', 'lucide'],
134
+ ['map', 'lucide'],
135
+ ['globe-2', 'lucide'],
136
+ ['mountain', 'lucide'],
137
+ ],
138
+ 'Media & Comms': [
139
+ ['monitor', 'lucide'],
140
+ ['laptop', 'lucide'],
141
+ ['smartphone', 'lucide'],
142
+ ['mail', 'lucide'],
143
+ ['message-circle', 'lucide'],
144
+ ['bell', 'lucide'],
145
+ ['phone', 'lucide'],
146
+ ['mic', 'lucide'],
147
+ ['music', 'lucide'],
148
+ ['headphones', 'lucide'],
149
+ ['video', 'lucide'],
150
+ ['radio', 'lucide'],
151
+ ],
152
+ 'Nature & Fun': [
153
+ ['sun', 'lucide'],
154
+ ['moon', 'lucide'],
155
+ ['cloud-sun', 'lucide'],
156
+ ['flame', 'lucide'],
157
+ ['waves', 'lucide'],
158
+ ['coffee', 'lucide'],
159
+ ['leaf', 'lucide'],
160
+ ['flower', 'lucide'],
161
+ ['flower-2', 'lucide'],
162
+ ['tree-pine', 'lucide'],
163
+ ['trees', 'lucide'],
164
+ ['snail', 'lucide'],
165
+ ['fish', 'lucide'],
166
+ ['rabbit', 'lucide'],
167
+ ['turtle', 'lucide'],
168
+ ['bird', 'lucide'],
169
+ ['dog', 'lucide'],
170
+ ['cat', 'lucide'],
171
+ ],
172
+ 'Lab Extras': [
173
+ ['owl', 'lab'],
174
+ ['planet', 'lab'],
175
+ ['bee', 'lab'],
176
+ ['venn', 'lab'],
177
+ ['copy-code', 'lab'],
178
+ ['grid-lines', 'lab'],
179
+ ['farm', 'lab'],
180
+ ['toolbox', 'lab'],
181
+ ['toolbox-2', 'lab'],
182
+ ['snowman', 'lab'],
183
+ ['mountain-snow', 'lab'],
184
+ ],
185
+ };
186
+
187
+ // ── SVG serializer ────────────────────────────────────────────────────────────
188
+ function iconDataToSvg(iconData) {
189
+ const children = iconData.map(([tag, attrs]) => {
190
+ // Filter out non-SVG metadata attrs ('key' is a React/lucide internal)
191
+ const entries = Object.entries(attrs).filter(([k]) => k !== 'key');
192
+ const attrStr = entries.map(([k, v]) => `${k}="${v}"`).join(' ');
193
+ return `<${tag} ${attrStr}/>`;
194
+ }).join('');
195
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${children}</svg>`;
196
+ }
197
+
198
+ // ── Main ──────────────────────────────────────────────────────────────────────
199
+ const icons = {}; // { "folder": "<svg>...</svg>", ... }
200
+ const categories = {}; // { "Folders & Files": ["folder", "folder-open", ...], ... }
201
+ const skipped = [];
202
+
203
+ for (const [category, entries] of Object.entries(ICON_CATEGORIES)) {
204
+ const catIcons = [];
205
+ for (const [name, pkg] of entries) {
206
+ // De-dup (e.g. 'cpu' appears twice in the list above)
207
+ if (icons[name]) { catIcons.push(name); continue; }
208
+
209
+ const iconDir = pkg === 'lab'
210
+ ? resolve(ROOT, `node_modules/@lucide/lab/dist/esm/icons/${name}.js`)
211
+ : resolve(ROOT, `node_modules/lucide/dist/esm/icons/${name}.js`);
212
+
213
+ try {
214
+ const src = readFileSync(iconDir, 'utf8');
215
+ // Extract: const <Name> = <array>;\nexport { <Name> as default }
216
+ // Use greedy match up to the last ";" before the export statement
217
+ const match = src.match(/const \w+ = ([\s\S]+?);\s*\nexport/);
218
+ if (!match) { skipped.push(`${name} (parse fail)`); continue; }
219
+ // Evaluate JS object literal (has unquoted keys, not valid JSON)
220
+ const iconData = vm.runInNewContext(`(${match[1]})`);
221
+ icons[name] = iconDataToSvg(iconData);
222
+ catIcons.push(name);
223
+ } catch {
224
+ skipped.push(`${name} (${pkg})`);
225
+ }
226
+ }
227
+ if (catIcons.length > 0) categories[category] = catIcons;
228
+ }
229
+
230
+ // ── Output ────────────────────────────────────────────────────────────────────
231
+ mkdirSync(resolve(ROOT, 'src/web/public/vendor'), { recursive: true });
232
+
233
+ const output = `/* lucide icons bundle — generated by scripts/build-lucide-bundle.mjs */
234
+ /* Includes ${Object.keys(icons).length} icons from lucide + @lucide/lab */
235
+ (function() {
236
+ window.__lucideIcons = ${JSON.stringify(icons)};
237
+ window.__lucideIconCategories = ${JSON.stringify(categories)};
238
+ document.dispatchEvent(new Event('lucide-ready'));
239
+ })();
240
+ `;
241
+
242
+ writeFileSync(OUT, output);
243
+ console.log(`✓ Built ${Object.keys(icons).length} icons → src/web/public/vendor/lucide.bundle.js`);
244
+ if (skipped.length) console.log(` Skipped (not found): ${skipped.join(', ')}`);
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script: generates public/vendor/material-icons.bundle.js
4
+ * Includes ALL ~2191 icons from @material-icons/svg (baseline/filled variant),
5
+ * organized by their official categories from data.json.
6
+ *
7
+ * Icon names are stored with a "mi/" prefix when used in ws.icon so they
8
+ * can coexist with Lucide icon names without collision.
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
+ import { fileURLToPath } from 'url';
13
+ import { dirname, resolve } from 'path';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = resolve(__dirname, '..');
17
+ const ICONS_DIR = resolve(ROOT, 'node_modules/@material-icons/svg/svg');
18
+ const DATA_JSON = resolve(ROOT, 'node_modules/@material-icons/svg/data.json');
19
+ const OUT = resolve(ROOT, 'src/web/public/vendor/material-icons.bundle.js');
20
+
21
+ // Official category short-name → display name
22
+ const CATEGORY_LABELS = {
23
+ action: 'Action',
24
+ alert: 'Alert',
25
+ av: 'Audio & Video',
26
+ communication:'Communication',
27
+ content: 'Content',
28
+ device: 'Device',
29
+ editor: 'Editor',
30
+ file: 'File',
31
+ hardware: 'Hardware',
32
+ home: 'Home',
33
+ image: 'Image',
34
+ maps: 'Maps',
35
+ math: 'Math',
36
+ navigation: 'Navigation',
37
+ notification: 'Notification',
38
+ places: 'Places',
39
+ search: 'Search',
40
+ shopping: 'Shopping',
41
+ social: 'Social',
42
+ toggle: 'Toggle',
43
+ };
44
+
45
+ // ── SVG processor ─────────────────────────────────────────────────────────────
46
+ function processSvg(raw) {
47
+ return raw
48
+ .replace(/width="24"/, 'width="14"')
49
+ .replace(/height="24"/, 'height="14"')
50
+ // Make color inherit from CSS (without this, icons always render black)
51
+ .replace('<svg ', '<svg fill="currentColor" ');
52
+ }
53
+
54
+ // ── Load icon metadata ────────────────────────────────────────────────────────
55
+ const data = JSON.parse(readFileSync(DATA_JSON, 'utf8'));
56
+ const iconMeta = data.icons; // [{ name, categories: [string], tags: [string] }]
57
+
58
+ // Build category → [name] map using official categories
59
+ const categoryMap = {}; // { 'Audio & Video': ['10k', ...], ... }
60
+ for (const icon of iconMeta) {
61
+ for (const cat of (icon.categories || [])) {
62
+ const label = CATEGORY_LABELS[cat] || cat;
63
+ if (!categoryMap[label]) categoryMap[label] = [];
64
+ categoryMap[label].push(icon.name);
65
+ }
66
+ }
67
+
68
+ // ── Process all icons ─────────────────────────────────────────────────────────
69
+ const icons = {}; // { name: '<svg>...' }
70
+ const categories = {}; // { 'Audio & Video': ['10k', ...] }
71
+ const skipped = [];
72
+
73
+ for (const icon of iconMeta) {
74
+ const svgPath = resolve(ICONS_DIR, icon.name, 'baseline.svg');
75
+ if (!existsSync(svgPath)) {
76
+ skipped.push(icon.name);
77
+ continue;
78
+ }
79
+ try {
80
+ const raw = readFileSync(svgPath, 'utf8').trim();
81
+ icons[icon.name] = processSvg(raw);
82
+ } catch {
83
+ skipped.push(icon.name);
84
+ }
85
+ }
86
+
87
+ // Build final categories (only include icons that were successfully loaded)
88
+ for (const [label, names] of Object.entries(categoryMap)) {
89
+ const valid = names.filter(n => icons[n]);
90
+ if (valid.length > 0) categories[label] = valid;
91
+ }
92
+
93
+ // ── Output ────────────────────────────────────────────────────────────────────
94
+ mkdirSync(resolve(ROOT, 'src/web/public/vendor'), { recursive: true });
95
+
96
+ const output = `/* Material Icons bundle — generated by scripts/build-material-bundle.mjs */
97
+ /* Includes ${Object.keys(icons).length} icons from @material-icons/svg (baseline variant) */
98
+ /* Icon names are stored as "mi/<name>" in workspace records to avoid Lucide collisions */
99
+ (function() {
100
+ window.__materialIcons = ${JSON.stringify(icons)};
101
+ window.__materialIconCategories = ${JSON.stringify(categories)};
102
+ document.dispatchEvent(new Event('material-icons-ready'));
103
+ })();
104
+ `;
105
+
106
+ writeFileSync(OUT, output);
107
+ console.log(`✓ Built ${Object.keys(icons).length} Material icons across ${Object.keys(categories).length} categories → src/web/public/vendor/material-icons.bundle.js`);
108
+ if (skipped.length) console.log(` Skipped (not found): ${skipped.join(', ')}`);
@@ -82,7 +82,8 @@ async function listIssues(cwd, filters = {}, binary = DEFAULT_TD_BINARY) {
82
82
  if (filters.status) args.push('--status', filters.status);
83
83
  const { stdout } = await runTd(binary, args, cwd);
84
84
  const parsed = JSON.parse(stdout);
85
- // td --json returns either an array or { issues: [...] }
85
+ // td --json returns either an array, { issues: [...] }, or null (empty repo)
86
+ if (!parsed) return [];
86
87
  return Array.isArray(parsed) ? parsed : (parsed.issues || []);
87
88
  }
88
89
 
@@ -452,7 +452,7 @@ class Store extends EventEmitter {
452
452
 
453
453
  // ─── Workspace CRUD ──────────────────────────────────────
454
454
 
455
- createWorkspace({ name, description = '', color = 'cyan' }) {
455
+ createWorkspace({ name, description = '', color = 'cyan', icon = null }) {
456
456
  const id = crypto.randomUUID();
457
457
  const now = new Date().toISOString();
458
458
  const workspace = {
@@ -460,6 +460,7 @@ class Store extends EventEmitter {
460
460
  name,
461
461
  description,
462
462
  color,
463
+ icon,
463
464
  sessions: [],
464
465
  createdAt: now,
465
466
  lastActive: now,