@triflux/core 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 (75) hide show
  1. package/hooks/agent-route-guard.mjs +109 -0
  2. package/hooks/cross-review-tracker.mjs +122 -0
  3. package/hooks/error-context.mjs +148 -0
  4. package/hooks/hook-manager.mjs +352 -0
  5. package/hooks/hook-orchestrator.mjs +312 -0
  6. package/hooks/hook-registry.json +213 -0
  7. package/hooks/hooks.json +89 -0
  8. package/hooks/keyword-rules.json +581 -0
  9. package/hooks/lib/resolve-root.mjs +59 -0
  10. package/hooks/mcp-config-watcher.mjs +85 -0
  11. package/hooks/pipeline-stop.mjs +76 -0
  12. package/hooks/safety-guard.mjs +106 -0
  13. package/hooks/subagent-verifier.mjs +80 -0
  14. package/hub/assign-callbacks.mjs +133 -0
  15. package/hub/bridge.mjs +799 -0
  16. package/hub/cli-adapter-base.mjs +192 -0
  17. package/hub/codex-adapter.mjs +190 -0
  18. package/hub/codex-compat.mjs +78 -0
  19. package/hub/codex-preflight.mjs +147 -0
  20. package/hub/delegator/contracts.mjs +37 -0
  21. package/hub/delegator/index.mjs +14 -0
  22. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  23. package/hub/delegator/service.mjs +307 -0
  24. package/hub/delegator/tool-definitions.mjs +35 -0
  25. package/hub/fullcycle.mjs +96 -0
  26. package/hub/gemini-adapter.mjs +179 -0
  27. package/hub/hitl.mjs +143 -0
  28. package/hub/intent.mjs +193 -0
  29. package/hub/lib/process-utils.mjs +361 -0
  30. package/hub/middleware/request-logger.mjs +81 -0
  31. package/hub/paths.mjs +30 -0
  32. package/hub/pipeline/gates/confidence.mjs +56 -0
  33. package/hub/pipeline/gates/consensus.mjs +94 -0
  34. package/hub/pipeline/gates/index.mjs +5 -0
  35. package/hub/pipeline/gates/selfcheck.mjs +82 -0
  36. package/hub/pipeline/index.mjs +318 -0
  37. package/hub/pipeline/state.mjs +191 -0
  38. package/hub/pipeline/transitions.mjs +124 -0
  39. package/hub/platform.mjs +225 -0
  40. package/hub/quality/deslop.mjs +253 -0
  41. package/hub/reflexion.mjs +372 -0
  42. package/hub/research.mjs +146 -0
  43. package/hub/router.mjs +791 -0
  44. package/hub/routing/complexity.mjs +166 -0
  45. package/hub/routing/index.mjs +117 -0
  46. package/hub/routing/q-learning.mjs +336 -0
  47. package/hub/session-fingerprint.mjs +352 -0
  48. package/hub/state.mjs +245 -0
  49. package/hub/team-bridge.mjs +25 -0
  50. package/hub/token-mode.mjs +224 -0
  51. package/hub/workers/worker-utils.mjs +104 -0
  52. package/hud/colors.mjs +88 -0
  53. package/hud/constants.mjs +81 -0
  54. package/hud/hud-qos-status.mjs +206 -0
  55. package/hud/providers/claude.mjs +309 -0
  56. package/hud/providers/codex.mjs +151 -0
  57. package/hud/providers/gemini.mjs +320 -0
  58. package/hud/renderers.mjs +424 -0
  59. package/hud/terminal.mjs +140 -0
  60. package/hud/utils.mjs +287 -0
  61. package/package.json +31 -0
  62. package/scripts/lib/claudemd-manager.mjs +325 -0
  63. package/scripts/lib/context.mjs +67 -0
  64. package/scripts/lib/cross-review-utils.mjs +51 -0
  65. package/scripts/lib/env-probe.mjs +241 -0
  66. package/scripts/lib/gemini-profiles.mjs +85 -0
  67. package/scripts/lib/hook-utils.mjs +14 -0
  68. package/scripts/lib/keyword-rules.mjs +166 -0
  69. package/scripts/lib/logger.mjs +105 -0
  70. package/scripts/lib/mcp-filter.mjs +739 -0
  71. package/scripts/lib/mcp-guard-engine.mjs +940 -0
  72. package/scripts/lib/mcp-manifest.mjs +79 -0
  73. package/scripts/lib/mcp-server-catalog.mjs +118 -0
  74. package/scripts/lib/psmux-info.mjs +119 -0
  75. package/scripts/lib/remote-spawn-transfer.mjs +196 -0
@@ -0,0 +1,192 @@
1
+ // hub/cli-adapter-base.mjs — codex/gemini 공통 CLI adapter 인터페이스
2
+ // Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+
7
+ import { killProcess, IS_WINDOWS } from './platform.mjs';
8
+
9
+ // ── Shell utilities ─────────────────────────────────────────────
10
+
11
+ export function normalizePathForShell(value) {
12
+ return IS_WINDOWS ? String(value).replace(/\\/g, '/') : String(value);
13
+ }
14
+
15
+ export function shellQuote(value) {
16
+ return JSON.stringify(String(value));
17
+ }
18
+
19
+ // ── Sleep ───────────────────────────────────────────────────────
20
+
21
+ export function sleep(ms) {
22
+ return new Promise((resolve) => {
23
+ const timer = setTimeout(resolve, ms);
24
+ timer.unref?.();
25
+ });
26
+ }
27
+
28
+ // ── Result factory ──────────────────────────────────────────────
29
+
30
+ export function createResult(ok, extra = {}) {
31
+ return {
32
+ ok,
33
+ output: '',
34
+ stderr: '',
35
+ exitCode: null,
36
+ duration: 0,
37
+ retried: false,
38
+ fellBack: false,
39
+ failureMode: ok ? null : 'crash',
40
+ ...extra,
41
+ };
42
+ }
43
+
44
+ export function appendWarnings(stderr, warnings = []) {
45
+ const text = warnings.map((item) => `[preflight] ${item}`).join('\n');
46
+ return [stderr, text].filter(Boolean).join('\n');
47
+ }
48
+
49
+ // ── Circuit breaker factory ─────────────────────────────────────
50
+
51
+ export function createCircuitBreaker(opts = {}) {
52
+ const state = {
53
+ failures: [],
54
+ maxFailures: opts.maxFailures ?? 3,
55
+ windowMs: opts.windowMs ?? 10 * 60_000,
56
+ openedAt: 0,
57
+ trialInFlight: false,
58
+ };
59
+
60
+ function pruneFailures(now = Date.now()) {
61
+ state.failures = state.failures.filter((stamp) => now - stamp < state.windowMs);
62
+ }
63
+
64
+ function reset() {
65
+ state.failures = [];
66
+ state.openedAt = 0;
67
+ state.trialInFlight = false;
68
+ }
69
+
70
+ function recordFailure(isHalfOpen, now = Date.now()) {
71
+ pruneFailures(now);
72
+ state.failures = [...state.failures, now];
73
+ state.trialInFlight = false;
74
+ if (isHalfOpen || state.failures.length >= state.maxFailures) {
75
+ state.openedAt = now;
76
+ }
77
+ }
78
+
79
+ function getState(now = Date.now()) {
80
+ pruneFailures(now);
81
+ const withinWindow = state.openedAt && now - state.openedAt < state.windowMs;
82
+ const current = withinWindow ? 'open' : (state.openedAt ? 'half-open' : 'closed');
83
+ return {
84
+ state: current,
85
+ failures: [...state.failures],
86
+ maxFailures: state.maxFailures,
87
+ windowMs: state.windowMs,
88
+ openedAt: state.openedAt || null,
89
+ trialInFlight: state.trialInFlight,
90
+ };
91
+ }
92
+
93
+ function canExecute() {
94
+ const circuit = getState();
95
+ if (circuit.state === 'open') return { allowed: false, halfOpen: false };
96
+ if (circuit.state === 'half-open' && state.trialInFlight) return { allowed: false, halfOpen: true };
97
+ const halfOpen = circuit.state === 'half-open';
98
+ if (halfOpen) state.trialInFlight = true;
99
+ return { allowed: true, halfOpen };
100
+ }
101
+
102
+ function clearTrial() {
103
+ state.trialInFlight = false;
104
+ }
105
+
106
+ return { getState, recordFailure, reset, canExecute, clearTrial };
107
+ }
108
+
109
+ // ── Process termination ─────────────────────────────────────────
110
+
111
+ export async function terminateChild(pid, opts = {}) {
112
+ if (!pid) return;
113
+ const graceMs = opts.graceMs ?? 5000;
114
+ killProcess(pid, { signal: 'SIGTERM', tree: true, timeout: graceMs });
115
+ await sleep(graceMs);
116
+ killProcess(pid, { signal: 'SIGKILL', tree: true, force: true, timeout: graceMs });
117
+ }
118
+
119
+ // ── Process execution with stall detection ──────────────────────
120
+
121
+ /**
122
+ * Spawn a CLI process with timeout + stall detection.
123
+ *
124
+ * @param {string} command — shell command to run
125
+ * @param {string} workdir — cwd for the child process
126
+ * @param {number} timeout — max duration in ms
127
+ * @param {object} [opts]
128
+ * @param {string} [opts.resultFile] — file to read output from (if CLI writes there)
129
+ * @param {function} [opts.inferStallMode] — (stdout, stderr) => string. Default: () => 'timeout'
130
+ * @param {number} [opts.stallCheckIntervalMs] — stall check interval (default 10_000)
131
+ * @param {number} [opts.stallThresholdMs] — stall threshold (default 30_000)
132
+ * @returns {Promise<object>} createResult-shaped object
133
+ */
134
+ export async function runProcess(command, workdir, timeout, opts = {}) {
135
+ const startedAt = Date.now();
136
+ const inferStallMode = opts.inferStallMode || (() => 'timeout');
137
+ const stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 10_000;
138
+ const stallThresholdMs = opts.stallThresholdMs ?? 30_000;
139
+ const resultFile = opts.resultFile || null;
140
+
141
+ let stdout = '';
142
+ let stderr = '';
143
+ let exitCode = null;
144
+ let failureMode = null;
145
+ let child;
146
+
147
+ try {
148
+ child = spawn(command, { cwd: workdir, shell: true, windowsHide: true });
149
+ } catch (error) {
150
+ return createResult(false, { stderr: String(error?.message || error), duration: Date.now() - startedAt });
151
+ }
152
+
153
+ let lastBytes = 0;
154
+ let lastChange = Date.now();
155
+ const touch = () => { lastChange = Date.now(); };
156
+ child.stdout?.on('data', (chunk) => { stdout += String(chunk); touch(); });
157
+ child.stderr?.on('data', (chunk) => { stderr += String(chunk); touch(); });
158
+ child.on('error', (error) => { stderr += String(error?.message || error); failureMode ||= 'crash'; });
159
+
160
+ const stopFor = async (mode) => {
161
+ if (failureMode) return;
162
+ failureMode = mode;
163
+ await terminateChild(child.pid);
164
+ };
165
+
166
+ const timeoutTimer = setTimeout(() => { void stopFor('timeout'); }, timeout);
167
+ const stallTimer = setInterval(() => {
168
+ const size = Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
169
+ if (size !== lastBytes) {
170
+ lastBytes = size;
171
+ return;
172
+ }
173
+ if (Date.now() - lastChange >= stallThresholdMs) void stopFor(inferStallMode(stdout, stderr));
174
+ }, stallCheckIntervalMs);
175
+ timeoutTimer.unref?.();
176
+ stallTimer.unref?.();
177
+
178
+ await new Promise((resolve) => child.on('close', (code) => { exitCode = code; resolve(); }));
179
+ clearTimeout(timeoutTimer);
180
+ clearInterval(stallTimer);
181
+
182
+ const fileOutput = resultFile && existsSync(resultFile) ? readFileSync(resultFile, 'utf8') : '';
183
+ const output = fileOutput || stdout;
184
+ const ok = failureMode == null && exitCode === 0;
185
+ return createResult(ok, {
186
+ output,
187
+ stderr,
188
+ exitCode,
189
+ duration: Date.now() - startedAt,
190
+ failureMode: ok ? null : (failureMode || 'crash'),
191
+ });
192
+ }
@@ -0,0 +1,190 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+
5
+ import { runPreflight } from './codex-preflight.mjs';
6
+ import { withRetry } from './workers/worker-utils.mjs';
7
+ import { buildExecCommand } from './codex-compat.mjs';
8
+ import {
9
+ createCircuitBreaker,
10
+ createResult,
11
+ appendWarnings,
12
+ normalizePathForShell,
13
+ shellQuote,
14
+ runProcess,
15
+ } from './cli-adapter-base.mjs';
16
+
17
+ const breaker = createCircuitBreaker();
18
+
19
+ // ── Codex-specific stall inference ──────────────────────────────
20
+
21
+ function inferStallMode(stdout, stderr) {
22
+ const text = `${stdout}\n${stderr}`.toLowerCase();
23
+ if (/(approval|approve|permission|sandbox|bypass)/u.test(text)) return 'approval_stall';
24
+ if (/\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)) return 'mcp_stall';
25
+ return 'timeout';
26
+ }
27
+
28
+ // ── Codex command building ──────────────────────────────────────
29
+
30
+ function commandWithOverrides(command, prompt, codexPath, overrides = []) {
31
+ let next = codexPath ? command.replace(/^codex\b/u, shellQuote(codexPath)) : command;
32
+ if (!overrides.length) return next;
33
+ const promptArg = JSON.stringify(prompt);
34
+ const flags = overrides.flatMap((value) => ['-c', shellQuote(value)]).join(' ');
35
+ return next.endsWith(promptArg)
36
+ ? `${next.slice(0, -promptArg.length)}${flags} ${promptArg}`
37
+ : `${next} ${flags}`;
38
+ }
39
+
40
+ function buildOverrides(requested, excluded) {
41
+ return [...new Set((requested || []).filter((name) => (excluded || []).includes(name)))]
42
+ .map((name) => `mcp_servers.${name}.enabled=false`);
43
+ }
44
+
45
+ function buildAttempts(opts, preflight) {
46
+ const timeout = Number.isFinite(opts.timeout) ? opts.timeout : 300_000;
47
+ const requested = Array.isArray(opts.mcpServers) ? [...opts.mcpServers] : [];
48
+ const base = {
49
+ timeout,
50
+ profile: opts.profile,
51
+ requested,
52
+ excluded: [...(preflight.excludeMcpServers || [])],
53
+ forceBypass: preflight.needsBypass,
54
+ };
55
+ if (opts.retryOnFail === false) return [base];
56
+ return [
57
+ base,
58
+ { ...base, timeout: timeout * 2, excluded: requested, forceBypass: true },
59
+ ];
60
+ }
61
+
62
+ // ── Launch script ───────────────────────────────────────────────
63
+
64
+ function createLaunchScriptText(opts) {
65
+ const parts = ['codex'];
66
+ if (opts.profile) parts.push('--profile', shellQuote(opts.profile));
67
+ parts.push(
68
+ 'exec',
69
+ '--dangerously-bypass-approvals-and-sandbox',
70
+ '--skip-git-repo-check',
71
+ '$(cat "$PROMPT_FILE")',
72
+ );
73
+ return [
74
+ '#!/usr/bin/env bash',
75
+ 'set -euo pipefail',
76
+ `cd ${shellQuote(normalizePathForShell(opts.workdir))}`,
77
+ `PROMPT_FILE=${shellQuote(normalizePathForShell(opts.promptFile))}`,
78
+ `TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ?? ''))}`,
79
+ parts.join(' '),
80
+ '',
81
+ ].join('\n');
82
+ }
83
+
84
+ export function buildLaunchScript(opts = {}) {
85
+ const dir = join(tmpdir(), 'triflux-codex-launch');
86
+ mkdirSync(dir, { recursive: true });
87
+ const path = join(dir, `${String(opts.id || 'launch')}.sh`);
88
+ writeFileSync(path, createLaunchScriptText(opts), 'utf8');
89
+ return path;
90
+ }
91
+
92
+ // ── Exec args builder ───────────────────────────────────────────
93
+
94
+ export function buildExecArgs(opts = {}) {
95
+ const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
96
+ const command = buildExecCommand(prompt, opts.resultFile || null, {
97
+ profile: opts.profile,
98
+ skipGitRepoCheck: true,
99
+ sandboxBypass: true,
100
+ });
101
+
102
+ if (!prompt) return command.replace(/\s+""$/u, '');
103
+
104
+ const quotedPrompt = JSON.stringify(prompt);
105
+ if (/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) && command.endsWith(quotedPrompt)) {
106
+ return `${command.slice(0, -quotedPrompt.length)}${prompt}`;
107
+ }
108
+
109
+ return command;
110
+ }
111
+
112
+ // ── Codex execution ─────────────────────────────────────────────
113
+
114
+ async function runCodex(prompt, workdir, preflight, attempt) {
115
+ const dir = join(tmpdir(), 'triflux-codex-exec');
116
+ mkdirSync(dir, { recursive: true });
117
+ const resultFile = join(dir, `codex-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
118
+ const command = commandWithOverrides(
119
+ buildExecCommand(prompt, resultFile, {
120
+ profile: attempt.profile,
121
+ skipGitRepoCheck: true,
122
+ sandboxBypass: attempt.forceBypass,
123
+ }),
124
+ prompt,
125
+ preflight.codexPath,
126
+ buildOverrides(attempt.requested, attempt.excluded),
127
+ );
128
+ return runProcess(command, workdir, attempt.timeout, { resultFile, inferStallMode });
129
+ }
130
+
131
+ // ── Public API ──────────────────────────────────────────────────
132
+
133
+ export function getCircuitState(now) {
134
+ return breaker.getState(now);
135
+ }
136
+
137
+ export async function execute(opts = {}) {
138
+ const entry = breaker.canExecute();
139
+ if (!entry.allowed) {
140
+ return createResult(false, { fellBack: true, failureMode: 'circuit_open' });
141
+ }
142
+
143
+ const preflight = await runPreflight({ mcpServers: opts.mcpServers, subcommand: 'exec' });
144
+ if (!preflight.ok) {
145
+ breaker.clearTrial();
146
+ breaker.recordFailure(entry.halfOpen);
147
+ return createResult(false, {
148
+ stderr: appendWarnings('', preflight.warnings),
149
+ fellBack: opts.fallbackToClaude !== false,
150
+ failureMode: 'crash',
151
+ });
152
+ }
153
+
154
+ const attempts = buildAttempts(opts, preflight);
155
+ let attemptIndex = 0;
156
+ let lastResult = createResult(false);
157
+
158
+ try {
159
+ lastResult = await withRetry(async () => {
160
+ const result = await runCodex(opts.prompt || '', opts.workdir || process.cwd(), preflight, attempts[attemptIndex]);
161
+ const current = { ...result, stderr: appendWarnings(result.stderr, preflight.warnings), retried: attemptIndex > 0 };
162
+ const canRetry = !current.ok && attemptIndex < attempts.length - 1;
163
+ attemptIndex += 1;
164
+ if (!canRetry) return current;
165
+ const error = new Error('retry');
166
+ error.retryable = true;
167
+ error.result = current;
168
+ throw error;
169
+ }, {
170
+ maxAttempts: attempts.length,
171
+ baseDelayMs: 250,
172
+ maxDelayMs: 750,
173
+ shouldRetry: (error) => error?.retryable === true,
174
+ });
175
+ } catch (error) {
176
+ lastResult = error?.result || createResult(false, { stderr: String(error?.message || error) });
177
+ }
178
+
179
+ if (lastResult.ok) {
180
+ breaker.reset();
181
+ return lastResult;
182
+ }
183
+
184
+ breaker.recordFailure(entry.halfOpen);
185
+ return {
186
+ ...lastResult,
187
+ retried: attempts.length > 1,
188
+ fellBack: opts.fallbackToClaude !== false,
189
+ };
190
+ }
@@ -0,0 +1,78 @@
1
+ // hub/team/codex-compat.mjs — Codex CLI 버전 어댑터
2
+ // Codex 0.117.0+ (Rust 리라이트): exec 서브커맨드 기반
3
+ import { execSync } from "node:child_process";
4
+
5
+ let _cachedVersion = null;
6
+
7
+ /**
8
+ * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
9
+ * 파싱 실패 시 0 반환 (구버전으로 간주).
10
+ * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
11
+ */
12
+ export function getCodexVersion() {
13
+ if (_cachedVersion !== null) return _cachedVersion;
14
+ try {
15
+ const out = execSync("codex --version", { encoding: "utf8", timeout: 5000 }).trim();
16
+ // "codex 0.117.0" 또는 "0.117.0" 형식 대응
17
+ const m = out.match(/(\d+)\.(\d+)\.(\d+)/);
18
+ _cachedVersion = m ? parseInt(m[2], 10) : 0;
19
+ } catch {
20
+ _cachedVersion = 0;
21
+ }
22
+ return _cachedVersion;
23
+ }
24
+
25
+ /**
26
+ * 최소 마이너 버전 이상인지 확인.
27
+ * @param {number} minMinor
28
+ * @returns {boolean}
29
+ */
30
+ export function gte(minMinor) {
31
+ return getCodexVersion() >= minMinor;
32
+ }
33
+
34
+ /**
35
+ * Codex CLI 기능별 분기 객체.
36
+ * 117 = 0.117.0 (Rust 리라이트, exec 서브커맨드 도입)
37
+ */
38
+ export const FEATURES = {
39
+ /** exec 서브커맨드 사용 가능 여부 */
40
+ get execSubcommand() { return gte(117); },
41
+ /** --output-last-message 플래그 지원 여부 */
42
+ get outputLastMessage() { return gte(117); },
43
+ /** --color never 플래그 지원 여부 */
44
+ get colorNever() { return gte(117); },
45
+ /** 플러그인 시스템 지원 여부 (향후 확장용) */
46
+ get pluginSystem() { return gte(120); },
47
+ };
48
+
49
+ /**
50
+ * long-form 플래그 기반 명령 빌더.
51
+ * @param {string} prompt
52
+ * @param {string|null} resultFile — null이면 --output-last-message 생략
53
+ * @param {{ profile?: string, skipGitRepoCheck?: boolean, sandboxBypass?: boolean }} [opts]
54
+ * @returns {string} 실행할 셸 커맨드
55
+ */
56
+ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
57
+ const { profile, skipGitRepoCheck = true, sandboxBypass = true } = opts;
58
+
59
+ const parts = ["codex"];
60
+ if (profile) parts.push("--profile", profile);
61
+
62
+ if (FEATURES.execSubcommand) {
63
+ parts.push("exec");
64
+ if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
65
+ if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
66
+ if (resultFile && FEATURES.outputLastMessage) {
67
+ parts.push("--output-last-message", resultFile);
68
+ }
69
+ if (FEATURES.colorNever) parts.push("--color", "never");
70
+ } else {
71
+ // 구버전 fallback
72
+ parts.push("--dangerously-bypass-approvals-and-sandbox");
73
+ if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
74
+ }
75
+
76
+ parts.push(JSON.stringify(prompt));
77
+ return parts.join(" ");
78
+ }
@@ -0,0 +1,147 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ import { whichCommandAsync } from './platform.mjs';
6
+ import { getCodexVersion } from './codex-compat.mjs';
7
+
8
+ const MIN_RECOMMENDED_MINOR = 118;
9
+
10
+ function escapeRegExp(value) {
11
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+ }
13
+
14
+ function readConfigText(configPath = join(homedir(), '.codex', 'config.toml')) {
15
+ if (!existsSync(configPath)) return '';
16
+ try {
17
+ return readFileSync(configPath, 'utf8');
18
+ } catch {
19
+ return '';
20
+ }
21
+ }
22
+
23
+ function readTomlString(text, key) {
24
+ const match = String(text).match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, 'mu'));
25
+ return match?.[1] ?? null;
26
+ }
27
+
28
+ function readSection(text, name) {
29
+ const lines = String(text).split(/\r?\n/u);
30
+ const header = `[${name}]`;
31
+ const start = lines.findIndex((line) => line.trim() === header);
32
+ if (start < 0) return '';
33
+ const body = [];
34
+ for (const line of lines.slice(start + 1)) {
35
+ if (/^\s*\[[^\]]+\]\s*$/u.test(line)) break;
36
+ body.push(line);
37
+ }
38
+ return body.join('\n');
39
+ }
40
+
41
+ async function checkCodexInstalled() {
42
+ const codexPath = await whichCommandAsync('codex');
43
+ if (codexPath) return { codexPath, ok: true, warnings: [] };
44
+ return {
45
+ codexPath: null,
46
+ ok: false,
47
+ warnings: ['Codex CLI not found. Install Codex and ensure `codex` is available on PATH.'],
48
+ };
49
+ }
50
+
51
+ function checkCodexVersion() {
52
+ const version = getCodexVersion();
53
+ const warnings = version >= MIN_RECOMMENDED_MINOR
54
+ ? []
55
+ : [`Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`];
56
+ return { version, warnings };
57
+ }
58
+
59
+ function checkApprovalMode(configText, opts = {}) {
60
+ const approvalMode = readTomlString(configText, 'approval_mode');
61
+ const sandbox = readTomlString(configText, 'sandbox');
62
+ const subcommand = opts.subcommand || 'exec';
63
+ return {
64
+ needsBypass: subcommand === 'exec' || approvalMode !== 'full-auto',
65
+ approvalMode,
66
+ sandbox,
67
+ };
68
+ }
69
+
70
+ async function verifyServerHealth(name, configText) {
71
+ const section = readSection(configText, `mcp_servers.${name}`);
72
+ if (!section) return { ok: false, warning: `MCP server '${name}' is not configured.` };
73
+ if (/^\s*enabled\s*=\s*false\s*$/mu.test(section)) {
74
+ return { ok: false, warning: `MCP server '${name}' is disabled in config.toml.` };
75
+ }
76
+
77
+ const command = readTomlString(section, 'command');
78
+ if (command) {
79
+ const resolved = await whichCommandAsync(command);
80
+ return resolved
81
+ ? { ok: true, warning: '' }
82
+ : { ok: false, warning: `MCP server '${name}' command not found: ${command}` };
83
+ }
84
+
85
+ const url = readTomlString(section, 'url');
86
+ if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: '' };
87
+ try {
88
+ const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
89
+ return response.status < 500
90
+ ? { ok: true, warning: '' }
91
+ : { ok: false, warning: `MCP server '${name}' returned HTTP ${response.status}.` };
92
+ } catch {
93
+ return { ok: false, warning: `MCP server '${name}' is unreachable at ${url}.` };
94
+ }
95
+ }
96
+
97
+ async function checkMcpHealth(mcpServers, configText) {
98
+ const excludeMcpServers = [];
99
+ const warnings = [];
100
+
101
+ for (const name of Array.isArray(mcpServers) ? mcpServers : []) {
102
+ const server = String(name ?? '').trim();
103
+ if (!server) continue;
104
+ const result = await verifyServerHealth(server, configText);
105
+ if (!result.ok) excludeMcpServers.push(server);
106
+ if (result.warning) warnings.push(result.warning);
107
+ }
108
+
109
+ return { excludeMcpServers, warnings };
110
+ }
111
+
112
+ export async function runPreflight(opts = {}) {
113
+ const install = await checkCodexInstalled();
114
+ if (!install.ok) {
115
+ return {
116
+ codexPath: null,
117
+ version: 0,
118
+ needsBypass: true,
119
+ excludeMcpServers: [],
120
+ warnings: install.warnings,
121
+ ok: false,
122
+ };
123
+ }
124
+
125
+ const warnings = [...install.warnings];
126
+ const { version, warnings: versionWarnings } = checkCodexVersion();
127
+ warnings.push(...versionWarnings);
128
+
129
+ const configText = readConfigText(opts.configPath);
130
+ const approval = checkApprovalMode(configText, opts);
131
+ if (approval.approvalMode !== 'full-auto') {
132
+ warnings.push(`approval_mode is '${approval.approvalMode || 'unset'}'; bypass flag will be used.`);
133
+ }
134
+ if (approval.sandbox) warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
135
+
136
+ const mcp = await checkMcpHealth(opts.mcpServers, configText);
137
+ warnings.push(...mcp.warnings);
138
+
139
+ return {
140
+ codexPath: install.codexPath,
141
+ version,
142
+ needsBypass: approval.needsBypass,
143
+ excludeMcpServers: mcp.excludeMcpServers,
144
+ warnings,
145
+ ok: true,
146
+ };
147
+ }
@@ -0,0 +1,37 @@
1
+ export const DELEGATOR_MCP_SERVER_INFO = Object.freeze({
2
+ name: 'triflux-delegator',
3
+ version: '0.1.0',
4
+ });
5
+
6
+ export const DELEGATOR_TOOL_NAMES = Object.freeze({
7
+ delegate: 'delegate',
8
+ delegateReply: 'delegate-reply',
9
+ status: 'status',
10
+ });
11
+
12
+ export const DELEGATOR_PIPE_ACTIONS = Object.freeze({
13
+ delegate: 'delegator_delegate',
14
+ delegateReply: 'delegator_reply',
15
+ status: 'delegator_status',
16
+ });
17
+
18
+ export const DELEGATOR_JOB_STATUSES = Object.freeze([
19
+ 'queued',
20
+ 'running',
21
+ 'waiting_reply',
22
+ 'completed',
23
+ 'failed',
24
+ ]);
25
+
26
+ export const DELEGATOR_MODES = Object.freeze([
27
+ 'sync',
28
+ 'async',
29
+ ]);
30
+
31
+ export const DELEGATOR_PROVIDERS = Object.freeze([
32
+ 'auto',
33
+ 'codex',
34
+ 'gemini',
35
+ ]);
36
+
37
+ export const DELEGATOR_SCHEMA_URL = new URL('./schema/delegator-tools.schema.json', import.meta.url);
@@ -0,0 +1,14 @@
1
+ export {
2
+ DELEGATOR_JOB_STATUSES,
3
+ DELEGATOR_MCP_SERVER_INFO,
4
+ DELEGATOR_MODES,
5
+ DELEGATOR_PIPE_ACTIONS,
6
+ DELEGATOR_PROVIDERS,
7
+ DELEGATOR_SCHEMA_URL,
8
+ DELEGATOR_TOOL_NAMES,
9
+ } from './contracts.mjs';
10
+ export { DelegatorService } from './service.mjs';
11
+ export {
12
+ getDelegatorMcpToolDefinitions,
13
+ loadDelegatorSchemaBundle,
14
+ } from './tool-definitions.mjs';