triflux 9.7.13 → 9.8.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 (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +2 -0
  4. package/README.md +2 -0
  5. package/bin/triflux.mjs +297 -47
  6. package/hooks/hook-registry.json +4 -4
  7. package/hub/fullcycle.mjs +96 -0
  8. package/hub/paths.mjs +30 -28
  9. package/hub/pipeline/index.mjs +318 -318
  10. package/hub/schema.sql +146 -146
  11. package/hub/team/cli/commands/kill.mjs +37 -37
  12. package/hub/team/cli/commands/stop.mjs +31 -31
  13. package/hub/team/cli/commands/task.mjs +30 -30
  14. package/hub/team/cli/services/hub-client.mjs +208 -208
  15. package/hub/team/cli/services/native-control.mjs +118 -118
  16. package/hub/team/cli/services/runtime-mode.mjs +62 -62
  17. package/hub/team/cli/services/state-store.mjs +48 -48
  18. package/hub/team/dashboard.mjs +274 -274
  19. package/hub/team/native.mjs +649 -649
  20. package/hub/team/psmux.mjs +68 -13
  21. package/hub/tools.mjs +554 -554
  22. package/hub/workers/claude-worker.mjs +423 -423
  23. package/hub/workers/codex-mcp.mjs +410 -410
  24. package/hub/workers/gemini-worker.mjs +429 -429
  25. package/hub/workers/interface.mjs +40 -40
  26. package/package.json +1 -1
  27. package/scripts/__tests__/remote-spawn-transfer.test.mjs +1 -1
  28. package/scripts/cache-warmup.mjs +1 -0
  29. package/scripts/claude-logged.ps1 +54 -0
  30. package/scripts/demo-tui.mjs +59 -0
  31. package/scripts/headless-guard.mjs +4 -7
  32. package/scripts/hub-ensure.mjs +120 -120
  33. package/scripts/lib/psmux-info.mjs +119 -0
  34. package/scripts/lib/remote-spawn-transfer.mjs +1 -1
  35. package/scripts/setup.mjs +150 -6
  36. package/scripts/tfx-route-post.mjs +90 -13
  37. package/scripts/token-snapshot.mjs +575 -575
  38. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  39. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  40. package/skills/.omc/state/last-tool-error.json +7 -0
  41. package/skills/.omc/state/subagent-tracking.json +7 -0
  42. package/skills/tfx-codex-swarm/SKILL.md +40 -5
  43. package/skills/tfx-codex-swarm/mcp-daemon/register-autostart.ps1 +32 -0
  44. package/skills/tfx-doctor/SKILL.md +3 -0
  45. package/skills/tfx-fullcycle/SKILL.md +79 -4
  46. package/skills/tfx-hub/SKILL.md +3 -1
  47. package/skills/tfx-psmux-rules/SKILL.md +53 -31
  48. package/skills/tfx-remote-spawn/references/hosts.json +16 -16
  49. package/skills/tfx-setup/SKILL.md +9 -0
  50. package/tui/doctor.mjs +1 -0
@@ -1,410 +1,410 @@
1
- // hub/workers/codex-mcp.mjs — Codex MCP 서버 래퍼
2
- import process from 'node:process';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
-
8
- const REQUIRED_TOOLS = ['codex', 'codex-reply'];
9
-
10
- export const CODEX_MCP_TRANSPORT_EXIT_CODE = 70;
11
- export const CODEX_MCP_EXECUTION_EXIT_CODE = 1;
12
- export const DEFAULT_CODEX_MCP_TIMEOUT_MS = 10 * 60 * 1000;
13
- export const DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS = 10 * 1000;
14
-
15
- /**
16
- * Codex MCP transport/bootstrap 계층 오류
17
- */
18
- export class CodexMcpTransportError extends Error {
19
- /**
20
- * @param {string} message
21
- * @param {object} [options]
22
- * @param {unknown} [options.cause]
23
- * @param {string} [options.stderr]
24
- */
25
- constructor(message, options = {}) {
26
- super(message, { cause: options.cause });
27
- this.name = 'CodexMcpTransportError';
28
- this.stderr = options.stderr || '';
29
- }
30
- }
31
-
32
- function cloneEnv(env = process.env) {
33
- return Object.fromEntries(
34
- Object.entries(env).filter(([, value]) => typeof value === 'string'),
35
- );
36
- }
37
-
38
- function collectTextContent(content = []) {
39
- return content
40
- .filter((item) => item?.type === 'text' && typeof item.text === 'string')
41
- .map((item) => item.text)
42
- .join('\n')
43
- .trim();
44
- }
45
-
46
- function normalizeStructuredContent(structuredContent, fallbackText = '') {
47
- if (!structuredContent || typeof structuredContent !== 'object') {
48
- return { threadId: null, content: fallbackText };
49
- }
50
-
51
- const threadId = typeof structuredContent.threadId === 'string'
52
- ? structuredContent.threadId
53
- : null;
54
- const content = typeof structuredContent.content === 'string'
55
- ? structuredContent.content
56
- : fallbackText;
57
-
58
- return { threadId, content };
59
- }
60
-
61
- function buildCodexArguments(prompt, opts = {}) {
62
- const args = { prompt };
63
-
64
- if (typeof opts.cwd === 'string' && opts.cwd) args.cwd = opts.cwd;
65
- if (typeof opts.model === 'string' && opts.model) args.model = opts.model;
66
- if (typeof opts.profile === 'string' && opts.profile) args.profile = opts.profile;
67
- if (typeof opts.approvalPolicy === 'string' && opts.approvalPolicy) {
68
- args['approval-policy'] = opts.approvalPolicy;
69
- }
70
- if (typeof opts.sandbox === 'string' && opts.sandbox) args.sandbox = opts.sandbox;
71
- if (opts.config && typeof opts.config === 'object') args.config = opts.config;
72
- if (typeof opts.baseInstructions === 'string' && opts.baseInstructions) {
73
- args['base-instructions'] = opts.baseInstructions;
74
- }
75
- if (typeof opts.developerInstructions === 'string' && opts.developerInstructions) {
76
- args['developer-instructions'] = opts.developerInstructions;
77
- }
78
- if (typeof opts.compactPrompt === 'string' && opts.compactPrompt) {
79
- args['compact-prompt'] = opts.compactPrompt;
80
- }
81
-
82
- return args;
83
- }
84
-
85
- function pickToolName(threadId) {
86
- return threadId ? 'codex-reply' : 'codex';
87
- }
88
-
89
- function withTimeout(promise, timeoutMs, message) {
90
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
91
- return promise;
92
- }
93
-
94
- let timer;
95
- const timeoutPromise = new Promise((_, reject) => {
96
- timer = setTimeout(() => {
97
- reject(new Error(message));
98
- }, timeoutMs);
99
- timer.unref?.();
100
- });
101
-
102
- return Promise.race([promise, timeoutPromise]).finally(() => {
103
- clearTimeout(timer);
104
- });
105
- }
106
-
107
- /**
108
- * Codex MCP 워커
109
- */
110
- export class CodexMcpWorker {
111
- type = 'codex';
112
-
113
- /**
114
- * @param {object} [options]
115
- * @param {string} [options.command]
116
- * @param {string[]} [options.args]
117
- * @param {string} [options.cwd]
118
- * @param {Record<string, string>} [options.env]
119
- * @param {{ name: string, version: string }} [options.clientInfo]
120
- * @param {number} [options.bootstrapTimeoutMs]
121
- */
122
- constructor(options = {}) {
123
- this.command = options.command || process.env.CODEX_BIN || 'codex';
124
- this.args = Array.isArray(options.args) && options.args.length
125
- ? [...options.args]
126
- : ['mcp-server'];
127
- this.cwd = options.cwd || process.cwd();
128
- this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
129
- this.clientInfo = options.clientInfo || { name: 'triflux-codex-mcp', version: '1.0.0' };
130
- this.bootstrapTimeoutMs = Number.isFinite(options.bootstrapTimeoutMs)
131
- ? options.bootstrapTimeoutMs
132
- : DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS;
133
-
134
- this.client = null;
135
- this.transport = null;
136
- this.ready = false;
137
- this.availableTools = new Set();
138
- this.threadIds = new Map();
139
- this.serverStderr = '';
140
- }
141
-
142
- isReady() {
143
- return this.ready;
144
- }
145
-
146
- getThreadId(sessionKey) {
147
- return this.threadIds.get(sessionKey) || null;
148
- }
149
-
150
- setThreadId(sessionKey, threadId) {
151
- if (!sessionKey || !threadId) return;
152
- this.threadIds.set(sessionKey, threadId);
153
- }
154
-
155
- clearThread(sessionKey) {
156
- if (!sessionKey) return;
157
- this.threadIds.delete(sessionKey);
158
- }
159
-
160
- async start() {
161
- if (this.ready && this.client && this.transport) return;
162
-
163
- await this.stop();
164
-
165
- const transport = new StdioClientTransport({
166
- command: this.command,
167
- args: this.args,
168
- cwd: this.cwd,
169
- env: this.env,
170
- stderr: 'pipe',
171
- });
172
- const client = new Client(this.clientInfo, { capabilities: {} });
173
-
174
- this.serverStderr = '';
175
- transport.stderr?.on('data', (chunk) => {
176
- this.serverStderr += String(chunk);
177
- if (this.serverStderr.length > 16000) {
178
- this.serverStderr = this.serverStderr.slice(-16000);
179
- }
180
- });
181
-
182
- try {
183
- await withTimeout((async () => {
184
- await client.connect(transport);
185
- const tools = await client.listTools(undefined, { timeout: this.bootstrapTimeoutMs });
186
- this.availableTools = new Set(tools.tools.map((tool) => tool.name));
187
-
188
- for (const requiredTool of REQUIRED_TOOLS) {
189
- if (!this.availableTools.has(requiredTool)) {
190
- throw new Error(`필수 MCP 도구 누락: ${requiredTool}`);
191
- }
192
- }
193
- })(), this.bootstrapTimeoutMs, `Codex MCP bootstrap timeout (${this.bootstrapTimeoutMs}ms)`);
194
- } catch (error) {
195
- await client.close().catch(() => {});
196
- transport.stderr?.destroy?.();
197
- throw new CodexMcpTransportError(
198
- `Codex MCP 연결 실패: ${error instanceof Error ? error.message : String(error)}`,
199
- { cause: error, stderr: this.serverStderr.trim() },
200
- );
201
- }
202
-
203
- this.client = client;
204
- this.transport = transport;
205
- this.ready = true;
206
- }
207
-
208
- async stop() {
209
- this.ready = false;
210
- this.availableTools.clear();
211
-
212
- const client = this.client;
213
- const transport = this.transport;
214
- this.transport = null;
215
- this.client = null;
216
-
217
- if (client) {
218
- await client.close().catch(() => {});
219
- } else if (transport) {
220
- await transport.close().catch(() => {});
221
- }
222
-
223
- transport?.stderr?.destroy?.();
224
- }
225
-
226
- /**
227
- * @param {string} prompt
228
- * @param {import('./interface.mjs').WorkerExecuteOptions} [opts]
229
- * @returns {Promise<import('./interface.mjs').WorkerResult>}
230
- */
231
- async execute(prompt, opts = {}) {
232
- if (typeof prompt !== 'string' || !prompt.trim()) {
233
- return {
234
- output: 'prompt는 비어 있을 수 없습니다.',
235
- exitCode: CODEX_MCP_EXECUTION_EXIT_CODE,
236
- threadId: null,
237
- sessionKey: opts.sessionKey || null,
238
- raw: null,
239
- };
240
- }
241
-
242
- await this.start();
243
-
244
- const sessionKey = typeof opts.sessionKey === 'string' && opts.sessionKey
245
- ? opts.sessionKey
246
- : null;
247
-
248
- if (opts.resetSession && sessionKey) {
249
- this.clearThread(sessionKey);
250
- }
251
-
252
- const threadId = typeof opts.threadId === 'string' && opts.threadId
253
- ? opts.threadId
254
- : (sessionKey ? this.getThreadId(sessionKey) : null);
255
-
256
- const toolName = pickToolName(threadId);
257
- const toolArguments = toolName === 'codex-reply'
258
- ? { prompt, threadId }
259
- : buildCodexArguments(prompt, opts);
260
-
261
- let rawResult;
262
- try {
263
- rawResult = await this.client.callTool(
264
- { name: toolName, arguments: toolArguments },
265
- undefined,
266
- { timeout: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_CODEX_MCP_TIMEOUT_MS },
267
- );
268
- } catch (error) {
269
- return {
270
- output: error instanceof Error ? error.message : String(error),
271
- exitCode: CODEX_MCP_EXECUTION_EXIT_CODE,
272
- threadId,
273
- sessionKey,
274
- raw: null,
275
- };
276
- }
277
-
278
- const textContent = collectTextContent(rawResult.content);
279
- const normalized = normalizeStructuredContent(rawResult.structuredContent, textContent);
280
-
281
- if (sessionKey && normalized.threadId) {
282
- this.setThreadId(sessionKey, normalized.threadId);
283
- }
284
-
285
- return {
286
- output: normalized.content,
287
- exitCode: rawResult.isError ? CODEX_MCP_EXECUTION_EXIT_CODE : 0,
288
- threadId: normalized.threadId,
289
- sessionKey,
290
- raw: rawResult,
291
- };
292
- }
293
- }
294
-
295
- function parseCliArgs(argv) {
296
- const options = {
297
- command: process.env.CODEX_BIN || 'codex',
298
- cwd: process.cwd(),
299
- timeoutMs: DEFAULT_CODEX_MCP_TIMEOUT_MS,
300
- };
301
-
302
- for (let i = 0; i < argv.length; i += 1) {
303
- const token = argv[i];
304
- const next = () => {
305
- const value = argv[i + 1];
306
- if (value === undefined) {
307
- throw new Error(`${token} 값이 필요합니다.`);
308
- }
309
- i += 1;
310
- return value;
311
- };
312
-
313
- switch (token) {
314
- case '--prompt':
315
- options.prompt = next();
316
- break;
317
- case '--thread-id':
318
- options.threadId = next();
319
- break;
320
- case '--session-key':
321
- options.sessionKey = next();
322
- break;
323
- case '--cwd':
324
- options.cwd = next();
325
- break;
326
- case '--profile':
327
- options.profile = next();
328
- break;
329
- case '--model':
330
- options.model = next();
331
- break;
332
- case '--approval-policy':
333
- options.approvalPolicy = next();
334
- break;
335
- case '--sandbox':
336
- options.sandbox = next();
337
- break;
338
- case '--base-instructions':
339
- options.baseInstructions = next();
340
- break;
341
- case '--developer-instructions':
342
- options.developerInstructions = next();
343
- break;
344
- case '--compact-prompt':
345
- options.compactPrompt = next();
346
- break;
347
- case '--timeout-ms':
348
- options.timeoutMs = Number.parseInt(next(), 10);
349
- break;
350
- case '--config-json':
351
- options.config = JSON.parse(next());
352
- break;
353
- case '--codex-command':
354
- options.command = next();
355
- break;
356
- case '--reset-session':
357
- options.resetSession = true;
358
- break;
359
- default:
360
- throw new Error(`알 수 없는 옵션: ${token}`);
361
- }
362
- }
363
-
364
- if (typeof options.prompt !== 'string' || !options.prompt) {
365
- throw new Error('--prompt는 필수입니다.');
366
- }
367
-
368
- return options;
369
- }
370
-
371
- export async function runCodexMcpCli(argv = process.argv.slice(2)) {
372
- let options;
373
- try {
374
- options = parseCliArgs(argv);
375
- } catch (error) {
376
- console.error(`[codex-mcp] ${error instanceof Error ? error.message : String(error)}`);
377
- process.exitCode = 64;
378
- return;
379
- }
380
-
381
- const worker = new CodexMcpWorker({
382
- command: options.command,
383
- cwd: options.cwd,
384
- });
385
-
386
- try {
387
- const result = await worker.execute(options.prompt, options);
388
- if (result.output) {
389
- process.stdout.write(result.output);
390
- if (!result.output.endsWith('\n')) {
391
- process.stdout.write('\n');
392
- }
393
- }
394
- process.exitCode = result.exitCode;
395
- } catch (error) {
396
- const lines = [error instanceof Error ? error.message : String(error)];
397
- if (error instanceof CodexMcpTransportError && error.stderr) {
398
- lines.push(error.stderr);
399
- }
400
- console.error(`[codex-mcp] ${lines.join('\n')}`);
401
- process.exitCode = CODEX_MCP_TRANSPORT_EXIT_CODE;
402
- } finally {
403
- await worker.stop();
404
- }
405
- }
406
-
407
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
408
- await runCodexMcpCli();
409
- process.exit(process.exitCode ?? 0);
410
- }
1
+ // hub/workers/codex-mcp.mjs — Codex MCP 서버 래퍼
2
+ import process from 'node:process';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
+
8
+ const REQUIRED_TOOLS = ['codex', 'codex-reply'];
9
+
10
+ export const CODEX_MCP_TRANSPORT_EXIT_CODE = 70;
11
+ export const CODEX_MCP_EXECUTION_EXIT_CODE = 1;
12
+ export const DEFAULT_CODEX_MCP_TIMEOUT_MS = 10 * 60 * 1000;
13
+ export const DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS = 10 * 1000;
14
+
15
+ /**
16
+ * Codex MCP transport/bootstrap 계층 오류
17
+ */
18
+ export class CodexMcpTransportError extends Error {
19
+ /**
20
+ * @param {string} message
21
+ * @param {object} [options]
22
+ * @param {unknown} [options.cause]
23
+ * @param {string} [options.stderr]
24
+ */
25
+ constructor(message, options = {}) {
26
+ super(message, { cause: options.cause });
27
+ this.name = 'CodexMcpTransportError';
28
+ this.stderr = options.stderr || '';
29
+ }
30
+ }
31
+
32
+ function cloneEnv(env = process.env) {
33
+ return Object.fromEntries(
34
+ Object.entries(env).filter(([, value]) => typeof value === 'string'),
35
+ );
36
+ }
37
+
38
+ function collectTextContent(content = []) {
39
+ return content
40
+ .filter((item) => item?.type === 'text' && typeof item.text === 'string')
41
+ .map((item) => item.text)
42
+ .join('\n')
43
+ .trim();
44
+ }
45
+
46
+ function normalizeStructuredContent(structuredContent, fallbackText = '') {
47
+ if (!structuredContent || typeof structuredContent !== 'object') {
48
+ return { threadId: null, content: fallbackText };
49
+ }
50
+
51
+ const threadId = typeof structuredContent.threadId === 'string'
52
+ ? structuredContent.threadId
53
+ : null;
54
+ const content = typeof structuredContent.content === 'string'
55
+ ? structuredContent.content
56
+ : fallbackText;
57
+
58
+ return { threadId, content };
59
+ }
60
+
61
+ function buildCodexArguments(prompt, opts = {}) {
62
+ const args = { prompt };
63
+
64
+ if (typeof opts.cwd === 'string' && opts.cwd) args.cwd = opts.cwd;
65
+ if (typeof opts.model === 'string' && opts.model) args.model = opts.model;
66
+ if (typeof opts.profile === 'string' && opts.profile) args.profile = opts.profile;
67
+ if (typeof opts.approvalPolicy === 'string' && opts.approvalPolicy) {
68
+ args['approval-policy'] = opts.approvalPolicy;
69
+ }
70
+ if (typeof opts.sandbox === 'string' && opts.sandbox) args.sandbox = opts.sandbox;
71
+ if (opts.config && typeof opts.config === 'object') args.config = opts.config;
72
+ if (typeof opts.baseInstructions === 'string' && opts.baseInstructions) {
73
+ args['base-instructions'] = opts.baseInstructions;
74
+ }
75
+ if (typeof opts.developerInstructions === 'string' && opts.developerInstructions) {
76
+ args['developer-instructions'] = opts.developerInstructions;
77
+ }
78
+ if (typeof opts.compactPrompt === 'string' && opts.compactPrompt) {
79
+ args['compact-prompt'] = opts.compactPrompt;
80
+ }
81
+
82
+ return args;
83
+ }
84
+
85
+ function pickToolName(threadId) {
86
+ return threadId ? 'codex-reply' : 'codex';
87
+ }
88
+
89
+ function withTimeout(promise, timeoutMs, message) {
90
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
91
+ return promise;
92
+ }
93
+
94
+ let timer;
95
+ const timeoutPromise = new Promise((_, reject) => {
96
+ timer = setTimeout(() => {
97
+ reject(new Error(message));
98
+ }, timeoutMs);
99
+ timer.unref?.();
100
+ });
101
+
102
+ return Promise.race([promise, timeoutPromise]).finally(() => {
103
+ clearTimeout(timer);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Codex MCP 워커
109
+ */
110
+ export class CodexMcpWorker {
111
+ type = 'codex';
112
+
113
+ /**
114
+ * @param {object} [options]
115
+ * @param {string} [options.command]
116
+ * @param {string[]} [options.args]
117
+ * @param {string} [options.cwd]
118
+ * @param {Record<string, string>} [options.env]
119
+ * @param {{ name: string, version: string }} [options.clientInfo]
120
+ * @param {number} [options.bootstrapTimeoutMs]
121
+ */
122
+ constructor(options = {}) {
123
+ this.command = options.command || process.env.CODEX_BIN || 'codex';
124
+ this.args = Array.isArray(options.args) && options.args.length
125
+ ? [...options.args]
126
+ : ['mcp-server'];
127
+ this.cwd = options.cwd || process.cwd();
128
+ this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
129
+ this.clientInfo = options.clientInfo || { name: 'triflux-codex-mcp', version: '1.0.0' };
130
+ this.bootstrapTimeoutMs = Number.isFinite(options.bootstrapTimeoutMs)
131
+ ? options.bootstrapTimeoutMs
132
+ : DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS;
133
+
134
+ this.client = null;
135
+ this.transport = null;
136
+ this.ready = false;
137
+ this.availableTools = new Set();
138
+ this.threadIds = new Map();
139
+ this.serverStderr = '';
140
+ }
141
+
142
+ isReady() {
143
+ return this.ready;
144
+ }
145
+
146
+ getThreadId(sessionKey) {
147
+ return this.threadIds.get(sessionKey) || null;
148
+ }
149
+
150
+ setThreadId(sessionKey, threadId) {
151
+ if (!sessionKey || !threadId) return;
152
+ this.threadIds.set(sessionKey, threadId);
153
+ }
154
+
155
+ clearThread(sessionKey) {
156
+ if (!sessionKey) return;
157
+ this.threadIds.delete(sessionKey);
158
+ }
159
+
160
+ async start() {
161
+ if (this.ready && this.client && this.transport) return;
162
+
163
+ await this.stop();
164
+
165
+ const transport = new StdioClientTransport({
166
+ command: this.command,
167
+ args: this.args,
168
+ cwd: this.cwd,
169
+ env: this.env,
170
+ stderr: 'pipe',
171
+ });
172
+ const client = new Client(this.clientInfo, { capabilities: {} });
173
+
174
+ this.serverStderr = '';
175
+ transport.stderr?.on('data', (chunk) => {
176
+ this.serverStderr += String(chunk);
177
+ if (this.serverStderr.length > 16000) {
178
+ this.serverStderr = this.serverStderr.slice(-16000);
179
+ }
180
+ });
181
+
182
+ try {
183
+ await withTimeout((async () => {
184
+ await client.connect(transport);
185
+ const tools = await client.listTools(undefined, { timeout: this.bootstrapTimeoutMs });
186
+ this.availableTools = new Set(tools.tools.map((tool) => tool.name));
187
+
188
+ for (const requiredTool of REQUIRED_TOOLS) {
189
+ if (!this.availableTools.has(requiredTool)) {
190
+ throw new Error(`필수 MCP 도구 누락: ${requiredTool}`);
191
+ }
192
+ }
193
+ })(), this.bootstrapTimeoutMs, `Codex MCP bootstrap timeout (${this.bootstrapTimeoutMs}ms)`);
194
+ } catch (error) {
195
+ await client.close().catch(() => {});
196
+ transport.stderr?.destroy?.();
197
+ throw new CodexMcpTransportError(
198
+ `Codex MCP 연결 실패: ${error instanceof Error ? error.message : String(error)}`,
199
+ { cause: error, stderr: this.serverStderr.trim() },
200
+ );
201
+ }
202
+
203
+ this.client = client;
204
+ this.transport = transport;
205
+ this.ready = true;
206
+ }
207
+
208
+ async stop() {
209
+ this.ready = false;
210
+ this.availableTools.clear();
211
+
212
+ const client = this.client;
213
+ const transport = this.transport;
214
+ this.transport = null;
215
+ this.client = null;
216
+
217
+ if (client) {
218
+ await client.close().catch(() => {});
219
+ } else if (transport) {
220
+ await transport.close().catch(() => {});
221
+ }
222
+
223
+ transport?.stderr?.destroy?.();
224
+ }
225
+
226
+ /**
227
+ * @param {string} prompt
228
+ * @param {import('./interface.mjs').WorkerExecuteOptions} [opts]
229
+ * @returns {Promise<import('./interface.mjs').WorkerResult>}
230
+ */
231
+ async execute(prompt, opts = {}) {
232
+ if (typeof prompt !== 'string' || !prompt.trim()) {
233
+ return {
234
+ output: 'prompt는 비어 있을 수 없습니다.',
235
+ exitCode: CODEX_MCP_EXECUTION_EXIT_CODE,
236
+ threadId: null,
237
+ sessionKey: opts.sessionKey || null,
238
+ raw: null,
239
+ };
240
+ }
241
+
242
+ await this.start();
243
+
244
+ const sessionKey = typeof opts.sessionKey === 'string' && opts.sessionKey
245
+ ? opts.sessionKey
246
+ : null;
247
+
248
+ if (opts.resetSession && sessionKey) {
249
+ this.clearThread(sessionKey);
250
+ }
251
+
252
+ const threadId = typeof opts.threadId === 'string' && opts.threadId
253
+ ? opts.threadId
254
+ : (sessionKey ? this.getThreadId(sessionKey) : null);
255
+
256
+ const toolName = pickToolName(threadId);
257
+ const toolArguments = toolName === 'codex-reply'
258
+ ? { prompt, threadId }
259
+ : buildCodexArguments(prompt, opts);
260
+
261
+ let rawResult;
262
+ try {
263
+ rawResult = await this.client.callTool(
264
+ { name: toolName, arguments: toolArguments },
265
+ undefined,
266
+ { timeout: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_CODEX_MCP_TIMEOUT_MS },
267
+ );
268
+ } catch (error) {
269
+ return {
270
+ output: error instanceof Error ? error.message : String(error),
271
+ exitCode: CODEX_MCP_EXECUTION_EXIT_CODE,
272
+ threadId,
273
+ sessionKey,
274
+ raw: null,
275
+ };
276
+ }
277
+
278
+ const textContent = collectTextContent(rawResult.content);
279
+ const normalized = normalizeStructuredContent(rawResult.structuredContent, textContent);
280
+
281
+ if (sessionKey && normalized.threadId) {
282
+ this.setThreadId(sessionKey, normalized.threadId);
283
+ }
284
+
285
+ return {
286
+ output: normalized.content,
287
+ exitCode: rawResult.isError ? CODEX_MCP_EXECUTION_EXIT_CODE : 0,
288
+ threadId: normalized.threadId,
289
+ sessionKey,
290
+ raw: rawResult,
291
+ };
292
+ }
293
+ }
294
+
295
+ function parseCliArgs(argv) {
296
+ const options = {
297
+ command: process.env.CODEX_BIN || 'codex',
298
+ cwd: process.cwd(),
299
+ timeoutMs: DEFAULT_CODEX_MCP_TIMEOUT_MS,
300
+ };
301
+
302
+ for (let i = 0; i < argv.length; i += 1) {
303
+ const token = argv[i];
304
+ const next = () => {
305
+ const value = argv[i + 1];
306
+ if (value === undefined) {
307
+ throw new Error(`${token} 값이 필요합니다.`);
308
+ }
309
+ i += 1;
310
+ return value;
311
+ };
312
+
313
+ switch (token) {
314
+ case '--prompt':
315
+ options.prompt = next();
316
+ break;
317
+ case '--thread-id':
318
+ options.threadId = next();
319
+ break;
320
+ case '--session-key':
321
+ options.sessionKey = next();
322
+ break;
323
+ case '--cwd':
324
+ options.cwd = next();
325
+ break;
326
+ case '--profile':
327
+ options.profile = next();
328
+ break;
329
+ case '--model':
330
+ options.model = next();
331
+ break;
332
+ case '--approval-policy':
333
+ options.approvalPolicy = next();
334
+ break;
335
+ case '--sandbox':
336
+ options.sandbox = next();
337
+ break;
338
+ case '--base-instructions':
339
+ options.baseInstructions = next();
340
+ break;
341
+ case '--developer-instructions':
342
+ options.developerInstructions = next();
343
+ break;
344
+ case '--compact-prompt':
345
+ options.compactPrompt = next();
346
+ break;
347
+ case '--timeout-ms':
348
+ options.timeoutMs = Number.parseInt(next(), 10);
349
+ break;
350
+ case '--config-json':
351
+ options.config = JSON.parse(next());
352
+ break;
353
+ case '--codex-command':
354
+ options.command = next();
355
+ break;
356
+ case '--reset-session':
357
+ options.resetSession = true;
358
+ break;
359
+ default:
360
+ throw new Error(`알 수 없는 옵션: ${token}`);
361
+ }
362
+ }
363
+
364
+ if (typeof options.prompt !== 'string' || !options.prompt) {
365
+ throw new Error('--prompt는 필수입니다.');
366
+ }
367
+
368
+ return options;
369
+ }
370
+
371
+ export async function runCodexMcpCli(argv = process.argv.slice(2)) {
372
+ let options;
373
+ try {
374
+ options = parseCliArgs(argv);
375
+ } catch (error) {
376
+ console.error(`[codex-mcp] ${error instanceof Error ? error.message : String(error)}`);
377
+ process.exitCode = 64;
378
+ return;
379
+ }
380
+
381
+ const worker = new CodexMcpWorker({
382
+ command: options.command,
383
+ cwd: options.cwd,
384
+ });
385
+
386
+ try {
387
+ const result = await worker.execute(options.prompt, options);
388
+ if (result.output) {
389
+ process.stdout.write(result.output);
390
+ if (!result.output.endsWith('\n')) {
391
+ process.stdout.write('\n');
392
+ }
393
+ }
394
+ process.exitCode = result.exitCode;
395
+ } catch (error) {
396
+ const lines = [error instanceof Error ? error.message : String(error)];
397
+ if (error instanceof CodexMcpTransportError && error.stderr) {
398
+ lines.push(error.stderr);
399
+ }
400
+ console.error(`[codex-mcp] ${lines.join('\n')}`);
401
+ process.exitCode = CODEX_MCP_TRANSPORT_EXIT_CODE;
402
+ } finally {
403
+ await worker.stop();
404
+ }
405
+ }
406
+
407
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
408
+ await runCodexMcpCli();
409
+ process.exit(process.exitCode ?? 0);
410
+ }