@triflux/core 10.50.0 → 10.50.2
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/index.mjs +2 -8
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hud/cli-policy.mjs +1 -5
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +4 -135
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +43 -225
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +7 -698
- package/hud/renderers.mjs +40 -294
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +0 -24
- package/package.json +1 -2
- package/scripts/lib/gemini-profiles.mjs +1 -1
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/psmux-info.mjs +18 -49
- package/scripts/lib/skill-template.mjs +0 -26
- package/hub/codex-compat.mjs +0 -12
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
access,
|
|
3
|
-
mkdir,
|
|
4
|
-
open,
|
|
5
|
-
readdir,
|
|
6
|
-
readFile,
|
|
7
|
-
rename,
|
|
8
|
-
rm,
|
|
9
|
-
writeFile,
|
|
10
|
-
} from "node:fs/promises";
|
|
11
|
-
import { basename, join } from "node:path";
|
|
12
|
-
|
|
13
|
-
const DEFAULT_STATE_DIR = join(process.cwd(), ".tfx", "state");
|
|
14
|
-
const STOP_HOOKS = new Map();
|
|
15
|
-
|
|
16
|
-
function stateFilePath(stateDir, skillName) {
|
|
17
|
-
return join(stateDir, `${skillName}-active.json`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function assertValidSkillName(skillName) {
|
|
21
|
-
if (basename(skillName) !== skillName) {
|
|
22
|
-
throw new Error(`Invalid skill name: ${skillName}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function assertValidOnStop(onStop) {
|
|
27
|
-
if (onStop !== undefined && typeof onStop !== "function") {
|
|
28
|
-
throw new TypeError("onStop must be a function");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function readStateFile(filePath) {
|
|
33
|
-
try {
|
|
34
|
-
const raw = await readFile(filePath, "utf8");
|
|
35
|
-
return JSON.parse(raw);
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function rememberStopHook(filePath, onStop) {
|
|
42
|
-
if (typeof onStop === "function") {
|
|
43
|
-
STOP_HOOKS.set(filePath, onStop);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
STOP_HOOKS.delete(filePath);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function logStopHookWarning(message, error) {
|
|
50
|
-
if (error) {
|
|
51
|
-
console.warn(message, error);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
console.warn(message);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function runStopHook(filePath, state, onStop) {
|
|
58
|
-
if (!state?.hasStopHook) {
|
|
59
|
-
STOP_HOOKS.delete(filePath);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const hook = onStop ?? STOP_HOOKS.get(filePath);
|
|
64
|
-
STOP_HOOKS.delete(filePath);
|
|
65
|
-
|
|
66
|
-
if (typeof hook !== "function") {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
await hook({
|
|
72
|
-
skillName: state.skillName,
|
|
73
|
-
filePath,
|
|
74
|
-
state,
|
|
75
|
-
});
|
|
76
|
-
} catch (error) {
|
|
77
|
-
logStopHookWarning(
|
|
78
|
-
`Failed to run stop-hook for skill: ${state.skillName}`,
|
|
79
|
-
error,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Activate a skill by writing its state file.
|
|
86
|
-
* Throws if the skill is already active.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} skillName
|
|
89
|
-
* @param {{ stateDir?: string, onStop?: (() => Promise<void> | void) }} options
|
|
90
|
-
*/
|
|
91
|
-
export async function activateSkill(
|
|
92
|
-
skillName,
|
|
93
|
-
{ stateDir = DEFAULT_STATE_DIR, onStop } = {},
|
|
94
|
-
) {
|
|
95
|
-
assertValidSkillName(skillName);
|
|
96
|
-
assertValidOnStop(onStop);
|
|
97
|
-
|
|
98
|
-
await mkdir(stateDir, { recursive: true });
|
|
99
|
-
|
|
100
|
-
const filePath = stateFilePath(stateDir, skillName);
|
|
101
|
-
const lockPath = `${filePath}.lock`;
|
|
102
|
-
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
103
|
-
let lockHandle;
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
lockHandle = await open(lockPath, "wx");
|
|
107
|
-
try {
|
|
108
|
-
await access(filePath);
|
|
109
|
-
throw new Error(`Skill already active: ${skillName}`);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (error?.message === `Skill already active: ${skillName}`) {
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
114
|
-
if (error?.code !== "ENOENT") {
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const state = {
|
|
120
|
-
skillName,
|
|
121
|
-
pid: process.pid,
|
|
122
|
-
activatedAt: Date.now(),
|
|
123
|
-
hasStopHook: typeof onStop === "function",
|
|
124
|
-
};
|
|
125
|
-
await writeFile(tmpPath, JSON.stringify(state), "utf8");
|
|
126
|
-
await rename(tmpPath, filePath);
|
|
127
|
-
rememberStopHook(filePath, onStop);
|
|
128
|
-
} finally {
|
|
129
|
-
await rm(tmpPath, { force: true }).catch(() => {});
|
|
130
|
-
if (lockHandle) {
|
|
131
|
-
await lockHandle.close().catch(() => {});
|
|
132
|
-
}
|
|
133
|
-
await rm(lockPath, { force: true }).catch(() => {});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Deactivate a skill by removing its state file.
|
|
139
|
-
* Does not throw if the file does not exist.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} skillName
|
|
142
|
-
* @param {{ stateDir?: string, onStop?: (() => Promise<void> | void) }} options
|
|
143
|
-
*/
|
|
144
|
-
export async function deactivateSkill(
|
|
145
|
-
skillName,
|
|
146
|
-
{ stateDir = DEFAULT_STATE_DIR, onStop } = {},
|
|
147
|
-
) {
|
|
148
|
-
assertValidOnStop(onStop);
|
|
149
|
-
|
|
150
|
-
const filePath = stateFilePath(stateDir, skillName);
|
|
151
|
-
const state = await readStateFile(filePath);
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await runStopHook(filePath, state, onStop);
|
|
155
|
-
} finally {
|
|
156
|
-
STOP_HOOKS.delete(filePath);
|
|
157
|
-
await rm(filePath, { force: true }).catch(() => {});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Return all currently active skills by scanning *-active.json files.
|
|
163
|
-
*
|
|
164
|
-
* @param {{ stateDir?: string }} options
|
|
165
|
-
* @returns {Promise<Array<{ skillName: string, pid: number, activatedAt: number, hasStopHook?: boolean }>>}
|
|
166
|
-
*/
|
|
167
|
-
export async function getActiveSkills({ stateDir = DEFAULT_STATE_DIR } = {}) {
|
|
168
|
-
let entries;
|
|
169
|
-
try {
|
|
170
|
-
entries = await readdir(stateDir);
|
|
171
|
-
} catch {
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const results = [];
|
|
176
|
-
for (const entry of entries) {
|
|
177
|
-
if (!entry.endsWith("-active.json")) continue;
|
|
178
|
-
const state = await readStateFile(join(stateDir, entry));
|
|
179
|
-
if (state) {
|
|
180
|
-
results.push(state);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return results;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Remove state files for skills whose processes are no longer alive.
|
|
188
|
-
*
|
|
189
|
-
* @param {{ stateDir?: string }} options
|
|
190
|
-
* @returns {Promise<string[]>} list of pruned skill names
|
|
191
|
-
*/
|
|
192
|
-
export async function pruneOrphanSkillStates({
|
|
193
|
-
stateDir = DEFAULT_STATE_DIR,
|
|
194
|
-
} = {}) {
|
|
195
|
-
const active = await getActiveSkills({ stateDir });
|
|
196
|
-
const pruned = [];
|
|
197
|
-
|
|
198
|
-
for (const state of active) {
|
|
199
|
-
let alive = true;
|
|
200
|
-
try {
|
|
201
|
-
process.kill(state.pid, 0);
|
|
202
|
-
} catch {
|
|
203
|
-
alive = false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (!alive) {
|
|
207
|
-
const filePath = stateFilePath(stateDir, state.skillName);
|
|
208
|
-
STOP_HOOKS.delete(filePath);
|
|
209
|
-
if (state.hasStopHook) {
|
|
210
|
-
logStopHookWarning(
|
|
211
|
-
`Skipping stop-hook for orphaned skill state: ${state.skillName}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
await rm(filePath, { force: true }).catch(() => {});
|
|
215
|
-
pruned.push(state.skillName);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return pruned;
|
|
220
|
-
}
|
package/scripts/lib/team.mjs
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
function teamContext(env) {
|
|
5
|
-
const team = env.TFX_TEAM_NAME || "";
|
|
6
|
-
const taskId = env.TFX_TEAM_TASK_ID || "";
|
|
7
|
-
if (!team || !taskId) return null;
|
|
8
|
-
return {
|
|
9
|
-
team,
|
|
10
|
-
taskId,
|
|
11
|
-
agent: env.TFX_TEAM_AGENT_NAME || "agent",
|
|
12
|
-
lead: env.TFX_TEAM_LEAD_NAME || "team-lead",
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function claimTask({ env = process.env, bridge } = {}) {
|
|
17
|
-
const ctx = teamContext(env);
|
|
18
|
-
if (!ctx) return { skipped: true };
|
|
19
|
-
const response = await bridge.claim({
|
|
20
|
-
team: ctx.team,
|
|
21
|
-
taskId: ctx.taskId,
|
|
22
|
-
owner: ctx.agent,
|
|
23
|
-
status: "in_progress",
|
|
24
|
-
});
|
|
25
|
-
if (response?.ok) return { claimed: true };
|
|
26
|
-
const code = response?.error?.code;
|
|
27
|
-
const before = response?.error?.details?.task_before ?? {};
|
|
28
|
-
if (
|
|
29
|
-
code === "CLAIM_CONFLICT" &&
|
|
30
|
-
before.owner === ctx.agent &&
|
|
31
|
-
before.status === "in_progress"
|
|
32
|
-
) {
|
|
33
|
-
return { claimed: true, alreadyClaimed: true };
|
|
34
|
-
}
|
|
35
|
-
if (code === "CLAIM_CONFLICT") {
|
|
36
|
-
return { claimed: false, conflict: true, owner: before.owner ?? null };
|
|
37
|
-
}
|
|
38
|
-
return { claimed: false, error: response?.error ?? null };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function completeTask({
|
|
42
|
-
env = process.env,
|
|
43
|
-
bridge,
|
|
44
|
-
result = "success",
|
|
45
|
-
summary = "작업 완료",
|
|
46
|
-
now = () => new Date().toISOString(),
|
|
47
|
-
} = {}) {
|
|
48
|
-
const ctx = teamContext(env);
|
|
49
|
-
if (!ctx) return { skipped: true };
|
|
50
|
-
const trimmed = String(summary).slice(0, 4096);
|
|
51
|
-
const response = await bridge.complete({
|
|
52
|
-
team: ctx.team,
|
|
53
|
-
taskId: ctx.taskId,
|
|
54
|
-
agent: ctx.agent,
|
|
55
|
-
result,
|
|
56
|
-
summary: trimmed,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const resultDir =
|
|
60
|
-
env.TFX_RESULT_DIR ||
|
|
61
|
-
join(process.env.HOME || ".", ".claude", "tfx-results", ctx.team);
|
|
62
|
-
mkdirSync(resultDir, { recursive: true });
|
|
63
|
-
writeFileSync(
|
|
64
|
-
join(resultDir, `${ctx.taskId}.json`),
|
|
65
|
-
`${JSON.stringify({
|
|
66
|
-
taskId: ctx.taskId,
|
|
67
|
-
agent: ctx.agent,
|
|
68
|
-
team: ctx.team,
|
|
69
|
-
result,
|
|
70
|
-
summary: trimmed,
|
|
71
|
-
timestamp: now(),
|
|
72
|
-
})}\n`,
|
|
73
|
-
);
|
|
74
|
-
return { completed: Boolean(response?.ok), response };
|
|
75
|
-
}
|
package/scripts/lib/timeout.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export function timeoutCodeForSignal(signal) {
|
|
4
|
-
return signal ? 124 : null;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function runWithTimeout(command, args = [], options = {}) {
|
|
8
|
-
const {
|
|
9
|
-
cwd = process.cwd(),
|
|
10
|
-
env = process.env,
|
|
11
|
-
input,
|
|
12
|
-
timeoutMs = 0,
|
|
13
|
-
stdio = "pipe",
|
|
14
|
-
spawnFn = spawn,
|
|
15
|
-
} = options;
|
|
16
|
-
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const controller = new AbortController();
|
|
19
|
-
let timedOut = false;
|
|
20
|
-
const child = spawnFn(command, args, {
|
|
21
|
-
cwd,
|
|
22
|
-
env,
|
|
23
|
-
stdio,
|
|
24
|
-
signal: controller.signal,
|
|
25
|
-
});
|
|
26
|
-
const stdout = [];
|
|
27
|
-
const stderr = [];
|
|
28
|
-
let timer = null;
|
|
29
|
-
|
|
30
|
-
if (child.stdout) child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
31
|
-
if (child.stderr) child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
32
|
-
child.on("error", (err) => {
|
|
33
|
-
if (timedOut && err.name === "AbortError") return;
|
|
34
|
-
reject(err);
|
|
35
|
-
});
|
|
36
|
-
child.on("exit", (code, signal) => {
|
|
37
|
-
if (timer) clearTimeout(timer);
|
|
38
|
-
resolve({
|
|
39
|
-
code: timedOut ? 124 : (code ?? timeoutCodeForSignal(signal) ?? 1),
|
|
40
|
-
signal,
|
|
41
|
-
timedOut,
|
|
42
|
-
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
43
|
-
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (timeoutMs > 0) {
|
|
48
|
-
timer = setTimeout(() => {
|
|
49
|
-
timedOut = true;
|
|
50
|
-
controller.abort();
|
|
51
|
-
if (child.pid) {
|
|
52
|
-
try {
|
|
53
|
-
process.kill(child.pid, "SIGTERM");
|
|
54
|
-
} catch {
|
|
55
|
-
// Process already exited.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}, timeoutMs);
|
|
59
|
-
timer.unref?.();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (input !== undefined && child.stdin) {
|
|
63
|
-
child.stdin.end(input);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
package/scripts/lib/tmp.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export function resolveTmpDir({ env = process.env, cwd = process.cwd() } = {}) {
|
|
6
|
-
const candidate = env.TFX_TMP || tmpdir();
|
|
7
|
-
try {
|
|
8
|
-
mkdirSync(candidate, { recursive: true });
|
|
9
|
-
return candidate;
|
|
10
|
-
} catch {
|
|
11
|
-
const fallback = join(cwd, ".tfx-tmp");
|
|
12
|
-
try {
|
|
13
|
-
mkdirSync(fallback, { recursive: true });
|
|
14
|
-
} catch {
|
|
15
|
-
// Bash printed the fallback path even when mkdir failed.
|
|
16
|
-
}
|
|
17
|
-
return fallback;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveJobsDir({
|
|
22
|
-
env = process.env,
|
|
23
|
-
tmpDir = resolveTmpDir({ env }),
|
|
24
|
-
} = {}) {
|
|
25
|
-
return env.TFX_JOBS_DIR || join(tmpDir, "tfx-jobs");
|
|
26
|
-
}
|
package/scripts/lib/toml.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { sanitizeCodexProfileConfig } from "./codex-profile-config.mjs";
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
let TOML = null;
|
|
8
|
-
try {
|
|
9
|
-
TOML = require("@iarna/toml");
|
|
10
|
-
} catch {
|
|
11
|
-
TOML = null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function walk(value, visitor) {
|
|
15
|
-
if (!value || typeof value !== "object") return;
|
|
16
|
-
for (const [key, child] of Object.entries(value)) {
|
|
17
|
-
visitor(value, key, child);
|
|
18
|
-
walk(child, visitor);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function parseToml(source) {
|
|
23
|
-
if (!TOML) return parseTomlFallback(source);
|
|
24
|
-
return TOML.parse(source || "");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function stringifyToml(value) {
|
|
28
|
-
if (!TOML) return stringifyTomlFallback(value);
|
|
29
|
-
return TOML.stringify(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseTomlFallback(source = "") {
|
|
33
|
-
const root = {};
|
|
34
|
-
let current = root;
|
|
35
|
-
for (const rawLine of String(source).split(/\r?\n/)) {
|
|
36
|
-
const line = rawLine.trim();
|
|
37
|
-
if (!line || line.startsWith("#")) continue;
|
|
38
|
-
const section = line.match(/^\[([^\]]+)\]$/);
|
|
39
|
-
if (section) {
|
|
40
|
-
current = root;
|
|
41
|
-
for (const part of section[1].split(".")) {
|
|
42
|
-
current[part] ??= {};
|
|
43
|
-
current = current[part];
|
|
44
|
-
}
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"$/);
|
|
48
|
-
if (assignment) {
|
|
49
|
-
current[assignment[1]] = assignment[2];
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return root;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stringifyTomlFallback(value) {
|
|
56
|
-
const lines = [];
|
|
57
|
-
for (const [key, child] of Object.entries(value)) {
|
|
58
|
-
if (child && typeof child === "object") continue;
|
|
59
|
-
lines.push(`${key} = "${String(child)}"`);
|
|
60
|
-
}
|
|
61
|
-
function writeSections(prefix, obj) {
|
|
62
|
-
for (const [key, child] of Object.entries(obj)) {
|
|
63
|
-
if (!child || typeof child !== "object") continue;
|
|
64
|
-
const section = [...prefix, key];
|
|
65
|
-
lines.push("", `[${section.join(".")}]`);
|
|
66
|
-
for (const [childKey, childValue] of Object.entries(child)) {
|
|
67
|
-
if (childValue && typeof childValue === "object") continue;
|
|
68
|
-
lines.push(`${childKey} = "${String(childValue)}"`);
|
|
69
|
-
}
|
|
70
|
-
writeSections(section, child);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
writeSections([], value);
|
|
74
|
-
return `${lines.join("\n")}\n`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function detectTopLevelCodexConfig(source) {
|
|
78
|
-
const parsed = parseToml(source);
|
|
79
|
-
return {
|
|
80
|
-
hasTopLevelSandboxOrApproval:
|
|
81
|
-
Object.hasOwn(parsed, "sandbox") ||
|
|
82
|
-
Object.hasOwn(parsed, "approval_mode"),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function patchMcpApprovalMode(source) {
|
|
87
|
-
const parsed = parseToml(source);
|
|
88
|
-
let count = 0;
|
|
89
|
-
walk(parsed.mcp_servers, (parent, key, value) => {
|
|
90
|
-
if (key === "approval_mode" && value === "approve") {
|
|
91
|
-
parent[key] = "full-auto";
|
|
92
|
-
count += 1;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
return {
|
|
96
|
-
changed: count > 0,
|
|
97
|
-
count,
|
|
98
|
-
toml: count > 0 ? stringifyToml(parsed) : source,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function patchCodexConfigFile(path, { now = () => new Date() } = {}) {
|
|
103
|
-
if (!path || !existsSync(path)) return { changed: false, count: 0 };
|
|
104
|
-
const source = readFileSync(path, "utf8");
|
|
105
|
-
const approvalPatched = patchMcpApprovalMode(source);
|
|
106
|
-
const profileSanitized = sanitizeCodexProfileConfig(approvalPatched.toml, {
|
|
107
|
-
codexHome: dirname(path),
|
|
108
|
-
});
|
|
109
|
-
const changed = approvalPatched.changed || profileSanitized.changed;
|
|
110
|
-
if (!changed) {
|
|
111
|
-
return {
|
|
112
|
-
changed: false,
|
|
113
|
-
count: 0,
|
|
114
|
-
removedProfiles: [],
|
|
115
|
-
migratedProfiles: [],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
const stamp = now()
|
|
119
|
-
.toISOString()
|
|
120
|
-
.replace(/[-:]/g, "")
|
|
121
|
-
.replace(/\..*$/, "")
|
|
122
|
-
.replace("T", "-");
|
|
123
|
-
writeFileSync(`${path}.bak-${stamp}`, source);
|
|
124
|
-
writeFileSync(path, profileSanitized.toml);
|
|
125
|
-
return {
|
|
126
|
-
changed: true,
|
|
127
|
-
count: approvalPatched.count,
|
|
128
|
-
removedProfiles: profileSanitized.removedProfiles,
|
|
129
|
-
migratedProfiles: profileSanitized.migratedProfiles,
|
|
130
|
-
toml: profileSanitized.toml,
|
|
131
|
-
};
|
|
132
|
-
}
|