@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.
- package/hub/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
+
|
|
3
|
+
import { GeminiWorker } from './gemini-worker.mjs';
|
|
4
|
+
import { ClaudeWorker } from './claude-worker.mjs';
|
|
5
|
+
import { CodexMcpWorker } from './codex-mcp.mjs';
|
|
6
|
+
import { DelegatorMcpWorker } from './delegator-mcp.mjs';
|
|
7
|
+
|
|
8
|
+
export function createWorker(type, opts = {}) {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'gemini':
|
|
11
|
+
return new GeminiWorker(opts);
|
|
12
|
+
case 'claude':
|
|
13
|
+
return new ClaudeWorker(opts);
|
|
14
|
+
case 'codex':
|
|
15
|
+
return new CodexMcpWorker(opts);
|
|
16
|
+
case 'delegator':
|
|
17
|
+
return new DelegatorMcpWorker(opts);
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unknown worker type: ${type}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
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
|
+
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 findLastEvent(events, predicate) {
|
|
28
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
29
|
+
if (predicate(events[index])) return events[index];
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildGeminiArgs(options) {
|
|
35
|
+
const args = [];
|
|
36
|
+
|
|
37
|
+
if (options.model) {
|
|
38
|
+
args.push('--model', options.model);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.approvalMode) {
|
|
42
|
+
args.push('--approval-mode', options.approvalMode);
|
|
43
|
+
} else if (options.yolo !== false) {
|
|
44
|
+
args.push('--yolo');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const allowedMcpServers = toStringList(options.allowedMcpServerNames);
|
|
48
|
+
if (allowedMcpServers.length) {
|
|
49
|
+
args.push('--allowed-mcp-server-names', ...allowedMcpServers);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const extraArgs = toStringList(options.extraArgs);
|
|
53
|
+
if (extraArgs.length) args.push(...extraArgs);
|
|
54
|
+
|
|
55
|
+
args.push('--prompt', options.promptArgument ?? '');
|
|
56
|
+
args.push('--output-format', 'stream-json');
|
|
57
|
+
|
|
58
|
+
return args;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createWorkerError(message, details = {}) {
|
|
62
|
+
const error = new Error(message);
|
|
63
|
+
Object.assign(error, details);
|
|
64
|
+
return error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeRetryOptions(retryOptions) {
|
|
68
|
+
if (!retryOptions || typeof retryOptions !== 'object') {
|
|
69
|
+
return Object.freeze({});
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze({ ...retryOptions });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isGeminiRetryable(error) {
|
|
75
|
+
return error?.code === 'WORKER_EXIT'
|
|
76
|
+
&& error?.result?.exitCode !== 0
|
|
77
|
+
&& error?.result?.exitCode !== 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function detectGeminiCategory(error) {
|
|
81
|
+
const combined = `${error?.message || ''}\n${error?.stderr || ''}`.toLowerCase();
|
|
82
|
+
|
|
83
|
+
if (/(unauthorized|forbidden|auth|login|token|credential|apikey|api key)/.test(combined)) {
|
|
84
|
+
return 'auth';
|
|
85
|
+
}
|
|
86
|
+
if (error?.result?.exitCode === 2 || /expected stream-json|unknown option|invalid option|config/.test(combined)) {
|
|
87
|
+
return 'config';
|
|
88
|
+
}
|
|
89
|
+
if (error?.code === 'WORKER_EVENT_ERROR') {
|
|
90
|
+
return 'input';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return 'transient';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildGeminiErrorInfo(error, attempts) {
|
|
97
|
+
const category = detectGeminiCategory(error);
|
|
98
|
+
const retryable = isGeminiRetryable(error);
|
|
99
|
+
let recovery = 'Retry the Gemini worker after correcting the reported issue.';
|
|
100
|
+
|
|
101
|
+
if (category === 'auth') {
|
|
102
|
+
recovery = 'Refresh the Gemini authentication state and retry.';
|
|
103
|
+
} else if (category === 'config') {
|
|
104
|
+
recovery = 'Check the Gemini CLI flags and worker configuration.';
|
|
105
|
+
} else if (category === 'input') {
|
|
106
|
+
recovery = 'Check the Gemini request payload and streamed event format.';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
code: error?.code || 'GEMINI_EXECUTION_ERROR',
|
|
111
|
+
retryable,
|
|
112
|
+
attempts,
|
|
113
|
+
category,
|
|
114
|
+
recovery,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gemini stream-json 래퍼
|
|
120
|
+
*/
|
|
121
|
+
export class GeminiWorker {
|
|
122
|
+
type = 'gemini';
|
|
123
|
+
|
|
124
|
+
constructor(options = {}) {
|
|
125
|
+
this.command = options.command || 'gemini';
|
|
126
|
+
this.commandArgs = toStringList(options.commandArgs || options.args);
|
|
127
|
+
this.cwd = options.cwd || process.cwd();
|
|
128
|
+
this.env = { ...process.env, ...(options.env || {}) };
|
|
129
|
+
this.model = options.model || null;
|
|
130
|
+
this.approvalMode = options.approvalMode || null;
|
|
131
|
+
this.yolo = options.yolo !== false;
|
|
132
|
+
this.allowedMcpServerNames = toStringList(options.allowedMcpServerNames);
|
|
133
|
+
this.extraArgs = toStringList(options.extraArgs);
|
|
134
|
+
this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
|
|
135
|
+
this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
|
|
136
|
+
this.retryOptions = normalizeRetryOptions(options.retryOptions);
|
|
137
|
+
this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
|
|
138
|
+
|
|
139
|
+
this.state = 'idle';
|
|
140
|
+
this.child = null;
|
|
141
|
+
this.lastRun = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getStatus() {
|
|
145
|
+
return {
|
|
146
|
+
type: 'gemini',
|
|
147
|
+
state: this.state,
|
|
148
|
+
pid: this.child?.pid || null,
|
|
149
|
+
last_run_at_ms: this.lastRun?.finishedAtMs || null,
|
|
150
|
+
last_exit_code: this.lastRun?.exitCode ?? null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async start() {
|
|
155
|
+
if (this.state === 'stopped') {
|
|
156
|
+
this.state = 'idle';
|
|
157
|
+
}
|
|
158
|
+
return this.getStatus();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async stop() {
|
|
162
|
+
if (!this.child) {
|
|
163
|
+
this.state = 'stopped';
|
|
164
|
+
return this.getStatus();
|
|
165
|
+
}
|
|
166
|
+
const child = this.child;
|
|
167
|
+
terminateChild(child, this.killGraceMs);
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
child.once('close', resolve);
|
|
170
|
+
setTimeout(resolve, this.killGraceMs + 50).unref?.();
|
|
171
|
+
});
|
|
172
|
+
this.child = null;
|
|
173
|
+
this.state = 'stopped';
|
|
174
|
+
return this.getStatus();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async restart() {
|
|
178
|
+
await this.stop();
|
|
179
|
+
this.state = 'idle';
|
|
180
|
+
return this.getStatus();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async run(prompt, options = {}) {
|
|
184
|
+
if (this.child) {
|
|
185
|
+
throw createWorkerError('GeminiWorker is already running', { code: 'WORKER_BUSY' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.start();
|
|
189
|
+
|
|
190
|
+
const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
|
|
191
|
+
const startedAtMs = Date.now();
|
|
192
|
+
const args = [
|
|
193
|
+
...this.commandArgs,
|
|
194
|
+
...buildGeminiArgs({
|
|
195
|
+
model: options.model || this.model,
|
|
196
|
+
approvalMode: options.approvalMode || this.approvalMode,
|
|
197
|
+
yolo: options.yolo ?? this.yolo,
|
|
198
|
+
allowedMcpServerNames: options.allowedMcpServerNames || this.allowedMcpServerNames,
|
|
199
|
+
extraArgs: options.extraArgs || this.extraArgs,
|
|
200
|
+
promptArgument: options.promptArgument ?? '',
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const child = spawn(this.command, args, {
|
|
205
|
+
cwd: options.cwd || this.cwd,
|
|
206
|
+
env: { ...this.env, ...(options.env || {}) },
|
|
207
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
|
+
windowsHide: true,
|
|
209
|
+
shell: process.platform === 'win32',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this.child = child;
|
|
213
|
+
this.state = 'running';
|
|
214
|
+
|
|
215
|
+
const events = [];
|
|
216
|
+
const stdoutLines = [];
|
|
217
|
+
const stderrLines = [];
|
|
218
|
+
let lastErrorEvent = null;
|
|
219
|
+
let timedOut = false;
|
|
220
|
+
let exitCode = null;
|
|
221
|
+
let exitSignal = null;
|
|
222
|
+
|
|
223
|
+
const stdoutReader = readline.createInterface({
|
|
224
|
+
input: child.stdout,
|
|
225
|
+
crlfDelay: Infinity,
|
|
226
|
+
});
|
|
227
|
+
const stderrReader = readline.createInterface({
|
|
228
|
+
input: child.stderr,
|
|
229
|
+
crlfDelay: Infinity,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
stdoutReader.on('line', (line) => {
|
|
233
|
+
if (!line) return;
|
|
234
|
+
const event = safeJsonParse(line);
|
|
235
|
+
if (!event) {
|
|
236
|
+
stdoutLines.push(line);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
events.push(event);
|
|
241
|
+
if (event.type === 'error') lastErrorEvent = event;
|
|
242
|
+
if (this.onEvent) {
|
|
243
|
+
try { this.onEvent(event); } catch {}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
stderrReader.on('line', (line) => {
|
|
248
|
+
if (!line) return;
|
|
249
|
+
stderrLines.push(line);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
253
|
+
child.once('error', reject);
|
|
254
|
+
child.once('close', (code, signal) => {
|
|
255
|
+
exitCode = code;
|
|
256
|
+
exitSignal = signal;
|
|
257
|
+
resolve();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const timeout = setTimeout(() => {
|
|
262
|
+
timedOut = true;
|
|
263
|
+
terminateChild(child, this.killGraceMs);
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
timeout.unref?.();
|
|
266
|
+
|
|
267
|
+
child.stdin.on('error', () => {});
|
|
268
|
+
child.stdin.end(String(prompt ?? ''));
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await closePromise;
|
|
272
|
+
} finally {
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
stdoutReader.close();
|
|
275
|
+
stderrReader.close();
|
|
276
|
+
if (this.child === child) {
|
|
277
|
+
this.child = null;
|
|
278
|
+
}
|
|
279
|
+
this.state = 'idle';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const resultEvent = findLastEvent(events, (event) => event?.type === 'result');
|
|
283
|
+
const response = [
|
|
284
|
+
extractText(resultEvent),
|
|
285
|
+
...events
|
|
286
|
+
.filter((event) => event?.type === 'message' || event?.type === 'assistant')
|
|
287
|
+
.map((event) => extractText(event))
|
|
288
|
+
.filter(Boolean),
|
|
289
|
+
...stdoutLines,
|
|
290
|
+
]
|
|
291
|
+
.filter(Boolean)
|
|
292
|
+
.join('\n')
|
|
293
|
+
.trim();
|
|
294
|
+
|
|
295
|
+
const result = {
|
|
296
|
+
type: 'gemini',
|
|
297
|
+
command: this.command,
|
|
298
|
+
args,
|
|
299
|
+
response,
|
|
300
|
+
events,
|
|
301
|
+
resultEvent,
|
|
302
|
+
usage: resultEvent?.usage || null,
|
|
303
|
+
stdout: stdoutLines.join('\n').trim(),
|
|
304
|
+
stderr: stderrLines.join('\n').trim(),
|
|
305
|
+
exitCode,
|
|
306
|
+
exitSignal,
|
|
307
|
+
timedOut,
|
|
308
|
+
startedAtMs,
|
|
309
|
+
finishedAtMs: Date.now(),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
this.lastRun = result;
|
|
313
|
+
|
|
314
|
+
if (timedOut) {
|
|
315
|
+
throw createWorkerError(`Gemini worker timed out after ${timeoutMs}ms`, {
|
|
316
|
+
code: 'ETIMEDOUT',
|
|
317
|
+
result,
|
|
318
|
+
stderr: result.stderr,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (exitCode !== 0) {
|
|
323
|
+
throw createWorkerError(`Gemini worker exited with code ${exitCode}`, {
|
|
324
|
+
code: 'WORKER_EXIT',
|
|
325
|
+
result,
|
|
326
|
+
stderr: result.stderr,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (lastErrorEvent) {
|
|
331
|
+
throw createWorkerError('Gemini worker emitted an error event', {
|
|
332
|
+
code: 'WORKER_EVENT_ERROR',
|
|
333
|
+
result,
|
|
334
|
+
stderr: result.stderr,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
isReady() {
|
|
342
|
+
return this.state !== 'stopped';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async execute(prompt, options = {}) {
|
|
346
|
+
let attempts = 0;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const result = await withRetry(async () => {
|
|
350
|
+
attempts += 1;
|
|
351
|
+
return this.run(prompt, options);
|
|
352
|
+
}, {
|
|
353
|
+
...this.retryOptions,
|
|
354
|
+
shouldRetry: (error) => isGeminiRetryable(error),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
output: result.response,
|
|
359
|
+
exitCode: 0,
|
|
360
|
+
sessionKey: options.sessionKey || null,
|
|
361
|
+
raw: result,
|
|
362
|
+
};
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return {
|
|
365
|
+
output: error.stderr || error.message || 'Gemini worker failed',
|
|
366
|
+
exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
|
|
367
|
+
sessionKey: options.sessionKey || null,
|
|
368
|
+
error: buildGeminiErrorInfo(error, attempts || 1),
|
|
369
|
+
raw: error.result || null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// hub/workers/interface.mjs — Worker 공통 인터페이스 정의
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 워커 실행 옵션
|
|
5
|
+
* @typedef {object} WorkerExecuteOptions
|
|
6
|
+
* @property {string} [cwd] - 워커 작업 디렉터리
|
|
7
|
+
* @property {string} [sessionKey] - 내부 세션 키
|
|
8
|
+
* @property {string} [threadId] - 외부에서 지정한 Codex threadId
|
|
9
|
+
* @property {boolean} [resetSession] - 기존 세션을 무시하고 새 세션 시작 여부
|
|
10
|
+
* @property {string} [model] - Codex 모델 이름
|
|
11
|
+
* @property {string} [profile] - Codex 프로필 이름
|
|
12
|
+
* @property {'untrusted'|'on-failure'|'on-request'|'never'} [approvalPolicy] - 승인 정책
|
|
13
|
+
* @property {'read-only'|'workspace-write'|'danger-full-access'} [sandbox] - 샌드박스 정책
|
|
14
|
+
* @property {Record<string, unknown>} [config] - 추가 Codex 설정
|
|
15
|
+
* @property {string} [baseInstructions] - 기본 시스템 지침
|
|
16
|
+
* @property {string} [developerInstructions] - 개발자 지침
|
|
17
|
+
* @property {string} [compactPrompt] - 컴팩션 프롬프트
|
|
18
|
+
* @property {number} [timeoutMs] - MCP 요청 타임아웃(ms)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 워커 구조화 오류 메타데이터
|
|
23
|
+
* @typedef {object} WorkerErrorInfo
|
|
24
|
+
* @property {string} code - 오류 코드
|
|
25
|
+
* @property {boolean} retryable - 재시도 대상 여부
|
|
26
|
+
* @property {number} attempts - 실행 시도 횟수
|
|
27
|
+
* @property {'transient'|'auth'|'config'|'input'} category - 오류 분류
|
|
28
|
+
* @property {string} recovery - 권장 복구 가이드
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 워커 실행 결과
|
|
33
|
+
* @typedef {object} WorkerResult
|
|
34
|
+
* @property {string} output - 최종 텍스트 출력
|
|
35
|
+
* @property {number} exitCode - 종료 코드(0=성공)
|
|
36
|
+
* @property {string | null} [threadId] - Codex 세션 threadId
|
|
37
|
+
* @property {string | null} [sessionKey] - 내부 세션 키
|
|
38
|
+
* @property {WorkerErrorInfo} [error] - 구조화 오류 메타데이터
|
|
39
|
+
* @property {unknown} [raw] - 원본 tool call 결과
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 공통 워커 인터페이스
|
|
44
|
+
* @typedef {object} IWorker
|
|
45
|
+
* @property {(prompt: string, opts?: WorkerExecuteOptions) => Promise<WorkerResult>} execute
|
|
46
|
+
* @property {() => Promise<void>} start
|
|
47
|
+
* @property {() => Promise<void>} stop
|
|
48
|
+
* @property {() => boolean} isReady
|
|
49
|
+
* @property {string} type - 'codex' | 'gemini' | 'claude' | 'delegator'
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
export const WORKER_TYPES = Object.freeze(['codex', 'gemini', 'claude', 'delegator']);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// hub/workers/worker-utils.mjs — 워커 공통 유틸리티
|
|
2
|
+
// claude-worker, gemini-worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_KILL_GRACE_MS = 1000;
|
|
6
|
+
|
|
7
|
+
export function toStringList(value) {
|
|
8
|
+
if (!Array.isArray(value)) return [];
|
|
9
|
+
return value
|
|
10
|
+
.map((item) => String(item ?? '').trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function safeJsonParse(line) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(line);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createWorkerError(message, details = {}) {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
Object.assign(error, details);
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sleep(delayMs) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const timer = setTimeout(resolve, Math.max(0, delayMs));
|
|
31
|
+
timer.unref?.();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function withRetry(fn, opts = {}) {
|
|
36
|
+
const {
|
|
37
|
+
maxAttempts = 3,
|
|
38
|
+
baseDelayMs = 1000,
|
|
39
|
+
maxDelayMs = 15000,
|
|
40
|
+
shouldRetry = () => true,
|
|
41
|
+
} = opts;
|
|
42
|
+
|
|
43
|
+
let lastError;
|
|
44
|
+
|
|
45
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
lastError = error;
|
|
50
|
+
if (attempt >= maxAttempts || !shouldRetry(error, attempt)) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs)
|
|
55
|
+
* (0.5 + Math.random() * 0.5);
|
|
56
|
+
await sleep(delay);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw lastError;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function appendTextFragments(value, parts) {
|
|
64
|
+
if (value == null) return;
|
|
65
|
+
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (trimmed) parts.push(trimmed);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
for (const item of value) appendTextFragments(item, parts);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value !== 'object') return;
|
|
78
|
+
|
|
79
|
+
if (typeof value.text === 'string') appendTextFragments(value.text, parts);
|
|
80
|
+
if (typeof value.response === 'string') appendTextFragments(value.response, parts);
|
|
81
|
+
if (typeof value.result === 'string') appendTextFragments(value.result, parts);
|
|
82
|
+
if (value.content != null) appendTextFragments(value.content, parts);
|
|
83
|
+
if (value.message != null) appendTextFragments(value.message, parts);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function extractText(value) {
|
|
87
|
+
const parts = [];
|
|
88
|
+
appendTextFragments(value, parts);
|
|
89
|
+
return parts.join('\n').trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function terminateChild(child, killGraceMs) {
|
|
93
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
94
|
+
|
|
95
|
+
try { child.stdin.end(); } catch {}
|
|
96
|
+
try { child.kill(); } catch {}
|
|
97
|
+
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
if (child.exitCode === null) {
|
|
100
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
101
|
+
}
|
|
102
|
+
}, killGraceMs);
|
|
103
|
+
timer.unref?.();
|
|
104
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triflux/remote",
|
|
3
|
+
"version": "10.0.0-alpha.1",
|
|
4
|
+
"description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "hub/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./hub/index.mjs",
|
|
9
|
+
"./hub/*": "./hub/*"
|
|
10
|
+
},
|
|
11
|
+
"engines": { "node": ">=18.0.0" },
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@triflux/core": "^10.0.0-alpha.1"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
17
|
+
"better-sqlite3": "^12.6.2",
|
|
18
|
+
"systray2": "^2.1.4"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"hub"
|
|
22
|
+
],
|
|
23
|
+
"keywords": ["triflux", "team-mode", "psmux", "remote", "mcp-workers"],
|
|
24
|
+
"author": "tellang",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/tellang/triflux.git",
|
|
29
|
+
"directory": "packages/remote"
|
|
30
|
+
}
|
|
31
|
+
}
|