@mmmbuto/nexuscrew 0.2.5 → 0.4.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.
Files changed (94) hide show
  1. package/README.md +63 -163
  2. package/bin/nexuscrew.js +1 -296
  3. package/frontend/dist/assets/index-B41xYEDb.js +81 -0
  4. package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
  5. package/frontend/dist/icon-maskable.svg +4 -0
  6. package/frontend/dist/icon.svg +4 -0
  7. package/frontend/dist/index.html +11 -3
  8. package/frontend/dist/manifest.json +24 -0
  9. package/frontend/dist/sw.js +3 -0
  10. package/frontend/index.html +20 -0
  11. package/lib/auth/token.js +26 -0
  12. package/lib/config.js +25 -0
  13. package/lib/pty/attach.js +30 -0
  14. package/lib/pty/provider.js +33 -0
  15. package/lib/server.js +62 -0
  16. package/lib/tmux/actions.js +27 -0
  17. package/lib/tmux/list.js +33 -0
  18. package/lib/ws/bridge.js +66 -0
  19. package/package.json +20 -24
  20. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  24. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  25. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  26. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  27. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  28. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  29. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  30. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  32. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  36. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  37. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  38. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  39. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  40. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  42. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  47. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  58. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  59. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  61. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  62. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  63. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  64. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  65. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  66. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  67. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  68. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  69. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  70. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  71. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  72. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  73. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  74. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  75. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  76. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  77. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  78. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  79. package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
  80. package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
  81. package/lib/config/paths.js +0 -30
  82. package/lib/server/routes/hosts.js +0 -82
  83. package/lib/server/routes/launchers.js +0 -17
  84. package/lib/server/routes/models.js +0 -26
  85. package/lib/server/routes/runtime.js +0 -14
  86. package/lib/server/routes/send.js +0 -236
  87. package/lib/server/routes/sessions.js +0 -104
  88. package/lib/server/routes/status.js +0 -62
  89. package/lib/server/routes/tmux.js +0 -91
  90. package/lib/server/server.js +0 -189
  91. package/lib/services/engine-discovery.js +0 -317
  92. package/lib/services/log-watcher.js +0 -279
  93. package/lib/services/session-store.js +0 -198
  94. package/lib/services/tmux-manager.js +0 -288
@@ -1,317 +0,0 @@
1
- /**
2
- * LauncherDiscovery — environment-agnostic launcher detection
3
- *
4
- * Discovers launchers from:
5
- * - the current runtime shell via live introspection
6
- * - generic shell rc/profile files as fallback
7
- * - PATH binaries
8
- *
9
- * No user-specific config is assumed and no model catalog is hardcoded.
10
- */
11
-
12
- const path = require('path');
13
- const fs = require('fs');
14
- const os = require('os');
15
-
16
- const KNOWN_FAMILIES = ['claude', 'codex', 'gemini', 'qwen'];
17
-
18
- class EngineDiscovery {
19
- constructor(config = {}) {
20
- this.preferredShell = config.preferredShell || '';
21
- this.extraShellSources = Array.isArray(config.extraShellSources) ? config.extraShellSources : [];
22
- this.tmux = config.tmux || null;
23
- this._cache = new Map();
24
- }
25
-
26
- discover(host = null) {
27
- const hostKey = this._hostKey(host);
28
- if (this._cache.has(hostKey)) return this._cache.get(hostKey);
29
-
30
- const runtime = this.getRuntimeInfo(host);
31
- const shellPath = runtime.shell || this.getShellPath();
32
- const shellName = path.basename(shellPath || '/bin/sh');
33
- const launchers = [];
34
- const seen = new Set();
35
-
36
- const addLauncher = (launcher) => {
37
- const id = launcher.id || `${launcher.kind}:${launcher.command}`;
38
- if (!launcher.command || seen.has(id)) return;
39
- seen.add(id);
40
- launchers.push({
41
- id,
42
- label: launcher.label || launcher.name || launcher.command,
43
- family: launcher.family || this._detectFamily(launcher.name || launcher.command),
44
- kind: launcher.kind,
45
- command: launcher.command,
46
- commandPreview: launcher.commandPreview || launcher.command,
47
- source: launcher.source,
48
- shell: launcher.shell || shellName,
49
- available: launcher.available !== false,
50
- runnable: launcher.runnable !== false,
51
- bucket: launcher.bucket || this._classifyBucket(launcher)
52
- });
53
- };
54
-
55
- this._discoverPathBinaries(host).forEach(addLauncher);
56
- this._discoverLiveShell(shellPath, host).forEach(addLauncher);
57
- this._discoverShellFiles(shellName, host).forEach(addLauncher);
58
-
59
- launchers.sort((a, b) => a.label.localeCompare(b.label));
60
- const discovery = {
61
- shell: shellPath,
62
- homeDir: runtime.homeDir,
63
- platform: runtime.platform,
64
- host: hostKey,
65
- launchers,
66
- buckets: this._bucketize(launchers)
67
- };
68
- this._cache.set(hostKey, discovery);
69
- return discovery;
70
- }
71
-
72
- getLaunchers(host = null) {
73
- return this.discover(host).launchers;
74
- }
75
-
76
- getBuckets(host = null) {
77
- return this.discover(host).buckets;
78
- }
79
-
80
- getLauncher(id, host = null) {
81
- return this.getLaunchers(host).find((launcher) => launcher.id === id) || null;
82
- }
83
-
84
- getShellPath() {
85
- return this.preferredShell || process.env.SHELL || '/bin/sh';
86
- }
87
-
88
- getRuntimeInfo(host = null) {
89
- if (this.tmux) {
90
- return this.tmux.getRuntimeInfo(host);
91
- }
92
- return {
93
- homeDir: os.homedir(),
94
- shell: this.getShellPath(),
95
- platform: process.platform,
96
- host: this._hostKey(host)
97
- };
98
- }
99
-
100
- clearCache() {
101
- this._cache.clear();
102
- }
103
-
104
- _discoverPathBinaries(host = null) {
105
- return KNOWN_FAMILIES.flatMap((family) => {
106
- const binPath = this._resolveCommand(family, host);
107
- if (!binPath) return [];
108
- return [{
109
- id: `binary:${family}`,
110
- label: family,
111
- family,
112
- kind: 'binary',
113
- command: binPath,
114
- commandPreview: binPath,
115
- source: 'path',
116
- runnable: true
117
- }];
118
- });
119
- }
120
-
121
- _discoverLiveShell(shellPath, host = null) {
122
- const shellName = path.basename(shellPath);
123
- if (!this._fileExists(shellPath, host)) return [];
124
-
125
- try {
126
- const script = [
127
- 'alias -p 2>/dev/null || true',
128
- 'printf "\\n__NEXUSCREW_FUNCTIONS__\\n"',
129
- 'typeset -f 2>/dev/null || declare -f 2>/dev/null || true'
130
- ].join('; ');
131
-
132
- const output = this._runInteractiveShell(shellPath, shellName, script, host);
133
- return this._parseShellDump(output, shellName, 'live-shell');
134
- } catch {
135
- return [];
136
- }
137
- }
138
-
139
- _discoverShellFiles(shellName, host = null) {
140
- const homeDir = this.getRuntimeInfo(host).homeDir || os.homedir();
141
- const shellFiles = new Set();
142
-
143
- for (const candidate of this._defaultShellFiles(shellName)) {
144
- shellFiles.add(path.join(homeDir, candidate));
145
- }
146
- for (const candidate of this.extraShellSources) {
147
- if (candidate) shellFiles.add(candidate);
148
- }
149
-
150
- const discovered = [];
151
- for (const filePath of shellFiles) {
152
- if (!this._fileExists(filePath, host)) continue;
153
- try {
154
- const content = this._readFile(filePath, host);
155
- discovered.push(...this._parseShellFile(content, shellName, filePath));
156
- } catch {}
157
- }
158
- return discovered;
159
- }
160
-
161
- _parseShellDump(content, shellName, source) {
162
- const [aliasSection = '', functionSection = ''] = content.split('\n__NEXUSCREW_FUNCTIONS__\n');
163
- return [
164
- ...this._parseAliases(aliasSection, shellName, source),
165
- ...this._parseFunctions(functionSection, shellName, source)
166
- ];
167
- }
168
-
169
- _parseShellFile(content, shellName, filePath) {
170
- return [
171
- ...this._parseAliases(content, shellName, `shell-file:${filePath}`),
172
- ...this._parseFunctions(content, shellName, `shell-file:${filePath}`)
173
- ];
174
- }
175
-
176
- _parseAliases(content, shellName, source) {
177
- const aliases = [];
178
- const regex = /^alias\s+([A-Za-z0-9_.:-]+)=['"]([^'"]+)['"]$/gm;
179
- let match;
180
- while ((match = regex.exec(content)) !== null) {
181
- const name = match[1];
182
- const body = match[2].trim();
183
- if (!this._isLauncherCandidate(name, body)) continue;
184
- aliases.push({
185
- id: `alias:${name}`,
186
- label: name,
187
- name,
188
- family: this._detectFamily(name, body),
189
- kind: 'alias',
190
- command: name,
191
- commandPreview: body,
192
- source,
193
- shell: shellName,
194
- runnable: source === 'live-shell'
195
- });
196
- }
197
- return aliases;
198
- }
199
-
200
- _parseFunctions(content, shellName, source) {
201
- const functions = [];
202
- const regex = /^([A-Za-z0-9_.:-]+)\s*(?:\(\))?\s*\{/gm;
203
- let match;
204
- while ((match = regex.exec(content)) !== null) {
205
- const name = match[1];
206
- if (!this._isLauncherCandidate(name, name)) continue;
207
- functions.push({
208
- id: `function:${name}`,
209
- label: name,
210
- name,
211
- family: this._detectFamily(name),
212
- kind: 'function',
213
- command: name,
214
- commandPreview: `${name}()`,
215
- source,
216
- shell: shellName,
217
- runnable: source === 'live-shell'
218
- });
219
- }
220
- return functions;
221
- }
222
-
223
- _isLauncherCandidate(name, body = '') {
224
- const haystack = `${name} ${body}`.toLowerCase();
225
- return KNOWN_FAMILIES.some((family) => haystack.includes(family));
226
- }
227
-
228
- _detectFamily(name, body = '') {
229
- const haystack = `${name || ''} ${body}`.toLowerCase();
230
- return KNOWN_FAMILIES.find((family) => haystack.includes(family)) || 'custom';
231
- }
232
-
233
- _resolveCommand(command, host = null) {
234
- if (this.tmux) {
235
- return this.tmux.resolveCommand(command, host);
236
- }
237
- try {
238
- const { execSync } = require('child_process');
239
- const resolved = execSync(`command -v ${command} 2>/dev/null`, {
240
- encoding: 'utf8',
241
- stdio: ['ignore', 'pipe', 'ignore']
242
- }).trim();
243
- return resolved || null;
244
- } catch {
245
- return null;
246
- }
247
- }
248
-
249
- _defaultShellFiles(shellName) {
250
- switch (shellName) {
251
- case 'zsh':
252
- return ['.zshrc', '.zprofile', '.zshenv'];
253
- case 'bash':
254
- return ['.bashrc', '.bash_profile', '.profile'];
255
- default:
256
- return ['.profile'];
257
- }
258
- }
259
-
260
- _shellArgs(shellName, script) {
261
- if (shellName === 'bash' || shellName === 'zsh') {
262
- return ['-ilc', script];
263
- }
264
- return ['-lc', script];
265
- }
266
-
267
- _runInteractiveShell(shellPath, shellName, script, host = null) {
268
- if (this.tmux) {
269
- return this.tmux.runInteractiveShell(shellPath, script, host);
270
- }
271
- const { execFileSync } = require('child_process');
272
- return execFileSync(shellPath, this._shellArgs(shellName, script), {
273
- encoding: 'utf8',
274
- stdio: ['ignore', 'pipe', 'pipe']
275
- });
276
- }
277
-
278
- _fileExists(filePath, host = null) {
279
- if (this.tmux) {
280
- return this.tmux.fileExists(filePath, host);
281
- }
282
- return fs.existsSync(filePath);
283
- }
284
-
285
- _readFile(filePath, host = null) {
286
- if (this.tmux) {
287
- return this.tmux.readFile(filePath, host);
288
- }
289
- return fs.readFileSync(filePath, 'utf8');
290
- }
291
-
292
- _hostKey(host) {
293
- if (!host || host === 'local') return 'local';
294
- if (typeof host === 'string') return host;
295
- return host.name || 'remote';
296
- }
297
-
298
- _classifyBucket(launcher) {
299
- const name = String(launcher.label || launcher.name || launcher.command || '').trim();
300
- if (name.startsWith('_')) return 'internal';
301
- if (launcher.kind === 'function' && /(^_|_bin$|_run$|_cmd$|_provider_|_chutes$)/.test(name)) {
302
- return 'internal';
303
- }
304
- if (launcher.runnable === false) return 'detectedOnly';
305
- return 'runnable';
306
- }
307
-
308
- _bucketize(launchers) {
309
- return {
310
- runnable: launchers.filter((launcher) => launcher.bucket === 'runnable'),
311
- detectedOnly: launchers.filter((launcher) => launcher.bucket === 'detectedOnly'),
312
- internal: launchers.filter((launcher) => launcher.bucket === 'internal')
313
- };
314
- }
315
- }
316
-
317
- module.exports = EngineDiscovery;
@@ -1,279 +0,0 @@
1
- /**
2
- * LogWatcher — Monitor tmux tee log files for structured output
3
- *
4
- * Watches log files created by `tee` in tmux windows.
5
- * Parses output per-engine and emits events.
6
- */
7
-
8
- const fs = require('fs');
9
- const path = require('path');
10
- const { EventEmitter } = require('events');
11
-
12
- class LogWatcher extends EventEmitter {
13
- constructor() {
14
- super();
15
- this.watchers = new Map(); // logPath → { watcher, offset, parser }
16
- this.pollIntervals = new Map(); // logPath → intervalId
17
- }
18
-
19
- /**
20
- * Start watching a log file
21
- * @param {string} logPath - Path to the tee log file
22
- * @param {string} engine - Engine type (claude, codex, gemini, qwen)
23
- * @param {string} conversationId - Associated conversation
24
- */
25
- watch(logPath, engine, conversationId) {
26
- if (this.watchers.has(logPath)) return;
27
-
28
- const state = {
29
- offset: 0,
30
- engine,
31
- conversationId,
32
- buffer: '',
33
- parser: this._getParser(engine)
34
- };
35
-
36
- // Read existing content (in case log already has data)
37
- if (fs.existsSync(logPath)) {
38
- state.offset = fs.statSync(logPath).size;
39
- }
40
-
41
- // Poll-based watching (more reliable than fs.watch for log files)
42
- const intervalId = setInterval(() => this._poll(logPath, state), 200);
43
- this.pollIntervals.set(logPath, intervalId);
44
-
45
- // Also use fs.watch for immediate responsiveness
46
- try {
47
- const watcher = fs.watch(path.dirname(logPath), (eventType, filename) => {
48
- if (filename === path.basename(logPath)) {
49
- this._poll(logPath, state);
50
- }
51
- });
52
- this.watchers.set(logPath, { ...state, watcher });
53
- } catch {
54
- // fs.watch may fail in some environments, polling still works
55
- this.watchers.set(logPath, state);
56
- }
57
-
58
- this.emit('watching', { logPath, engine, conversationId });
59
- }
60
-
61
- /**
62
- * Stop watching a log file
63
- */
64
- unwatch(logPath) {
65
- const state = this.watchers.get(logPath);
66
- if (state?.watcher) state.watcher.close();
67
- this.watchers.delete(logPath);
68
-
69
- const intervalId = this.pollIntervals.get(logPath);
70
- if (intervalId) {
71
- clearInterval(intervalId);
72
- this.pollIntervals.delete(logPath);
73
- }
74
- }
75
-
76
- /**
77
- * Stop all watchers
78
- */
79
- unwatchAll() {
80
- for (const logPath of this.watchers.keys()) {
81
- this.unwatch(logPath);
82
- }
83
- }
84
-
85
- // --- Polling ---
86
-
87
- _poll(logPath, state) {
88
- try {
89
- if (!fs.existsSync(logPath)) return;
90
-
91
- const stat = fs.statSync(logPath);
92
- if (stat.size <= state.offset) return;
93
-
94
- const fd = fs.openSync(logPath, 'r');
95
- const buf = Buffer.alloc(stat.size - state.offset);
96
- fs.readSync(fd, buf, 0, buf.length, state.offset);
97
- fs.closeSync(fd);
98
-
99
- state.offset = stat.size;
100
- const chunk = buf.toString('utf8');
101
- state.buffer += chunk;
102
-
103
- // Process complete lines
104
- this._processBuffer(state);
105
- } catch (err) {
106
- // File might be temporarily unavailable
107
- }
108
- }
109
-
110
- _processBuffer(state) {
111
- const lines = state.buffer.split('\n');
112
- state.buffer = lines.pop() || ''; // Keep incomplete line in buffer
113
-
114
- for (const line of lines) {
115
- if (!line.trim()) continue;
116
- try {
117
- const events = state.parser(line, state);
118
- for (const event of events) {
119
- this.emit('data', {
120
- ...event,
121
- conversationId: state.conversationId,
122
- engine: state.engine
123
- });
124
- }
125
- } catch {
126
- // Emit raw text if parser fails
127
- this.emit('data', {
128
- type: 'raw',
129
- content: line,
130
- conversationId: state.conversationId,
131
- engine: state.engine
132
- });
133
- }
134
- }
135
- }
136
-
137
- // --- Parsers ---
138
-
139
- _getParser(engine) {
140
- switch (engine) {
141
- case 'claude': return this._parseClaude.bind(this);
142
- case 'codex': return this._parseCodex.bind(this);
143
- case 'gemini': return this._parseGemini.bind(this);
144
- case 'qwen': return this._parseQwen.bind(this);
145
- default: return this._parseGeneric.bind(this);
146
- }
147
- }
148
-
149
- /**
150
- * Claude Code JSON output parser
151
- * Claude emits JSON objects per-line when --output-format json is used
152
- */
153
- _parseClaude(line, state) {
154
- const events = [];
155
-
156
- // Try JSON parse (Claude --output-format json)
157
- try {
158
- const obj = JSON.parse(line);
159
-
160
- if (obj.type === 'assistant') {
161
- events.push({ type: 'status', content: 'thinking...' });
162
- } else if (obj.type === 'content_block_delta') {
163
- events.push({ type: 'chunk', content: obj.delta?.text || '' });
164
- } else if (obj.type === 'message_stop') {
165
- events.push({ type: 'done' });
166
- } else if (obj.type === 'tool_use') {
167
- events.push({ type: 'status', content: `tool: ${obj.name || 'unknown'}` });
168
- } else if (obj.result !== undefined) {
169
- // Final result
170
- events.push({
171
- type: 'message_done',
172
- content: typeof obj.result === 'string' ? obj.result : JSON.stringify(obj.result),
173
- usage: obj.usage
174
- });
175
- }
176
- return events;
177
- } catch {}
178
-
179
- // Fallback: plain text parsing
180
- if (line.includes('thinking...') || line.includes('Thinking...')) {
181
- events.push({ type: 'status', content: 'thinking...' });
182
- } else if (line.includes('tool:')) {
183
- events.push({ type: 'status', content: line.trim() });
184
- } else if (line.trim()) {
185
- events.push({ type: 'chunk', content: line });
186
- }
187
-
188
- return events;
189
- }
190
-
191
- /**
192
- * Codex exec --json output parser (JSONL)
193
- */
194
- _parseCodex(line) {
195
- const events = [];
196
-
197
- try {
198
- const obj = JSON.parse(line);
199
- if (obj.type === 'status') {
200
- events.push({ type: 'status', content: obj.message || obj.status });
201
- } else if (obj.type === 'response' || obj.type === 'result') {
202
- events.push({
203
- type: 'message_done',
204
- content: obj.content || obj.result || obj.text || '',
205
- usage: obj.usage
206
- });
207
- } else if (obj.type === 'tool_use' || obj.type === 'patch') {
208
- events.push({ type: 'status', content: `${obj.type}: ${obj.name || obj.path || ''}` });
209
- } else if (obj.delta) {
210
- events.push({ type: 'chunk', content: obj.delta });
211
- } else if (obj.content) {
212
- events.push({ type: 'chunk', content: typeof obj.content === 'string' ? obj.content : JSON.stringify(obj.content) });
213
- }
214
- } catch {
215
- if (line.trim()) {
216
- events.push({ type: 'chunk', content: line });
217
- }
218
- }
219
-
220
- return events;
221
- }
222
-
223
- /**
224
- * Gemini output parser
225
- */
226
- _parseGemini(line) {
227
- const events = [];
228
-
229
- try {
230
- const obj = JSON.parse(line);
231
- if (obj.type === 'status') {
232
- events.push({ type: 'status', content: obj.message });
233
- } else if (obj.content) {
234
- events.push({ type: 'chunk', content: obj.content });
235
- }
236
- } catch {
237
- // Detect Gemini status markers
238
- if (line.match(/^(⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏)/)) {
239
- events.push({ type: 'status', content: line.trim() });
240
- } else if (line.trim()) {
241
- events.push({ type: 'chunk', content: line });
242
- }
243
- }
244
-
245
- return events;
246
- }
247
-
248
- /**
249
- * Qwen output parser
250
- */
251
- _parseQwen(line) {
252
- const events = [];
253
-
254
- try {
255
- const obj = JSON.parse(line);
256
- if (obj.type === 'status') {
257
- events.push({ type: 'status', content: obj.message });
258
- } else if (obj.content) {
259
- events.push({ type: 'chunk', content: obj.content });
260
- }
261
- } catch {
262
- if (line.trim()) {
263
- events.push({ type: 'chunk', content: line });
264
- }
265
- }
266
-
267
- return events;
268
- }
269
-
270
- /**
271
- * Generic fallback parser
272
- */
273
- _parseGeneric(line) {
274
- if (!line.trim()) return [];
275
- return [{ type: 'chunk', content: line }];
276
- }
277
- }
278
-
279
- module.exports = LogWatcher;