@triflux/remote 10.0.0-alpha.1

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 (68) hide show
  1. package/hub/pipe.mjs +579 -0
  2. package/hub/public/dashboard.html +355 -0
  3. package/hub/public/tray-icon.ico +0 -0
  4. package/hub/public/tray-icon.png +0 -0
  5. package/hub/server.mjs +1124 -0
  6. package/hub/store-adapter.mjs +851 -0
  7. package/hub/store.mjs +897 -0
  8. package/hub/team/agent-map.json +11 -0
  9. package/hub/team/ansi.mjs +379 -0
  10. package/hub/team/backend.mjs +90 -0
  11. package/hub/team/cli/commands/attach.mjs +37 -0
  12. package/hub/team/cli/commands/control.mjs +43 -0
  13. package/hub/team/cli/commands/debug.mjs +74 -0
  14. package/hub/team/cli/commands/focus.mjs +53 -0
  15. package/hub/team/cli/commands/interrupt.mjs +36 -0
  16. package/hub/team/cli/commands/kill.mjs +37 -0
  17. package/hub/team/cli/commands/list.mjs +24 -0
  18. package/hub/team/cli/commands/send.mjs +37 -0
  19. package/hub/team/cli/commands/start/index.mjs +106 -0
  20. package/hub/team/cli/commands/start/parse-args.mjs +130 -0
  21. package/hub/team/cli/commands/start/start-headless.mjs +109 -0
  22. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  23. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  24. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  25. package/hub/team/cli/commands/status.mjs +87 -0
  26. package/hub/team/cli/commands/stop.mjs +31 -0
  27. package/hub/team/cli/commands/task.mjs +30 -0
  28. package/hub/team/cli/commands/tasks.mjs +13 -0
  29. package/hub/team/cli/help.mjs +42 -0
  30. package/hub/team/cli/index.mjs +41 -0
  31. package/hub/team/cli/manifest.mjs +29 -0
  32. package/hub/team/cli/render.mjs +30 -0
  33. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  34. package/hub/team/cli/services/hub-client.mjs +208 -0
  35. package/hub/team/cli/services/member-selector.mjs +30 -0
  36. package/hub/team/cli/services/native-control.mjs +117 -0
  37. package/hub/team/cli/services/runtime-mode.mjs +62 -0
  38. package/hub/team/cli/services/state-store.mjs +48 -0
  39. package/hub/team/cli/services/task-model.mjs +30 -0
  40. package/hub/team/dashboard-anchor.mjs +14 -0
  41. package/hub/team/dashboard-layout.mjs +33 -0
  42. package/hub/team/dashboard-open.mjs +153 -0
  43. package/hub/team/dashboard.mjs +274 -0
  44. package/hub/team/handoff.mjs +303 -0
  45. package/hub/team/headless.mjs +1149 -0
  46. package/hub/team/native-supervisor.mjs +392 -0
  47. package/hub/team/native.mjs +649 -0
  48. package/hub/team/nativeProxy.mjs +681 -0
  49. package/hub/team/orchestrator.mjs +161 -0
  50. package/hub/team/pane.mjs +153 -0
  51. package/hub/team/psmux.mjs +1354 -0
  52. package/hub/team/routing.mjs +223 -0
  53. package/hub/team/session.mjs +611 -0
  54. package/hub/team/shared.mjs +13 -0
  55. package/hub/team/staleState.mjs +361 -0
  56. package/hub/team/tui-lite.mjs +380 -0
  57. package/hub/team/tui-viewer.mjs +463 -0
  58. package/hub/team/tui.mjs +1245 -0
  59. package/hub/tools.mjs +554 -0
  60. package/hub/tray.mjs +376 -0
  61. package/hub/workers/claude-worker.mjs +475 -0
  62. package/hub/workers/codex-mcp.mjs +504 -0
  63. package/hub/workers/delegator-mcp.mjs +1076 -0
  64. package/hub/workers/factory.mjs +21 -0
  65. package/hub/workers/gemini-worker.mjs +373 -0
  66. package/hub/workers/interface.mjs +52 -0
  67. package/hub/workers/worker-utils.mjs +104 -0
  68. package/package.json +31 -0
@@ -0,0 +1,475 @@
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
+ import { extractText, terminateChild, withRetry } from './worker-utils.mjs';
8
+
9
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
10
+ const DEFAULT_KILL_GRACE_MS = 1000;
11
+
12
+ function toStringList(value) {
13
+ if (!Array.isArray(value)) return [];
14
+ return value
15
+ .map((item) => String(item ?? '').trim())
16
+ .filter(Boolean);
17
+ }
18
+
19
+ function safeJsonParse(line) {
20
+ try {
21
+ return JSON.parse(line);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function findSessionId(event) {
28
+ return event?.session_id
29
+ || event?.sessionId
30
+ || event?.message?.session_id
31
+ || event?.message?.sessionId
32
+ || null;
33
+ }
34
+
35
+ function createWorkerError(message, details = {}) {
36
+ const error = new Error(message);
37
+ Object.assign(error, details);
38
+ return error;
39
+ }
40
+
41
+ function normalizeRetryOptions(retryOptions) {
42
+ if (!retryOptions || typeof retryOptions !== 'object') {
43
+ return Object.freeze({});
44
+ }
45
+ return Object.freeze({ ...retryOptions });
46
+ }
47
+
48
+ function isClaudeRetryable(error) {
49
+ return error?.code === 'WORKER_EXIT'
50
+ || error?.code === 'ETIMEDOUT'
51
+ || error?.code === 'WORKER_STDIN_CLOSED';
52
+ }
53
+
54
+ function detectClaudeCategory(error) {
55
+ const combined = `${error?.message || ''}\n${error?.stderr || ''}`.toLowerCase();
56
+
57
+ if (/(unauthorized|forbidden|auth|login|token|credential|apikey|api key)/.test(combined)) {
58
+ return 'auth';
59
+ }
60
+ if (/unknown option|invalid option|config|permission-mode|mcp-config/.test(combined)) {
61
+ return 'config';
62
+ }
63
+ if (/stdin|prompt|input/.test(combined) && error?.code !== 'WORKER_STDIN_CLOSED') {
64
+ return 'input';
65
+ }
66
+
67
+ return 'transient';
68
+ }
69
+
70
+ function buildClaudeErrorInfo(error, attempts) {
71
+ const category = detectClaudeCategory(error);
72
+ let recovery = 'Restart the Claude worker session and retry the turn.';
73
+
74
+ if (category === 'auth') {
75
+ recovery = 'Refresh the Claude authentication state and retry.';
76
+ } else if (category === 'config') {
77
+ recovery = 'Check the Claude CLI flags, MCP configuration, and permission settings.';
78
+ } else if (category === 'input') {
79
+ recovery = 'Check the Claude request payload before retrying.';
80
+ }
81
+
82
+ return Object.freeze({
83
+ code: error?.code || 'CLAUDE_EXECUTION_ERROR',
84
+ retryable: isClaudeRetryable(error),
85
+ attempts,
86
+ category,
87
+ recovery,
88
+ });
89
+ }
90
+
91
+ function buildClaudeArgs(worker, options) {
92
+ const args = [...worker.commandArgs];
93
+
94
+ args.push('--print');
95
+ args.push('--input-format', 'stream-json');
96
+ args.push('--output-format', 'stream-json');
97
+
98
+ if (options.includePartialMessages) args.push('--include-partial-messages');
99
+ if (options.replayUserMessages) args.push('--replay-user-messages');
100
+ if (options.model) args.push('--model', options.model);
101
+ if (options.allowDangerouslySkipPermissions) args.push('--dangerously-skip-permissions');
102
+ if (options.permissionMode) args.push('--permission-mode', options.permissionMode);
103
+
104
+ for (const config of toStringList(options.mcpConfig)) {
105
+ args.push('--mcp-config', config);
106
+ }
107
+
108
+ if (worker.resumeSessionId) {
109
+ args.push('--resume', worker.resumeSessionId);
110
+ }
111
+
112
+ args.push(...toStringList(options.extraArgs));
113
+
114
+ return args;
115
+ }
116
+
117
+ /**
118
+ * Claude stream-json 세션 워커
119
+ */
120
+ export class ClaudeWorker {
121
+ type = 'claude';
122
+
123
+ constructor(options = {}) {
124
+ this.command = options.command || 'claude';
125
+ this.commandArgs = toStringList(options.commandArgs || options.args);
126
+ this.cwd = options.cwd || process.cwd();
127
+ this.env = { ...process.env, ...(options.env || {}) };
128
+ this.model = options.model || null;
129
+ this.permissionMode = options.permissionMode || null;
130
+ this.allowDangerouslySkipPermissions = options.allowDangerouslySkipPermissions !== false;
131
+ this.includePartialMessages = options.includePartialMessages === true;
132
+ this.replayUserMessages = options.replayUserMessages !== false;
133
+ this.mcpConfig = toStringList(options.mcpConfig);
134
+ this.extraArgs = toStringList(options.extraArgs);
135
+ this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
136
+ this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
137
+ this.retryOptions = normalizeRetryOptions(options.retryOptions);
138
+ this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
139
+ this.controlRequestHandler = typeof options.controlRequestHandler === 'function'
140
+ ? options.controlRequestHandler
141
+ : null;
142
+
143
+ this.state = 'idle';
144
+ this.child = null;
145
+ this.stdoutReader = null;
146
+ this.stderrReader = null;
147
+ this.pendingTurn = null;
148
+ this.history = [];
149
+ this.events = [];
150
+ this.stderrLines = [];
151
+ this.sessionId = null;
152
+ this.resumeSessionId = null;
153
+ this.lastTurn = null;
154
+ this._closePromise = null;
155
+ }
156
+
157
+ getStatus() {
158
+ return {
159
+ type: 'claude',
160
+ state: this.state,
161
+ pid: this.child?.pid || null,
162
+ session_id: this.sessionId,
163
+ history_length: this.history.length,
164
+ last_turn_at_ms: this.lastTurn?.finishedAtMs || null,
165
+ };
166
+ }
167
+
168
+ _writeFrame(frame) {
169
+ if (!this.child?.stdin?.writable) {
170
+ throw createWorkerError('Claude worker stdin is not writable', { code: 'WORKER_STDIN_CLOSED' });
171
+ }
172
+ this.child.stdin.write(`${JSON.stringify(frame)}\n`);
173
+ }
174
+
175
+ _terminateChild(child) {
176
+ terminateChild(child, this.killGraceMs);
177
+ }
178
+
179
+ async _handleControlRequest(event) {
180
+ let responseFrame = null;
181
+
182
+ if (this.controlRequestHandler) {
183
+ responseFrame = await this.controlRequestHandler(event, { worker: this });
184
+ } else {
185
+ const requestId = event.request_id || event.requestId;
186
+ if (!requestId) return;
187
+
188
+ const successPayload = event.subtype === 'can_use_tool'
189
+ ? { decision: 'allow', allowed: true }
190
+ : { acknowledged: true, subtype: event.subtype || 'unknown' };
191
+
192
+ responseFrame = {
193
+ type: 'control_response',
194
+ response: {
195
+ request_id: requestId,
196
+ subtype: 'success',
197
+ response: successPayload,
198
+ },
199
+ };
200
+ }
201
+
202
+ if (responseFrame) {
203
+ this._writeFrame(responseFrame);
204
+ }
205
+ }
206
+
207
+ _finalizePendingTurn(turn, event) {
208
+ if (!turn || turn.completed) return;
209
+
210
+ turn.completed = true;
211
+ clearTimeout(turn.timeout);
212
+ this.pendingTurn = null;
213
+
214
+ const response = [
215
+ ...turn.assistantTexts,
216
+ extractText(event),
217
+ ]
218
+ .filter(Boolean)
219
+ .join('\n')
220
+ .trim();
221
+
222
+ if (response) {
223
+ this.history.push({
224
+ role: 'assistant',
225
+ content: response,
226
+ at_ms: Date.now(),
227
+ });
228
+ }
229
+
230
+ const result = {
231
+ type: 'claude',
232
+ sessionId: this.sessionId,
233
+ response,
234
+ assistantEvents: turn.assistantEvents,
235
+ resultEvent: event,
236
+ stderr: this.stderrLines.join('\n').trim(),
237
+ history: [...this.history],
238
+ startedAtMs: turn.startedAtMs,
239
+ finishedAtMs: Date.now(),
240
+ durationMs: Date.now() - turn.startedAtMs,
241
+ };
242
+
243
+ this.lastTurn = result;
244
+ turn.resolve(result);
245
+ }
246
+
247
+ _rejectPendingTurn(error) {
248
+ if (!this.pendingTurn || this.pendingTurn.completed) return;
249
+ const turn = this.pendingTurn;
250
+ turn.completed = true;
251
+ clearTimeout(turn.timeout);
252
+ this.pendingTurn = null;
253
+ turn.reject(error);
254
+ }
255
+
256
+ _handleStdoutLine(line) {
257
+ if (!line) return;
258
+ const event = safeJsonParse(line);
259
+ if (!event) return;
260
+
261
+ this.events.push(event);
262
+ const sessionId = findSessionId(event);
263
+ if (sessionId) {
264
+ this.sessionId = sessionId;
265
+ this.resumeSessionId = sessionId;
266
+ }
267
+
268
+ if (this.onEvent) {
269
+ try { this.onEvent(event); } catch {}
270
+ }
271
+
272
+ if (event.type === 'control_request') {
273
+ void this._handleControlRequest(event);
274
+ return;
275
+ }
276
+
277
+ if (event.type === 'assistant' || event.type === 'streamlined_text') {
278
+ const text = extractText(event);
279
+ if (this.pendingTurn && text) {
280
+ this.pendingTurn.assistantTexts.push(text);
281
+ this.pendingTurn.assistantEvents.push(event);
282
+ }
283
+ return;
284
+ }
285
+
286
+ if (event.type === 'result' && this.pendingTurn) {
287
+ this._finalizePendingTurn(this.pendingTurn, event);
288
+ }
289
+ }
290
+
291
+ async start() {
292
+ if (this.child && this.child.exitCode === null) {
293
+ return this.getStatus();
294
+ }
295
+
296
+ const args = buildClaudeArgs(this, {
297
+ model: this.model,
298
+ permissionMode: this.permissionMode,
299
+ allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions,
300
+ includePartialMessages: this.includePartialMessages,
301
+ replayUserMessages: this.replayUserMessages,
302
+ mcpConfig: this.mcpConfig,
303
+ extraArgs: this.extraArgs,
304
+ });
305
+
306
+ const child = spawn(this.command, args, {
307
+ cwd: this.cwd,
308
+ env: this.env,
309
+ stdio: ['pipe', 'pipe', 'pipe'],
310
+ windowsHide: true,
311
+ });
312
+
313
+ this.child = child;
314
+ this.state = 'ready';
315
+ this.stderrLines = [];
316
+ this.stdoutReader = readline.createInterface({
317
+ input: child.stdout,
318
+ crlfDelay: Infinity,
319
+ });
320
+ this.stderrReader = readline.createInterface({
321
+ input: child.stderr,
322
+ crlfDelay: Infinity,
323
+ });
324
+
325
+ this.stdoutReader.on('line', (line) => this._handleStdoutLine(line));
326
+ this.stderrReader.on('line', (line) => {
327
+ if (!line) return;
328
+ this.stderrLines.push(line);
329
+ });
330
+
331
+ this._closePromise = new Promise((resolve, reject) => {
332
+ child.once('error', reject);
333
+ child.once('close', (code, signal) => {
334
+ const closeError = code === 0
335
+ ? null
336
+ : createWorkerError(`Claude worker exited with code ${code}`, {
337
+ code: 'WORKER_EXIT',
338
+ exitCode: code,
339
+ exitSignal: signal,
340
+ stderr: this.stderrLines.join('\n').trim(),
341
+ });
342
+
343
+ this.child = null;
344
+ this.state = 'idle';
345
+ try { this.stdoutReader?.close(); } catch {}
346
+ try { this.stderrReader?.close(); } catch {}
347
+ this.stdoutReader = null;
348
+ this.stderrReader = null;
349
+ if (closeError) this._rejectPendingTurn(closeError);
350
+ resolve({ code, signal });
351
+ });
352
+ });
353
+
354
+ return this.getStatus();
355
+ }
356
+
357
+ async stop() {
358
+ if (!this.child) {
359
+ this.state = 'stopped';
360
+ return this.getStatus();
361
+ }
362
+
363
+ const child = this.child;
364
+ this._terminateChild(child);
365
+ await Promise.race([
366
+ this._closePromise,
367
+ new Promise((resolve) => setTimeout(resolve, this.killGraceMs + 50)),
368
+ ]);
369
+
370
+ this.child = null;
371
+ this.state = 'stopped';
372
+ return this.getStatus();
373
+ }
374
+
375
+ async restart() {
376
+ await this.stop();
377
+ this.state = 'idle';
378
+ return this.start();
379
+ }
380
+
381
+ async run(prompt, options = {}) {
382
+ await this.start();
383
+
384
+ if (this.pendingTurn) {
385
+ throw createWorkerError('ClaudeWorker is already handling another turn', { code: 'WORKER_BUSY' });
386
+ }
387
+
388
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
389
+ const userText = String(prompt ?? '');
390
+ const startedAtMs = Date.now();
391
+
392
+ this.history.push({
393
+ role: 'user',
394
+ content: userText,
395
+ at_ms: startedAtMs,
396
+ });
397
+
398
+ const turnPromise = new Promise((resolve, reject) => {
399
+ const timeout = setTimeout(() => {
400
+ const timeoutError = createWorkerError(`Claude worker timed out after ${timeoutMs}ms`, {
401
+ code: 'ETIMEDOUT',
402
+ stderr: this.stderrLines.join('\n').trim(),
403
+ });
404
+ this._rejectPendingTurn(timeoutError);
405
+ this._terminateChild(this.child);
406
+ }, timeoutMs);
407
+ timeout.unref?.();
408
+
409
+ this.pendingTurn = {
410
+ startedAtMs,
411
+ assistantTexts: [],
412
+ assistantEvents: [],
413
+ timeout,
414
+ resolve,
415
+ reject,
416
+ completed: false,
417
+ };
418
+ });
419
+
420
+ this.state = 'running';
421
+ this._writeFrame({
422
+ type: 'user',
423
+ message: {
424
+ role: 'user',
425
+ content: userText,
426
+ },
427
+ });
428
+
429
+ try {
430
+ return await turnPromise;
431
+ } finally {
432
+ if (this.child) {
433
+ this.state = 'ready';
434
+ }
435
+ }
436
+ }
437
+
438
+ isReady() {
439
+ return this.state === 'ready' || this.state === 'running';
440
+ }
441
+
442
+ async execute(prompt, options = {}) {
443
+ let attempts = 0;
444
+
445
+ try {
446
+ const result = await withRetry(async () => {
447
+ attempts += 1;
448
+ if (attempts > 1) {
449
+ await this.restart();
450
+ }
451
+ return this.run(prompt, options);
452
+ }, {
453
+ ...this.retryOptions,
454
+ shouldRetry: (error) => isClaudeRetryable(error),
455
+ });
456
+
457
+ return {
458
+ output: result.response,
459
+ exitCode: 0,
460
+ threadId: null,
461
+ sessionKey: options.sessionKey || this.sessionId || null,
462
+ raw: result,
463
+ };
464
+ } catch (error) {
465
+ return {
466
+ output: error.stderr || error.message || 'Claude worker failed',
467
+ exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
468
+ threadId: null,
469
+ sessionKey: options.sessionKey || this.sessionId || null,
470
+ error: buildClaudeErrorInfo(error, attempts || 1),
471
+ raw: null,
472
+ };
473
+ }
474
+ }
475
+ }