@mmmbuto/nexuscrew 0.2.4 → 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.
- package/README.md +63 -163
- package/bin/nexuscrew.js +1 -296
- package/frontend/dist/assets/index-B41xYEDb.js +81 -0
- package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
- package/frontend/dist/icon-maskable.svg +4 -0
- package/frontend/dist/icon.svg +4 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/manifest.json +24 -0
- package/frontend/dist/sw.js +3 -0
- package/frontend/index.html +20 -0
- package/lib/auth/token.js +26 -0
- package/lib/config.js +25 -0
- package/lib/pty/attach.js +30 -0
- package/lib/pty/provider.js +33 -0
- package/lib/server.js +62 -0
- package/lib/tmux/actions.js +27 -0
- package/lib/tmux/list.js +33 -0
- package/lib/ws/bridge.js +66 -0
- package/package.json +20 -24
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
- package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
- package/lib/config/paths.js +0 -30
- package/lib/server/routes/hosts.js +0 -82
- package/lib/server/routes/launchers.js +0 -17
- package/lib/server/routes/models.js +0 -26
- package/lib/server/routes/runtime.js +0 -14
- package/lib/server/routes/send.js +0 -236
- package/lib/server/routes/sessions.js +0 -104
- package/lib/server/routes/status.js +0 -62
- package/lib/server/routes/tmux.js +0 -91
- package/lib/server/server.js +0 -189
- package/lib/services/engine-discovery.js +0 -317
- package/lib/services/log-watcher.js +0 -279
- package/lib/services/session-store.js +0 -198
- 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;
|