triflux 3.2.0-dev.8 → 3.2.0-dev.9
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/bin/triflux.mjs +581 -340
- package/hooks/keyword-rules.json +16 -0
- package/hub/bridge.mjs +410 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +512 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +59 -1459
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +12 -80
- package/hub/team/nativeProxy.mjs +121 -47
- package/hub/team/pane.mjs +66 -43
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +354 -291
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +41 -52
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +4 -2
- package/package.json +4 -1
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +128 -70
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +415 -80
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +53 -62
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// hub/workers/claude-worker.mjs — Claude stream-json subprocess 래퍼
|
|
2
|
+
// ADR-007: --input-format/--output-format stream-json 기반 세션 워커.
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
8
|
+
const DEFAULT_KILL_GRACE_MS = 1000;
|
|
9
|
+
|
|
10
|
+
function toStringList(value) {
|
|
11
|
+
if (!Array.isArray(value)) return [];
|
|
12
|
+
return value
|
|
13
|
+
.map((item) => String(item ?? '').trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeJsonParse(line) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function appendTextFragments(value, parts) {
|
|
26
|
+
if (value == null) return;
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (trimmed) parts.push(trimmed);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
for (const item of value) appendTextFragments(item, parts);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'object') {
|
|
37
|
+
if (typeof value.text === 'string') appendTextFragments(value.text, parts);
|
|
38
|
+
if (typeof value.result === 'string') appendTextFragments(value.result, parts);
|
|
39
|
+
if (typeof value.content === 'string' || Array.isArray(value.content) || value.content) {
|
|
40
|
+
appendTextFragments(value.content, parts);
|
|
41
|
+
}
|
|
42
|
+
if (typeof value.message === 'string' || Array.isArray(value.message) || value.message) {
|
|
43
|
+
appendTextFragments(value.message, parts);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractText(event) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
appendTextFragments(event, parts);
|
|
51
|
+
return parts.join('\n').trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findSessionId(event) {
|
|
55
|
+
return event?.session_id
|
|
56
|
+
|| event?.sessionId
|
|
57
|
+
|| event?.message?.session_id
|
|
58
|
+
|| event?.message?.sessionId
|
|
59
|
+
|| null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createWorkerError(message, details = {}) {
|
|
63
|
+
const error = new Error(message);
|
|
64
|
+
Object.assign(error, details);
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildClaudeArgs(worker, options) {
|
|
69
|
+
const args = [...worker.commandArgs];
|
|
70
|
+
|
|
71
|
+
args.push('--print');
|
|
72
|
+
args.push('--input-format', 'stream-json');
|
|
73
|
+
args.push('--output-format', 'stream-json');
|
|
74
|
+
|
|
75
|
+
if (options.includePartialMessages) args.push('--include-partial-messages');
|
|
76
|
+
if (options.replayUserMessages) args.push('--replay-user-messages');
|
|
77
|
+
if (options.model) args.push('--model', options.model);
|
|
78
|
+
if (options.allowDangerouslySkipPermissions) args.push('--dangerously-skip-permissions');
|
|
79
|
+
if (options.permissionMode) args.push('--permission-mode', options.permissionMode);
|
|
80
|
+
|
|
81
|
+
for (const config of toStringList(options.mcpConfig)) {
|
|
82
|
+
args.push('--mcp-config', config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (worker.resumeSessionId) {
|
|
86
|
+
args.push('--resume', worker.resumeSessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
args.push(...toStringList(options.extraArgs));
|
|
90
|
+
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Claude stream-json 세션 워커
|
|
96
|
+
*/
|
|
97
|
+
export class ClaudeWorker {
|
|
98
|
+
type = 'claude';
|
|
99
|
+
|
|
100
|
+
constructor(options = {}) {
|
|
101
|
+
this.command = options.command || 'claude';
|
|
102
|
+
this.commandArgs = toStringList(options.commandArgs || options.args);
|
|
103
|
+
this.cwd = options.cwd || process.cwd();
|
|
104
|
+
this.env = { ...process.env, ...(options.env || {}) };
|
|
105
|
+
this.model = options.model || null;
|
|
106
|
+
this.permissionMode = options.permissionMode || null;
|
|
107
|
+
this.allowDangerouslySkipPermissions = options.allowDangerouslySkipPermissions !== false;
|
|
108
|
+
this.includePartialMessages = options.includePartialMessages === true;
|
|
109
|
+
this.replayUserMessages = options.replayUserMessages !== false;
|
|
110
|
+
this.mcpConfig = toStringList(options.mcpConfig);
|
|
111
|
+
this.extraArgs = toStringList(options.extraArgs);
|
|
112
|
+
this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
|
|
113
|
+
this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
|
|
114
|
+
this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
|
|
115
|
+
this.controlRequestHandler = typeof options.controlRequestHandler === 'function'
|
|
116
|
+
? options.controlRequestHandler
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
this.state = 'idle';
|
|
120
|
+
this.child = null;
|
|
121
|
+
this.stdoutReader = null;
|
|
122
|
+
this.stderrReader = null;
|
|
123
|
+
this.pendingTurn = null;
|
|
124
|
+
this.history = [];
|
|
125
|
+
this.events = [];
|
|
126
|
+
this.stderrLines = [];
|
|
127
|
+
this.sessionId = null;
|
|
128
|
+
this.resumeSessionId = null;
|
|
129
|
+
this.lastTurn = null;
|
|
130
|
+
this._closePromise = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getStatus() {
|
|
134
|
+
return {
|
|
135
|
+
type: 'claude',
|
|
136
|
+
state: this.state,
|
|
137
|
+
pid: this.child?.pid || null,
|
|
138
|
+
session_id: this.sessionId,
|
|
139
|
+
history_length: this.history.length,
|
|
140
|
+
last_turn_at_ms: this.lastTurn?.finishedAtMs || null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_writeFrame(frame) {
|
|
145
|
+
if (!this.child?.stdin?.writable) {
|
|
146
|
+
throw createWorkerError('Claude worker stdin is not writable', { code: 'WORKER_STDIN_CLOSED' });
|
|
147
|
+
}
|
|
148
|
+
this.child.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_terminateChild(child) {
|
|
152
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
153
|
+
try { child.stdin.end(); } catch {}
|
|
154
|
+
try { child.kill(); } catch {}
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
if (child.exitCode === null) {
|
|
157
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
158
|
+
}
|
|
159
|
+
}, this.killGraceMs);
|
|
160
|
+
timer.unref?.();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _handleControlRequest(event) {
|
|
164
|
+
let responseFrame = null;
|
|
165
|
+
|
|
166
|
+
if (this.controlRequestHandler) {
|
|
167
|
+
responseFrame = await this.controlRequestHandler(event, { worker: this });
|
|
168
|
+
} else {
|
|
169
|
+
const requestId = event.request_id || event.requestId;
|
|
170
|
+
if (!requestId) return;
|
|
171
|
+
|
|
172
|
+
const successPayload = event.subtype === 'can_use_tool'
|
|
173
|
+
? { decision: 'allow', allowed: true }
|
|
174
|
+
: { acknowledged: true, subtype: event.subtype || 'unknown' };
|
|
175
|
+
|
|
176
|
+
responseFrame = {
|
|
177
|
+
type: 'control_response',
|
|
178
|
+
response: {
|
|
179
|
+
request_id: requestId,
|
|
180
|
+
subtype: 'success',
|
|
181
|
+
response: successPayload,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (responseFrame) {
|
|
187
|
+
this._writeFrame(responseFrame);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_finalizePendingTurn(turn, event) {
|
|
192
|
+
if (!turn || turn.completed) return;
|
|
193
|
+
|
|
194
|
+
turn.completed = true;
|
|
195
|
+
clearTimeout(turn.timeout);
|
|
196
|
+
this.pendingTurn = null;
|
|
197
|
+
|
|
198
|
+
const response = [
|
|
199
|
+
...turn.assistantTexts,
|
|
200
|
+
extractText(event),
|
|
201
|
+
]
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.join('\n')
|
|
204
|
+
.trim();
|
|
205
|
+
|
|
206
|
+
if (response) {
|
|
207
|
+
this.history.push({
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
content: response,
|
|
210
|
+
at_ms: Date.now(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = {
|
|
215
|
+
type: 'claude',
|
|
216
|
+
sessionId: this.sessionId,
|
|
217
|
+
response,
|
|
218
|
+
assistantEvents: turn.assistantEvents,
|
|
219
|
+
resultEvent: event,
|
|
220
|
+
stderr: this.stderrLines.join('\n').trim(),
|
|
221
|
+
history: [...this.history],
|
|
222
|
+
startedAtMs: turn.startedAtMs,
|
|
223
|
+
finishedAtMs: Date.now(),
|
|
224
|
+
durationMs: Date.now() - turn.startedAtMs,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
this.lastTurn = result;
|
|
228
|
+
turn.resolve(result);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_rejectPendingTurn(error) {
|
|
232
|
+
if (!this.pendingTurn || this.pendingTurn.completed) return;
|
|
233
|
+
const turn = this.pendingTurn;
|
|
234
|
+
turn.completed = true;
|
|
235
|
+
clearTimeout(turn.timeout);
|
|
236
|
+
this.pendingTurn = null;
|
|
237
|
+
turn.reject(error);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_handleStdoutLine(line) {
|
|
241
|
+
if (!line) return;
|
|
242
|
+
const event = safeJsonParse(line);
|
|
243
|
+
if (!event) return;
|
|
244
|
+
|
|
245
|
+
this.events.push(event);
|
|
246
|
+
const sessionId = findSessionId(event);
|
|
247
|
+
if (sessionId) {
|
|
248
|
+
this.sessionId = sessionId;
|
|
249
|
+
this.resumeSessionId = sessionId;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.onEvent) {
|
|
253
|
+
try { this.onEvent(event); } catch {}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (event.type === 'control_request') {
|
|
257
|
+
void this._handleControlRequest(event);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (event.type === 'assistant' || event.type === 'streamlined_text') {
|
|
262
|
+
const text = extractText(event);
|
|
263
|
+
if (this.pendingTurn && text) {
|
|
264
|
+
this.pendingTurn.assistantTexts.push(text);
|
|
265
|
+
this.pendingTurn.assistantEvents.push(event);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (event.type === 'result' && this.pendingTurn) {
|
|
271
|
+
this._finalizePendingTurn(this.pendingTurn, event);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async start() {
|
|
276
|
+
if (this.child && this.child.exitCode === null) {
|
|
277
|
+
return this.getStatus();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const args = buildClaudeArgs(this, {
|
|
281
|
+
model: this.model,
|
|
282
|
+
permissionMode: this.permissionMode,
|
|
283
|
+
allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions,
|
|
284
|
+
includePartialMessages: this.includePartialMessages,
|
|
285
|
+
replayUserMessages: this.replayUserMessages,
|
|
286
|
+
mcpConfig: this.mcpConfig,
|
|
287
|
+
extraArgs: this.extraArgs,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const child = spawn(this.command, args, {
|
|
291
|
+
cwd: this.cwd,
|
|
292
|
+
env: this.env,
|
|
293
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
294
|
+
windowsHide: true,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
this.child = child;
|
|
298
|
+
this.state = 'ready';
|
|
299
|
+
this.stderrLines = [];
|
|
300
|
+
this.stdoutReader = readline.createInterface({
|
|
301
|
+
input: child.stdout,
|
|
302
|
+
crlfDelay: Infinity,
|
|
303
|
+
});
|
|
304
|
+
this.stderrReader = readline.createInterface({
|
|
305
|
+
input: child.stderr,
|
|
306
|
+
crlfDelay: Infinity,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
this.stdoutReader.on('line', (line) => this._handleStdoutLine(line));
|
|
310
|
+
this.stderrReader.on('line', (line) => {
|
|
311
|
+
if (!line) return;
|
|
312
|
+
this.stderrLines.push(line);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this._closePromise = new Promise((resolve, reject) => {
|
|
316
|
+
child.once('error', reject);
|
|
317
|
+
child.once('close', (code, signal) => {
|
|
318
|
+
const closeError = code === 0
|
|
319
|
+
? null
|
|
320
|
+
: createWorkerError(`Claude worker exited with code ${code}`, {
|
|
321
|
+
code: 'WORKER_EXIT',
|
|
322
|
+
exitCode: code,
|
|
323
|
+
exitSignal: signal,
|
|
324
|
+
stderr: this.stderrLines.join('\n').trim(),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
this.child = null;
|
|
328
|
+
this.state = 'idle';
|
|
329
|
+
try { this.stdoutReader?.close(); } catch {}
|
|
330
|
+
try { this.stderrReader?.close(); } catch {}
|
|
331
|
+
this.stdoutReader = null;
|
|
332
|
+
this.stderrReader = null;
|
|
333
|
+
if (closeError) this._rejectPendingTurn(closeError);
|
|
334
|
+
resolve({ code, signal });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return this.getStatus();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async stop() {
|
|
342
|
+
if (!this.child) {
|
|
343
|
+
this.state = 'stopped';
|
|
344
|
+
return this.getStatus();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const child = this.child;
|
|
348
|
+
this._terminateChild(child);
|
|
349
|
+
await Promise.race([
|
|
350
|
+
this._closePromise,
|
|
351
|
+
new Promise((resolve) => setTimeout(resolve, this.killGraceMs + 50)),
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
this.child = null;
|
|
355
|
+
this.state = 'stopped';
|
|
356
|
+
return this.getStatus();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async restart() {
|
|
360
|
+
await this.stop();
|
|
361
|
+
this.state = 'idle';
|
|
362
|
+
return this.start();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async run(prompt, options = {}) {
|
|
366
|
+
await this.start();
|
|
367
|
+
|
|
368
|
+
if (this.pendingTurn) {
|
|
369
|
+
throw createWorkerError('ClaudeWorker is already handling another turn', { code: 'WORKER_BUSY' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
|
|
373
|
+
const userText = String(prompt ?? '');
|
|
374
|
+
const startedAtMs = Date.now();
|
|
375
|
+
|
|
376
|
+
this.history.push({
|
|
377
|
+
role: 'user',
|
|
378
|
+
content: userText,
|
|
379
|
+
at_ms: startedAtMs,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const turnPromise = new Promise((resolve, reject) => {
|
|
383
|
+
const timeout = setTimeout(() => {
|
|
384
|
+
const timeoutError = createWorkerError(`Claude worker timed out after ${timeoutMs}ms`, {
|
|
385
|
+
code: 'ETIMEDOUT',
|
|
386
|
+
stderr: this.stderrLines.join('\n').trim(),
|
|
387
|
+
});
|
|
388
|
+
this._rejectPendingTurn(timeoutError);
|
|
389
|
+
this._terminateChild(this.child);
|
|
390
|
+
}, timeoutMs);
|
|
391
|
+
timeout.unref?.();
|
|
392
|
+
|
|
393
|
+
this.pendingTurn = {
|
|
394
|
+
startedAtMs,
|
|
395
|
+
assistantTexts: [],
|
|
396
|
+
assistantEvents: [],
|
|
397
|
+
timeout,
|
|
398
|
+
resolve,
|
|
399
|
+
reject,
|
|
400
|
+
completed: false,
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
this.state = 'running';
|
|
405
|
+
this._writeFrame({
|
|
406
|
+
type: 'user',
|
|
407
|
+
message: {
|
|
408
|
+
role: 'user',
|
|
409
|
+
content: userText,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
return await turnPromise;
|
|
415
|
+
} finally {
|
|
416
|
+
if (this.child) {
|
|
417
|
+
this.state = 'ready';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
isReady() {
|
|
423
|
+
return this.state === 'ready' || this.state === 'running';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async execute(prompt, options = {}) {
|
|
427
|
+
try {
|
|
428
|
+
const result = await this.run(prompt, options);
|
|
429
|
+
return {
|
|
430
|
+
output: result.response,
|
|
431
|
+
exitCode: 0,
|
|
432
|
+
threadId: null,
|
|
433
|
+
sessionKey: options.sessionKey || this.sessionId || null,
|
|
434
|
+
raw: result,
|
|
435
|
+
};
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return {
|
|
438
|
+
output: error.stderr || error.message || 'Claude worker failed',
|
|
439
|
+
exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
|
|
440
|
+
threadId: null,
|
|
441
|
+
sessionKey: options.sessionKey || this.sessionId || null,
|
|
442
|
+
raw: null,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|