triflux 3.3.0-dev.7 → 4.0.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 (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -708
  8. package/hub/delegator/contracts.mjs +38 -0
  9. package/hub/delegator/index.mjs +14 -0
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  11. package/hub/delegator/service.mjs +302 -0
  12. package/hub/delegator/tool-definitions.mjs +35 -0
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -266
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -1,349 +1,349 @@
1
- // hub/workers/gemini-worker.mjs — Gemini headless subprocess 래퍼
2
- // ADR-006: --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.response === 'string') appendTextFragments(value.response, parts);
39
- if (typeof value.result === 'string') appendTextFragments(value.result, parts);
40
- if (typeof value.content === 'string' || Array.isArray(value.content) || value.content) {
41
- appendTextFragments(value.content, parts);
42
- }
43
- if (value.message) appendTextFragments(value.message, parts);
44
- }
45
- }
46
-
47
- function extractText(event) {
48
- const parts = [];
49
- appendTextFragments(event, parts);
50
- return parts.join('\n').trim();
51
- }
52
-
53
- function findLastEvent(events, predicate) {
54
- for (let index = events.length - 1; index >= 0; index -= 1) {
55
- if (predicate(events[index])) return events[index];
56
- }
57
- return null;
58
- }
59
-
60
- function buildGeminiArgs(options) {
61
- const args = [];
62
-
63
- if (options.model) {
64
- args.push('--model', options.model);
65
- }
66
-
67
- if (options.approvalMode) {
68
- args.push('--approval-mode', options.approvalMode);
69
- } else if (options.yolo !== false) {
70
- args.push('--yolo');
71
- }
72
-
73
- const allowedMcpServers = toStringList(options.allowedMcpServerNames);
74
- if (allowedMcpServers.length) {
75
- args.push('--allowed-mcp-server-names', ...allowedMcpServers);
76
- }
77
-
78
- const extraArgs = toStringList(options.extraArgs);
79
- if (extraArgs.length) args.push(...extraArgs);
80
-
81
- args.push('--prompt', options.promptArgument ?? '');
82
- args.push('--output-format', 'stream-json');
83
-
84
- return args;
85
- }
86
-
87
- function createWorkerError(message, details = {}) {
88
- const error = new Error(message);
89
- Object.assign(error, details);
90
- return error;
91
- }
92
-
93
- /**
94
- * Gemini stream-json 래퍼
95
- */
96
- export class GeminiWorker {
97
- type = 'gemini';
98
-
99
- constructor(options = {}) {
100
- this.command = options.command || 'gemini';
101
- this.commandArgs = toStringList(options.commandArgs || options.args);
102
- this.cwd = options.cwd || process.cwd();
103
- this.env = { ...process.env, ...(options.env || {}) };
104
- this.model = options.model || null;
105
- this.approvalMode = options.approvalMode || null;
106
- this.yolo = options.yolo !== false;
107
- this.allowedMcpServerNames = toStringList(options.allowedMcpServerNames);
108
- this.extraArgs = toStringList(options.extraArgs);
109
- this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
110
- this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
111
- this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
112
-
113
- this.state = 'idle';
114
- this.child = null;
115
- this.lastRun = null;
116
- }
117
-
118
- getStatus() {
119
- return {
120
- type: 'gemini',
121
- state: this.state,
122
- pid: this.child?.pid || null,
123
- last_run_at_ms: this.lastRun?.finishedAtMs || null,
124
- last_exit_code: this.lastRun?.exitCode ?? null,
125
- };
126
- }
127
-
128
- async start() {
129
- if (this.state === 'stopped') {
130
- this.state = 'idle';
131
- }
132
- return this.getStatus();
133
- }
134
-
135
- async stop() {
136
- if (!this.child) {
137
- this.state = 'stopped';
138
- return this.getStatus();
139
- }
140
- const child = this.child;
141
- this._terminateChild(child);
142
- await new Promise((resolve) => {
143
- child.once('close', resolve);
144
- setTimeout(resolve, this.killGraceMs + 50).unref?.();
145
- });
146
- this.child = null;
147
- this.state = 'stopped';
148
- return this.getStatus();
149
- }
150
-
151
- async restart() {
152
- await this.stop();
153
- this.state = 'idle';
154
- return this.getStatus();
155
- }
156
-
157
- _terminateChild(child) {
158
- if (!child || child.exitCode !== null || child.killed) return;
159
- try { child.stdin.end(); } catch {}
160
- try { child.kill(); } catch {}
161
-
162
- const timer = setTimeout(() => {
163
- if (child.exitCode === null) {
164
- try { child.kill('SIGKILL'); } catch {}
165
- }
166
- }, this.killGraceMs);
167
- timer.unref?.();
168
- }
169
-
170
- async run(prompt, options = {}) {
171
- if (this.child) {
172
- throw createWorkerError('GeminiWorker is already running', { code: 'WORKER_BUSY' });
173
- }
174
-
175
- await this.start();
176
-
177
- const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
178
- const startedAtMs = Date.now();
179
- const args = [
180
- ...this.commandArgs,
181
- ...buildGeminiArgs({
182
- model: options.model || this.model,
183
- approvalMode: options.approvalMode || this.approvalMode,
184
- yolo: options.yolo ?? this.yolo,
185
- allowedMcpServerNames: options.allowedMcpServerNames || this.allowedMcpServerNames,
186
- extraArgs: options.extraArgs || this.extraArgs,
187
- promptArgument: options.promptArgument ?? '',
188
- }),
189
- ];
190
-
191
- const child = spawn(this.command, args, {
192
- cwd: options.cwd || this.cwd,
193
- env: { ...this.env, ...(options.env || {}) },
194
- stdio: ['pipe', 'pipe', 'pipe'],
195
- windowsHide: true,
196
- });
197
-
198
- this.child = child;
199
- this.state = 'running';
200
-
201
- const events = [];
202
- const stdoutLines = [];
203
- const stderrLines = [];
204
- let lastErrorEvent = null;
205
- let timedOut = false;
206
- let exitCode = null;
207
- let exitSignal = null;
208
-
209
- const stdoutReader = readline.createInterface({
210
- input: child.stdout,
211
- crlfDelay: Infinity,
212
- });
213
- const stderrReader = readline.createInterface({
214
- input: child.stderr,
215
- crlfDelay: Infinity,
216
- });
217
-
218
- stdoutReader.on('line', (line) => {
219
- if (!line) return;
220
- const event = safeJsonParse(line);
221
- if (!event) {
222
- stdoutLines.push(line);
223
- return;
224
- }
225
-
226
- events.push(event);
227
- if (event.type === 'error') lastErrorEvent = event;
228
- if (this.onEvent) {
229
- try { this.onEvent(event); } catch {}
230
- }
231
- });
232
-
233
- stderrReader.on('line', (line) => {
234
- if (!line) return;
235
- stderrLines.push(line);
236
- });
237
-
238
- const closePromise = new Promise((resolve, reject) => {
239
- child.once('error', reject);
240
- child.once('close', (code, signal) => {
241
- exitCode = code;
242
- exitSignal = signal;
243
- resolve();
244
- });
245
- });
246
-
247
- const timeout = setTimeout(() => {
248
- timedOut = true;
249
- this._terminateChild(child);
250
- }, timeoutMs);
251
- timeout.unref?.();
252
-
253
- child.stdin.on('error', () => {});
254
- child.stdin.end(String(prompt ?? ''));
255
-
256
- try {
257
- await closePromise;
258
- } finally {
259
- clearTimeout(timeout);
260
- stdoutReader.close();
261
- stderrReader.close();
262
- if (this.child === child) {
263
- this.child = null;
264
- }
265
- this.state = 'idle';
266
- }
267
-
268
- const resultEvent = findLastEvent(events, (event) => event?.type === 'result');
269
- const response = [
270
- extractText(resultEvent),
271
- ...events
272
- .filter((event) => event?.type === 'message' || event?.type === 'assistant')
273
- .map((event) => extractText(event))
274
- .filter(Boolean),
275
- ...stdoutLines,
276
- ]
277
- .filter(Boolean)
278
- .join('\n')
279
- .trim();
280
-
281
- const result = {
282
- type: 'gemini',
283
- command: this.command,
284
- args,
285
- response,
286
- events,
287
- resultEvent,
288
- usage: resultEvent?.usage || null,
289
- stdout: stdoutLines.join('\n').trim(),
290
- stderr: stderrLines.join('\n').trim(),
291
- exitCode,
292
- exitSignal,
293
- timedOut,
294
- startedAtMs,
295
- finishedAtMs: Date.now(),
296
- };
297
-
298
- this.lastRun = result;
299
-
300
- if (timedOut) {
301
- throw createWorkerError(`Gemini worker timed out after ${timeoutMs}ms`, {
302
- code: 'ETIMEDOUT',
303
- result,
304
- stderr: result.stderr,
305
- });
306
- }
307
-
308
- if (exitCode !== 0) {
309
- throw createWorkerError(`Gemini worker exited with code ${exitCode}`, {
310
- code: 'WORKER_EXIT',
311
- result,
312
- stderr: result.stderr,
313
- });
314
- }
315
-
316
- if (lastErrorEvent) {
317
- throw createWorkerError('Gemini worker emitted an error event', {
318
- code: 'WORKER_EVENT_ERROR',
319
- result,
320
- stderr: result.stderr,
321
- });
322
- }
323
-
324
- return result;
325
- }
326
-
327
- isReady() {
328
- return this.state !== 'stopped';
329
- }
330
-
331
- async execute(prompt, options = {}) {
332
- try {
333
- const result = await this.run(prompt, options);
334
- return {
335
- output: result.response,
336
- exitCode: 0,
337
- sessionKey: options.sessionKey || null,
338
- raw: result,
339
- };
340
- } catch (error) {
341
- return {
342
- output: error.stderr || error.message || 'Gemini worker failed',
343
- exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
344
- sessionKey: options.sessionKey || null,
345
- raw: error.result || null,
346
- };
347
- }
348
- }
349
- }
1
+ // hub/workers/gemini-worker.mjs — Gemini headless subprocess 래퍼
2
+ // ADR-006: --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.response === 'string') appendTextFragments(value.response, parts);
39
+ if (typeof value.result === 'string') appendTextFragments(value.result, parts);
40
+ if (typeof value.content === 'string' || Array.isArray(value.content) || value.content) {
41
+ appendTextFragments(value.content, parts);
42
+ }
43
+ if (value.message) appendTextFragments(value.message, parts);
44
+ }
45
+ }
46
+
47
+ function extractText(event) {
48
+ const parts = [];
49
+ appendTextFragments(event, parts);
50
+ return parts.join('\n').trim();
51
+ }
52
+
53
+ function findLastEvent(events, predicate) {
54
+ for (let index = events.length - 1; index >= 0; index -= 1) {
55
+ if (predicate(events[index])) return events[index];
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function buildGeminiArgs(options) {
61
+ const args = [];
62
+
63
+ if (options.model) {
64
+ args.push('--model', options.model);
65
+ }
66
+
67
+ if (options.approvalMode) {
68
+ args.push('--approval-mode', options.approvalMode);
69
+ } else if (options.yolo !== false) {
70
+ args.push('--yolo');
71
+ }
72
+
73
+ const allowedMcpServers = toStringList(options.allowedMcpServerNames);
74
+ if (allowedMcpServers.length) {
75
+ args.push('--allowed-mcp-server-names', ...allowedMcpServers);
76
+ }
77
+
78
+ const extraArgs = toStringList(options.extraArgs);
79
+ if (extraArgs.length) args.push(...extraArgs);
80
+
81
+ args.push('--prompt', options.promptArgument ?? '');
82
+ args.push('--output-format', 'stream-json');
83
+
84
+ return args;
85
+ }
86
+
87
+ function createWorkerError(message, details = {}) {
88
+ const error = new Error(message);
89
+ Object.assign(error, details);
90
+ return error;
91
+ }
92
+
93
+ /**
94
+ * Gemini stream-json 래퍼
95
+ */
96
+ export class GeminiWorker {
97
+ type = 'gemini';
98
+
99
+ constructor(options = {}) {
100
+ this.command = options.command || 'gemini';
101
+ this.commandArgs = toStringList(options.commandArgs || options.args);
102
+ this.cwd = options.cwd || process.cwd();
103
+ this.env = { ...process.env, ...(options.env || {}) };
104
+ this.model = options.model || null;
105
+ this.approvalMode = options.approvalMode || null;
106
+ this.yolo = options.yolo !== false;
107
+ this.allowedMcpServerNames = toStringList(options.allowedMcpServerNames);
108
+ this.extraArgs = toStringList(options.extraArgs);
109
+ this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
110
+ this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
111
+ this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
112
+
113
+ this.state = 'idle';
114
+ this.child = null;
115
+ this.lastRun = null;
116
+ }
117
+
118
+ getStatus() {
119
+ return {
120
+ type: 'gemini',
121
+ state: this.state,
122
+ pid: this.child?.pid || null,
123
+ last_run_at_ms: this.lastRun?.finishedAtMs || null,
124
+ last_exit_code: this.lastRun?.exitCode ?? null,
125
+ };
126
+ }
127
+
128
+ async start() {
129
+ if (this.state === 'stopped') {
130
+ this.state = 'idle';
131
+ }
132
+ return this.getStatus();
133
+ }
134
+
135
+ async stop() {
136
+ if (!this.child) {
137
+ this.state = 'stopped';
138
+ return this.getStatus();
139
+ }
140
+ const child = this.child;
141
+ this._terminateChild(child);
142
+ await new Promise((resolve) => {
143
+ child.once('close', resolve);
144
+ setTimeout(resolve, this.killGraceMs + 50).unref?.();
145
+ });
146
+ this.child = null;
147
+ this.state = 'stopped';
148
+ return this.getStatus();
149
+ }
150
+
151
+ async restart() {
152
+ await this.stop();
153
+ this.state = 'idle';
154
+ return this.getStatus();
155
+ }
156
+
157
+ _terminateChild(child) {
158
+ if (!child || child.exitCode !== null || child.killed) return;
159
+ try { child.stdin.end(); } catch {}
160
+ try { child.kill(); } catch {}
161
+
162
+ const timer = setTimeout(() => {
163
+ if (child.exitCode === null) {
164
+ try { child.kill('SIGKILL'); } catch {}
165
+ }
166
+ }, this.killGraceMs);
167
+ timer.unref?.();
168
+ }
169
+
170
+ async run(prompt, options = {}) {
171
+ if (this.child) {
172
+ throw createWorkerError('GeminiWorker is already running', { code: 'WORKER_BUSY' });
173
+ }
174
+
175
+ await this.start();
176
+
177
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
178
+ const startedAtMs = Date.now();
179
+ const args = [
180
+ ...this.commandArgs,
181
+ ...buildGeminiArgs({
182
+ model: options.model || this.model,
183
+ approvalMode: options.approvalMode || this.approvalMode,
184
+ yolo: options.yolo ?? this.yolo,
185
+ allowedMcpServerNames: options.allowedMcpServerNames || this.allowedMcpServerNames,
186
+ extraArgs: options.extraArgs || this.extraArgs,
187
+ promptArgument: options.promptArgument ?? '',
188
+ }),
189
+ ];
190
+
191
+ const child = spawn(this.command, args, {
192
+ cwd: options.cwd || this.cwd,
193
+ env: { ...this.env, ...(options.env || {}) },
194
+ stdio: ['pipe', 'pipe', 'pipe'],
195
+ windowsHide: true,
196
+ });
197
+
198
+ this.child = child;
199
+ this.state = 'running';
200
+
201
+ const events = [];
202
+ const stdoutLines = [];
203
+ const stderrLines = [];
204
+ let lastErrorEvent = null;
205
+ let timedOut = false;
206
+ let exitCode = null;
207
+ let exitSignal = null;
208
+
209
+ const stdoutReader = readline.createInterface({
210
+ input: child.stdout,
211
+ crlfDelay: Infinity,
212
+ });
213
+ const stderrReader = readline.createInterface({
214
+ input: child.stderr,
215
+ crlfDelay: Infinity,
216
+ });
217
+
218
+ stdoutReader.on('line', (line) => {
219
+ if (!line) return;
220
+ const event = safeJsonParse(line);
221
+ if (!event) {
222
+ stdoutLines.push(line);
223
+ return;
224
+ }
225
+
226
+ events.push(event);
227
+ if (event.type === 'error') lastErrorEvent = event;
228
+ if (this.onEvent) {
229
+ try { this.onEvent(event); } catch {}
230
+ }
231
+ });
232
+
233
+ stderrReader.on('line', (line) => {
234
+ if (!line) return;
235
+ stderrLines.push(line);
236
+ });
237
+
238
+ const closePromise = new Promise((resolve, reject) => {
239
+ child.once('error', reject);
240
+ child.once('close', (code, signal) => {
241
+ exitCode = code;
242
+ exitSignal = signal;
243
+ resolve();
244
+ });
245
+ });
246
+
247
+ const timeout = setTimeout(() => {
248
+ timedOut = true;
249
+ this._terminateChild(child);
250
+ }, timeoutMs);
251
+ timeout.unref?.();
252
+
253
+ child.stdin.on('error', () => {});
254
+ child.stdin.end(String(prompt ?? ''));
255
+
256
+ try {
257
+ await closePromise;
258
+ } finally {
259
+ clearTimeout(timeout);
260
+ stdoutReader.close();
261
+ stderrReader.close();
262
+ if (this.child === child) {
263
+ this.child = null;
264
+ }
265
+ this.state = 'idle';
266
+ }
267
+
268
+ const resultEvent = findLastEvent(events, (event) => event?.type === 'result');
269
+ const response = [
270
+ extractText(resultEvent),
271
+ ...events
272
+ .filter((event) => event?.type === 'message' || event?.type === 'assistant')
273
+ .map((event) => extractText(event))
274
+ .filter(Boolean),
275
+ ...stdoutLines,
276
+ ]
277
+ .filter(Boolean)
278
+ .join('\n')
279
+ .trim();
280
+
281
+ const result = {
282
+ type: 'gemini',
283
+ command: this.command,
284
+ args,
285
+ response,
286
+ events,
287
+ resultEvent,
288
+ usage: resultEvent?.usage || null,
289
+ stdout: stdoutLines.join('\n').trim(),
290
+ stderr: stderrLines.join('\n').trim(),
291
+ exitCode,
292
+ exitSignal,
293
+ timedOut,
294
+ startedAtMs,
295
+ finishedAtMs: Date.now(),
296
+ };
297
+
298
+ this.lastRun = result;
299
+
300
+ if (timedOut) {
301
+ throw createWorkerError(`Gemini worker timed out after ${timeoutMs}ms`, {
302
+ code: 'ETIMEDOUT',
303
+ result,
304
+ stderr: result.stderr,
305
+ });
306
+ }
307
+
308
+ if (exitCode !== 0) {
309
+ throw createWorkerError(`Gemini worker exited with code ${exitCode}`, {
310
+ code: 'WORKER_EXIT',
311
+ result,
312
+ stderr: result.stderr,
313
+ });
314
+ }
315
+
316
+ if (lastErrorEvent) {
317
+ throw createWorkerError('Gemini worker emitted an error event', {
318
+ code: 'WORKER_EVENT_ERROR',
319
+ result,
320
+ stderr: result.stderr,
321
+ });
322
+ }
323
+
324
+ return result;
325
+ }
326
+
327
+ isReady() {
328
+ return this.state !== 'stopped';
329
+ }
330
+
331
+ async execute(prompt, options = {}) {
332
+ try {
333
+ const result = await this.run(prompt, options);
334
+ return {
335
+ output: result.response,
336
+ exitCode: 0,
337
+ sessionKey: options.sessionKey || null,
338
+ raw: result,
339
+ };
340
+ } catch (error) {
341
+ return {
342
+ output: error.stderr || error.message || 'Gemini worker failed',
343
+ exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
344
+ sessionKey: options.sessionKey || null,
345
+ raw: error.result || null,
346
+ };
347
+ }
348
+ }
349
+ }