@triflux/core 10.36.0 → 10.38.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.
- package/hooks/agy-session-hook.mjs +89 -29
- package/hooks/codex-session-hook.mjs +1 -1
- package/hooks/hooks.json +12 -12
- package/hooks/lib/resolve-root.mjs +3 -0
- package/hooks/session-start-fast.mjs +108 -6
- package/hooks/subagent-tracker.mjs +16 -2
- package/hub/routing/q-learning.mjs +5 -2
- package/hub/team/claude-daemon-control.mjs +42 -9
- package/hud/hud-qos-status.mjs +16 -5
- package/hud/providers/gemini.mjs +116 -31
- package/hud/renderers.mjs +2 -2
- package/package.json +1 -1
- package/scripts/lib/cli-agy.mjs +2 -0
- package/scripts/lib/codex-profile-config.mjs +142 -0
- package/scripts/lib/mcp-guard-engine.mjs +61 -1
- package/scripts/lib/toml.mjs +23 -4
|
@@ -30,12 +30,18 @@ function parsePayload(stdinData) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Convert an Antigravity hook payload into the Codex-shaped session payload
|
|
35
|
+
* consumed by the shared fast session registration helpers.
|
|
36
|
+
*
|
|
37
|
+
* Antigravity hook payloads use camelCase system metadata (`conversationId`,
|
|
38
|
+
* `workspacePaths`) rather than Codex's `session_id`/`cwd`. The
|
|
39
|
+
* `conversationId` is the stable per-conversation UUID; the first mounted
|
|
40
|
+
* workspace path is the effective cwd.
|
|
41
|
+
*
|
|
42
|
+
* @param {Record<string, unknown> | null | undefined} payload
|
|
43
|
+
* @returns {string} JSON string with `{ session_id, cwd, actor_cli }`.
|
|
44
|
+
*/
|
|
39
45
|
export function toSessionPayload(payload) {
|
|
40
46
|
const sessionId = String(payload?.conversationId || "").trim();
|
|
41
47
|
const workspacePaths = Array.isArray(payload?.workspacePaths)
|
|
@@ -45,13 +51,21 @@ export function toSessionPayload(payload) {
|
|
|
45
51
|
typeof workspacePaths[0] === "string" && workspacePaths[0]
|
|
46
52
|
? workspacePaths[0]
|
|
47
53
|
: process.cwd();
|
|
48
|
-
return JSON.stringify({ session_id: sessionId, cwd });
|
|
54
|
+
return JSON.stringify({ session_id: sessionId, cwd, actor_cli: "agy" });
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Decide whether a PreInvocation payload should register or heartbeat.
|
|
59
|
+
*
|
|
60
|
+
* agy has no distinct SessionStart/UserPromptSubmit events. PreInvocation fires
|
|
61
|
+
* before every model call, so per-conversation `invocationNum` gates register
|
|
62
|
+
* (first call) vs heartbeat (later calls). An explicit argv mode overrides this,
|
|
63
|
+
* mirroring the Codex hook.
|
|
64
|
+
*
|
|
65
|
+
* @param {string | null | undefined} argvMode
|
|
66
|
+
* @param {Record<string, unknown> | null | undefined} payload
|
|
67
|
+
* @returns {"register" | "heartbeat"}
|
|
68
|
+
*/
|
|
55
69
|
export function normalizeMode(argvMode, payload) {
|
|
56
70
|
const direct = String(argvMode || "")
|
|
57
71
|
.trim()
|
|
@@ -67,6 +81,50 @@ export function normalizeMode(argvMode, payload) {
|
|
|
67
81
|
return "register";
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
function swallowStdoutWrite(_chunk, encodingOrCallback, callback) {
|
|
85
|
+
const done =
|
|
86
|
+
typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
87
|
+
if (typeof done === "function") done();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runHookSideEffectsWithStdoutSuppressed(fn) {
|
|
92
|
+
const originalStdoutWrite = stdout.write;
|
|
93
|
+
const originalConsoleDebug = console.debug;
|
|
94
|
+
const originalConsoleInfo = console.info;
|
|
95
|
+
const originalConsoleLog = console.log;
|
|
96
|
+
|
|
97
|
+
stdout.write = swallowStdoutWrite;
|
|
98
|
+
console.debug = () => {};
|
|
99
|
+
console.info = () => {};
|
|
100
|
+
console.log = () => {};
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
} finally {
|
|
104
|
+
stdout.write = originalStdoutWrite;
|
|
105
|
+
console.debug = originalConsoleDebug;
|
|
106
|
+
console.info = originalConsoleInfo;
|
|
107
|
+
console.log = originalConsoleLog;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Execute the observational Antigravity session hook.
|
|
113
|
+
*
|
|
114
|
+
* The hook always returns/writes an empty JSON object so hook stdout remains
|
|
115
|
+
* JSON-only and hook failures never block the user session.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} stdinData
|
|
118
|
+
* @param {{
|
|
119
|
+
* argvMode?: string,
|
|
120
|
+
* writeStdout?: boolean,
|
|
121
|
+
* hubEnsureRun?: (stdinData: string) => Promise<unknown> | unknown,
|
|
122
|
+
* registerInteractiveSession?: (stdinData: string) => Promise<unknown> | unknown,
|
|
123
|
+
* heartbeatInteractiveSession?: (stdinData: string) => Promise<unknown> | unknown,
|
|
124
|
+
* drainPendingSynapse?: (timeoutMs?: number) => Promise<unknown> | unknown,
|
|
125
|
+
* }} [opts]
|
|
126
|
+
* @returns {Promise<string>}
|
|
127
|
+
*/
|
|
70
128
|
export async function runAgySessionHook(stdinData, opts = {}) {
|
|
71
129
|
const output = "{}\n";
|
|
72
130
|
const parsed = parsePayload(stdinData);
|
|
@@ -93,24 +151,26 @@ export async function runAgySessionHook(stdinData, opts = {}) {
|
|
|
93
151
|
opts.drainPendingSynapse || defaultDrainPendingSynapse;
|
|
94
152
|
|
|
95
153
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
155
|
+
if (mode === "register") {
|
|
156
|
+
try {
|
|
157
|
+
await hubEnsureRun(sessionPayload);
|
|
158
|
+
} catch {}
|
|
159
|
+
try {
|
|
160
|
+
await Promise.resolve(registerInteractiveSession(sessionPayload));
|
|
161
|
+
} catch {}
|
|
162
|
+
try {
|
|
163
|
+
await drainPendingSynapse(1000);
|
|
164
|
+
} catch {}
|
|
165
|
+
} else if (mode === "heartbeat") {
|
|
166
|
+
try {
|
|
167
|
+
heartbeatInteractiveSession(sessionPayload);
|
|
168
|
+
} catch {}
|
|
169
|
+
try {
|
|
170
|
+
await drainPendingSynapse(500);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
114
174
|
} catch {
|
|
115
175
|
// agy session hooks are observational and must never block the session.
|
|
116
176
|
}
|
|
@@ -99,7 +99,7 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
99
99
|
await hubEnsureRun(stdinData);
|
|
100
100
|
} catch {}
|
|
101
101
|
try {
|
|
102
|
-
registerInteractiveSession(stdinData);
|
|
102
|
+
await Promise.resolve(registerInteractiveSession(stdinData));
|
|
103
103
|
} catch {}
|
|
104
104
|
try {
|
|
105
105
|
await drainPendingSynapse(1000);
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "node \"
|
|
10
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
11
11
|
"timeout": 15
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"hooks": [
|
|
20
20
|
{
|
|
21
21
|
"type": "command",
|
|
22
|
-
"command": "node \"
|
|
22
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
23
23
|
"timeout": 5
|
|
24
24
|
}
|
|
25
25
|
]
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"hooks": [
|
|
32
32
|
{
|
|
33
33
|
"type": "command",
|
|
34
|
-
"command": "node \"
|
|
34
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
35
35
|
"timeout": 10
|
|
36
36
|
}
|
|
37
37
|
]
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"hooks": [
|
|
44
44
|
{
|
|
45
45
|
"type": "command",
|
|
46
|
-
"command": "node \"
|
|
46
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
47
47
|
"timeout": 3
|
|
48
48
|
}
|
|
49
49
|
]
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"hooks": [
|
|
56
56
|
{
|
|
57
57
|
"type": "command",
|
|
58
|
-
"command": "node \"
|
|
58
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
59
59
|
"timeout": 10
|
|
60
60
|
}
|
|
61
61
|
]
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"hooks": [
|
|
68
68
|
{
|
|
69
69
|
"type": "command",
|
|
70
|
-
"command": "node \"
|
|
70
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
71
71
|
"timeout": 5
|
|
72
72
|
}
|
|
73
73
|
]
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"hooks": [
|
|
80
80
|
{
|
|
81
81
|
"type": "command",
|
|
82
|
-
"command": "node \"
|
|
82
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
83
83
|
"timeout": 5
|
|
84
84
|
}
|
|
85
85
|
]
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"hooks": [
|
|
92
92
|
{
|
|
93
93
|
"type": "command",
|
|
94
|
-
"command": "node \"
|
|
94
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
95
95
|
"timeout": 35
|
|
96
96
|
}
|
|
97
97
|
]
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"hooks": [
|
|
104
104
|
{
|
|
105
105
|
"type": "command",
|
|
106
|
-
"command": "node \"
|
|
106
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
107
107
|
"timeout": 5
|
|
108
108
|
}
|
|
109
109
|
]
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hooks": [
|
|
116
116
|
{
|
|
117
117
|
"type": "command",
|
|
118
|
-
"command": "node \"
|
|
118
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
119
119
|
"timeout": 5
|
|
120
120
|
}
|
|
121
121
|
]
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"hooks": [
|
|
128
128
|
{
|
|
129
129
|
"type": "command",
|
|
130
|
-
"command": "node \"
|
|
130
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
131
131
|
"timeout": 5
|
|
132
132
|
}
|
|
133
133
|
]
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"hooks": [
|
|
140
140
|
{
|
|
141
141
|
"type": "command",
|
|
142
|
-
"command": "node \"
|
|
142
|
+
"command": "node -e \"const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process');const roots=[];try{roots.push(fs.readFileSync(path.join(os.homedir(),'.claude','scripts','.tfx-pkg-root'),'utf8').trim())}catch{}roots.push(process.env.PLUGIN_ROOT,process.env.CLAUDE_PLUGIN_ROOT);try{roots.push(cp.execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim())}catch{}roots.push(process.cwd());const root=roots.find(r=>r&&fs.existsSync(path.join(r,'hooks','hook-orchestrator.mjs')));if(!root){console.error('[triflux hook] cannot locate hooks/hook-orchestrator.mjs');process.exit(1)}const child=cp.spawnSync(process.execPath,[path.join(root,'hooks','hook-orchestrator.mjs')],{stdio:'inherit',env:process.env,windowsHide:true});process.exit(child.status??(child.signal?1:0));\"",
|
|
143
143
|
"timeout": 5
|
|
144
144
|
}
|
|
145
145
|
]
|
|
@@ -43,6 +43,9 @@ export function resolvePluginRoot(callerUrl) {
|
|
|
43
43
|
const breadcrumbRoot = readBreadcrumbRoot();
|
|
44
44
|
if (isValidPluginRoot(breadcrumbRoot)) return breadcrumbRoot;
|
|
45
45
|
|
|
46
|
+
const pluginRoot = normalizeCandidate(process.env.PLUGIN_ROOT);
|
|
47
|
+
if (isValidPluginRoot(pluginRoot)) return pluginRoot;
|
|
48
|
+
|
|
46
49
|
const envRoot = normalizeCandidate(process.env.CLAUDE_PLUGIN_ROOT);
|
|
47
50
|
if (isValidPluginRoot(envRoot)) return envRoot;
|
|
48
51
|
|
|
@@ -40,6 +40,98 @@ function parseStartPayload(stdinData) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function inferParticipantCli(payload) {
|
|
44
|
+
const direct = String(
|
|
45
|
+
payload?.actor_cli || payload?.cli || payload?.actor?.cli || "",
|
|
46
|
+
)
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase();
|
|
49
|
+
if (direct) return direct;
|
|
50
|
+
|
|
51
|
+
const eventName = String(payload?.hook_event_name || "").trim();
|
|
52
|
+
if (eventName === "SessionStart") return "claude";
|
|
53
|
+
if (eventName.toLowerCase().replace(/-/g, "_") === "session_start") {
|
|
54
|
+
return "codex";
|
|
55
|
+
}
|
|
56
|
+
return "participant";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function defaultAppendParticipantCtoEvent(lakeRoot, event) {
|
|
60
|
+
const { appendCtoEvent } = await import("../cto/events.mjs");
|
|
61
|
+
return appendCtoEvent(lakeRoot, event, {
|
|
62
|
+
lockRetries: 1,
|
|
63
|
+
lockRetryDelayMs: 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function defaultResolveParticipantLakeRoot(cwd) {
|
|
68
|
+
const { resolveLakeRootDir } = await import("../cto/lake-root.mjs");
|
|
69
|
+
const projectRoot = resolveLakeRootDir(cwd, {
|
|
70
|
+
execFileSync: (cmd, args, options = {}) =>
|
|
71
|
+
execFileSync(cmd, args, {
|
|
72
|
+
...options,
|
|
73
|
+
timeout: 500,
|
|
74
|
+
killSignal: "SIGKILL",
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
projectRoot,
|
|
79
|
+
lakeRoot: projectRoot ? join(projectRoot, ".triflux", "lake") : "",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Append a compact CTO `session_started` event for participant hooks.
|
|
85
|
+
* This is intentionally observational only: no cleanup, reconciliation, or
|
|
86
|
+
* summarization policy is performed here. Callers may fire-and-forget it.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} stdinData SessionStart-shaped stdin JSON
|
|
89
|
+
* @param {object} [seams] test seams
|
|
90
|
+
* @returns {Promise<object|null>}
|
|
91
|
+
*/
|
|
92
|
+
export async function emitParticipantSessionStarted(stdinData, seams = {}) {
|
|
93
|
+
try {
|
|
94
|
+
const payload = parseStartPayload(stdinData);
|
|
95
|
+
const sessionId = String(payload?.session_id || "").trim();
|
|
96
|
+
if (!sessionId) return null;
|
|
97
|
+
const cwd = typeof payload?.cwd === "string" ? payload.cwd : process.cwd();
|
|
98
|
+
const resolveLakeRoot =
|
|
99
|
+
seams.resolveLakeRoot || defaultResolveParticipantLakeRoot;
|
|
100
|
+
const resolved = await resolveLakeRoot(cwd);
|
|
101
|
+
const projectRoot =
|
|
102
|
+
typeof resolved === "string"
|
|
103
|
+
? resolved
|
|
104
|
+
: String(resolved?.projectRoot || "");
|
|
105
|
+
const lakeRoot =
|
|
106
|
+
typeof resolved === "string"
|
|
107
|
+
? join(resolved, ".triflux", "lake")
|
|
108
|
+
: String(resolved?.lakeRoot || "");
|
|
109
|
+
if (!lakeRoot) return null;
|
|
110
|
+
|
|
111
|
+
const event = {
|
|
112
|
+
event: "session_started",
|
|
113
|
+
source: "tfx_participant_hook",
|
|
114
|
+
session_id: sessionId,
|
|
115
|
+
project_root: projectRoot || cwd,
|
|
116
|
+
worktree_path: cwd,
|
|
117
|
+
branch: typeof payload?.branch === "string" ? payload.branch : "",
|
|
118
|
+
status: "active",
|
|
119
|
+
actor: {
|
|
120
|
+
cli: inferParticipantCli(payload),
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
host: typeof payload?.host === "string" ? payload.host : "local",
|
|
123
|
+
},
|
|
124
|
+
summary: `${inferParticipantCli(payload)} session_started ${sessionId}`,
|
|
125
|
+
now: seams.now,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const append = seams.ctoAppend || defaultAppendParticipantCtoEvent;
|
|
129
|
+
return await append(lakeRoot, event);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
43
135
|
function readAncestorCommands(pid = process.ppid, maxDepth = 6) {
|
|
44
136
|
if (process.platform === "win32") return [];
|
|
45
137
|
const commands = [];
|
|
@@ -146,17 +238,18 @@ function defaultGitRunner(cwd, args, cb) {
|
|
|
146
238
|
* @param {Function} [seams.register] registerSynapseSession 대체
|
|
147
239
|
* @param {Function} [seams.heartbeat] heartbeatSynapseSession 대체
|
|
148
240
|
* @param {Function} [seams.gitRunner] git runner 대체
|
|
149
|
-
* @returns {
|
|
241
|
+
* @returns {Promise<object|null>|null} CTO append promise; enrich 는 백그라운드에서 완료
|
|
150
242
|
*/
|
|
151
243
|
export function registerInteractiveSession(stdinData, seams = {}) {
|
|
244
|
+
let ctoEvent = null;
|
|
152
245
|
const register = seams.register || registerSynapseSession;
|
|
153
246
|
const heartbeat = seams.heartbeat || heartbeatSynapseSession;
|
|
154
247
|
const gitRunner = seams.gitRunner || defaultGitRunner;
|
|
155
248
|
try {
|
|
156
249
|
const payload = parseStartPayload(stdinData);
|
|
157
250
|
const sessionId = String(payload?.session_id || "").trim();
|
|
158
|
-
if (!sessionId) return;
|
|
159
|
-
if (shouldSkipInteractiveRegistration(payload, seams)) return;
|
|
251
|
+
if (!sessionId) return null;
|
|
252
|
+
if (shouldSkipInteractiveRegistration(payload, seams)) return null;
|
|
160
253
|
const cwd = typeof payload?.cwd === "string" ? payload.cwd : process.cwd();
|
|
161
254
|
|
|
162
255
|
// 1) cwd 만으로 즉시 minimal register (블로킹 git 없음, latency 0).
|
|
@@ -170,6 +263,10 @@ export function registerInteractiveSession(stdinData, seams = {}) {
|
|
|
170
263
|
isRemote: false,
|
|
171
264
|
});
|
|
172
265
|
|
|
266
|
+
ctoEvent = emitParticipantSessionStarted(stdinData, seams).catch(
|
|
267
|
+
() => null,
|
|
268
|
+
);
|
|
269
|
+
|
|
173
270
|
// 2) worktree/branch 는 블로킹 경로 밖에서 비동기 enrich → heartbeat partial.
|
|
174
271
|
gitContextAsync(cwd, gitRunner)
|
|
175
272
|
.then(({ worktreePath, branch }) => {
|
|
@@ -180,6 +277,7 @@ export function registerInteractiveSession(stdinData, seams = {}) {
|
|
|
180
277
|
} catch {
|
|
181
278
|
/* best-effort — never affects session start */
|
|
182
279
|
}
|
|
280
|
+
return ctoEvent;
|
|
183
281
|
}
|
|
184
282
|
|
|
185
283
|
/**
|
|
@@ -371,9 +469,10 @@ function runBackground(stdinData) {
|
|
|
371
469
|
.catch(() => {}); // 완전 무시
|
|
372
470
|
|
|
373
471
|
// Synapse: 인터랙티브 세션 self-register (fire-and-forget, hub 미응답 무시)
|
|
374
|
-
registerInteractiveSession(stdinData);
|
|
472
|
+
const ctoEvent = registerInteractiveSession(stdinData);
|
|
375
473
|
|
|
376
474
|
// session-vault은 external source — hook-orchestrator가 execFile로 실행
|
|
475
|
+
return { ctoEvent };
|
|
377
476
|
}
|
|
378
477
|
|
|
379
478
|
/**
|
|
@@ -392,7 +491,7 @@ export async function execute(stdinData, externalHooks = []) {
|
|
|
392
491
|
|
|
393
492
|
// DEFERRED + BACKGROUND: fire-and-forget
|
|
394
493
|
runDeferred(stdinData);
|
|
395
|
-
runBackground(stdinData);
|
|
494
|
+
const background = runBackground(stdinData);
|
|
396
495
|
|
|
397
496
|
// hook-orchestrator process.exit(0)s immediately after we return, which would
|
|
398
497
|
// drop the just-fired Synapse register POST before it flushes to the loopback
|
|
@@ -401,7 +500,10 @@ export async function execute(stdinData, externalHooks = []) {
|
|
|
401
500
|
// and the ceiling caps a hub stall so SessionStart never hangs. The async
|
|
402
501
|
// git-enrich heartbeat stays best-effort (it may not have fired yet) — losing
|
|
403
502
|
// it only drops worktree/branch enrichment, not the core registration.
|
|
404
|
-
await
|
|
503
|
+
await Promise.allSettled([
|
|
504
|
+
drainPendingSynapse(1000),
|
|
505
|
+
background?.ctoEvent || Promise.resolve(null),
|
|
506
|
+
]);
|
|
405
507
|
|
|
406
508
|
const totalDur = performance.now() - totalStart;
|
|
407
509
|
log.info(
|
|
@@ -16,7 +16,11 @@ import { dirname, join } from "node:path";
|
|
|
16
16
|
const STATE_VERSION = 1;
|
|
17
17
|
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
|
|
18
18
|
const MAX_COMPLETED = 20;
|
|
19
|
-
|
|
19
|
+
// Lifecycle hooks are best-effort, but under CI/Linux process contention a
|
|
20
|
+
// 30-way SubagentStart burst can legitimately take around a second to drain.
|
|
21
|
+
// Keep this bounded so hooks do not hang indefinitely, while avoiding silent
|
|
22
|
+
// lifecycle drops during normal high-concurrency fan-out.
|
|
23
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 3000;
|
|
20
24
|
const LOCK_RETRY_MS = 20;
|
|
21
25
|
|
|
22
26
|
function readStdin() {
|
|
@@ -96,6 +100,16 @@ function withStateLock(statePath, fn, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
function lockTimeoutMsFromOptions(opts = {}) {
|
|
104
|
+
if (Number.isFinite(opts.lockTimeoutMs)) return opts.lockTimeoutMs;
|
|
105
|
+
const raw = opts.env?.TRIFLUX_SUBAGENT_LOCK_TIMEOUT_MS;
|
|
106
|
+
if (raw === undefined || raw === "") return DEFAULT_LOCK_TIMEOUT_MS;
|
|
107
|
+
const parsed = Number(raw);
|
|
108
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
109
|
+
? Math.trunc(parsed)
|
|
110
|
+
: DEFAULT_LOCK_TIMEOUT_MS;
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
function lifecycleKey(input) {
|
|
100
114
|
if (typeof input.agent_id === "string" && input.agent_id.trim()) {
|
|
101
115
|
return input.agent_id;
|
|
@@ -241,7 +255,7 @@ export function recordLifecycle(input, opts = {}) {
|
|
|
241
255
|
writeState(statePath, state);
|
|
242
256
|
return output;
|
|
243
257
|
},
|
|
244
|
-
opts
|
|
258
|
+
lockTimeoutMsFromOptions(opts),
|
|
245
259
|
);
|
|
246
260
|
}
|
|
247
261
|
|
|
@@ -164,6 +164,7 @@ export class QLearningRouter {
|
|
|
164
164
|
* @param {number} [opts.epsilonMin=0.05] — 최소 엡실론
|
|
165
165
|
* @param {number} [opts.minConfidence=0.6] — 최소 신뢰도 (이하면 폴백)
|
|
166
166
|
* @param {string} [opts.modelPath] — Q-table 영속화 경로
|
|
167
|
+
* @param {() => number} [opts.random=Math.random] — 테스트/재현용 RNG seam
|
|
167
168
|
*/
|
|
168
169
|
constructor(opts = {}) {
|
|
169
170
|
this._lr = opts.learningRate ?? 0.1;
|
|
@@ -176,6 +177,8 @@ export class QLearningRouter {
|
|
|
176
177
|
this._minConfidence = opts.minConfidence ?? 0.6;
|
|
177
178
|
this._modelPath =
|
|
178
179
|
opts.modelPath ?? join(homedir(), ".omc", "routing-model.json");
|
|
180
|
+
this._random =
|
|
181
|
+
typeof opts.random === "function" ? opts.random : Math.random;
|
|
179
182
|
|
|
180
183
|
/** @type {Map<string, Map<string, number>>} state -> (action -> Q-value) */
|
|
181
184
|
this._qTable = new Map();
|
|
@@ -223,12 +226,12 @@ export class QLearningRouter {
|
|
|
223
226
|
const visits = this._visitCounts.get(state) || 0;
|
|
224
227
|
|
|
225
228
|
// 엡실론-그리디: 탐색 vs 활용
|
|
226
|
-
const isExploration =
|
|
229
|
+
const isExploration = this._random() < this._epsilon;
|
|
227
230
|
|
|
228
231
|
let action;
|
|
229
232
|
if (isExploration) {
|
|
230
233
|
// 무작위 탐색
|
|
231
|
-
action = ACTIONS[Math.floor(
|
|
234
|
+
action = ACTIONS[Math.floor(this._random() * ACTIONS.length)];
|
|
232
235
|
} else {
|
|
233
236
|
// 최적 액션 선택 (최대 Q-value)
|
|
234
237
|
let maxQ = -Infinity;
|
|
@@ -243,6 +243,7 @@ function candidateResultSummary(result) {
|
|
|
243
243
|
...summary,
|
|
244
244
|
ok: result.ok === true,
|
|
245
245
|
error: result.error ?? null,
|
|
246
|
+
errorCode: result.errorCode ?? null,
|
|
246
247
|
sessionCount: Array.isArray(result.sessions) ? result.sessions.length : 0,
|
|
247
248
|
};
|
|
248
249
|
}
|
|
@@ -270,6 +271,14 @@ function buildProbeResponse({
|
|
|
270
271
|
reason = "target-not-found";
|
|
271
272
|
error = "Claude daemon target not found in reachable candidates";
|
|
272
273
|
} else if (results.length > 0) {
|
|
274
|
+
const errorCodes = results
|
|
275
|
+
.map((result) => result.errorCode)
|
|
276
|
+
.filter(Boolean);
|
|
277
|
+
if (errorCodes.includes("stale-control-socket")) {
|
|
278
|
+
reason = "stale-control-socket";
|
|
279
|
+
} else if (errorCodes.includes("daemon-dir-missing")) {
|
|
280
|
+
reason = "daemon-dir-missing";
|
|
281
|
+
}
|
|
273
282
|
error =
|
|
274
283
|
results
|
|
275
284
|
.map((result) => result.error)
|
|
@@ -341,6 +350,18 @@ export async function probeClaudeDaemonCandidates({
|
|
|
341
350
|
error: ok ? null : list?.error || "Claude daemon list failed",
|
|
342
351
|
});
|
|
343
352
|
} catch (error) {
|
|
353
|
+
let errorCode = null;
|
|
354
|
+
if (error?.code === "ENOENT") {
|
|
355
|
+
try {
|
|
356
|
+
await fs.stat(candidate.daemonDir);
|
|
357
|
+
errorCode = "stale-control-socket";
|
|
358
|
+
} catch (statError) {
|
|
359
|
+
errorCode =
|
|
360
|
+
statError?.code === "ENOENT"
|
|
361
|
+
? "daemon-dir-missing"
|
|
362
|
+
: "stale-control-socket";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
344
365
|
results.push({
|
|
345
366
|
ok: false,
|
|
346
367
|
candidate,
|
|
@@ -348,6 +369,7 @@ export async function probeClaudeDaemonCandidates({
|
|
|
348
369
|
sessions: [],
|
|
349
370
|
target: null,
|
|
350
371
|
error: errorMessage(error),
|
|
372
|
+
errorCode,
|
|
351
373
|
});
|
|
352
374
|
}
|
|
353
375
|
}
|
|
@@ -446,20 +468,29 @@ export function sendClaudeControlRequest(
|
|
|
446
468
|
// 인증 미강제 daemon 으로 보고 auth 필드를 생략한다 (fail-open — 구버전 호환).
|
|
447
469
|
export async function readDaemonControlKey(
|
|
448
470
|
configDir = resolveClaudeConfigDir(),
|
|
471
|
+
{ diagnostics } = {},
|
|
449
472
|
) {
|
|
473
|
+
if (!configDir) return undefined;
|
|
474
|
+
const keyPath = path.join(configDir, "daemon", "control.key");
|
|
450
475
|
try {
|
|
451
|
-
const key = await fs.readFile(
|
|
452
|
-
path.join(configDir, "daemon", "control.key"),
|
|
453
|
-
"utf8",
|
|
454
|
-
);
|
|
476
|
+
const key = await fs.readFile(keyPath, "utf8");
|
|
455
477
|
return key.trim() || undefined;
|
|
456
|
-
} catch {
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error?.code === "ENOENT") return undefined;
|
|
480
|
+
if (Array.isArray(diagnostics)) {
|
|
481
|
+
diagnostics.push({
|
|
482
|
+
code: error?.code || "UNKNOWN",
|
|
483
|
+
path: keyPath,
|
|
484
|
+
message: error?.message || String(error),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
457
487
|
return undefined;
|
|
458
488
|
}
|
|
459
489
|
}
|
|
460
490
|
|
|
461
|
-
export async function buildDaemonControlAuth(configDir) {
|
|
462
|
-
|
|
491
|
+
export async function buildDaemonControlAuth(configDir, opts = {}) {
|
|
492
|
+
if (!configDir) return {};
|
|
493
|
+
const auth = await readDaemonControlKey(configDir, opts);
|
|
463
494
|
return auth ? { auth } : {};
|
|
464
495
|
}
|
|
465
496
|
|
|
@@ -1223,7 +1254,7 @@ export function attachClaudeDaemonSession({
|
|
|
1223
1254
|
cancelCompletionTimer();
|
|
1224
1255
|
}
|
|
1225
1256
|
});
|
|
1226
|
-
|
|
1257
|
+
const finishClosed = () => {
|
|
1227
1258
|
if (!settled) {
|
|
1228
1259
|
if (pendingCompletion) {
|
|
1229
1260
|
finish(pendingCompletion);
|
|
@@ -1231,7 +1262,9 @@ export function attachClaudeDaemonSession({
|
|
|
1231
1262
|
}
|
|
1232
1263
|
finish({ timedOut: false, matchedCompletion: false, closed: true });
|
|
1233
1264
|
}
|
|
1234
|
-
}
|
|
1265
|
+
};
|
|
1266
|
+
socket.on("end", finishClosed);
|
|
1267
|
+
socket.on("close", finishClosed);
|
|
1235
1268
|
});
|
|
1236
1269
|
}
|
|
1237
1270
|
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -177,8 +177,10 @@ async function main() {
|
|
|
177
177
|
// 세션/누적 토큰 → context 대비 절약 배수 (개별 provider sv%)
|
|
178
178
|
const ctxCapacity = deriveContextLimit(stdin);
|
|
179
179
|
let codexSv = null;
|
|
180
|
+
let codexAccumulatorSv = null;
|
|
180
181
|
if (svAccumulator?.codex?.tokens > 0) {
|
|
181
|
-
|
|
182
|
+
codexAccumulatorSv = svAccumulator.codex.tokens / ctxCapacity;
|
|
183
|
+
codexSv = codexAccumulatorSv;
|
|
182
184
|
} else if (codexBuckets) {
|
|
183
185
|
const main =
|
|
184
186
|
codexBuckets.codex || codexBuckets[Object.keys(codexBuckets)[0]];
|
|
@@ -186,8 +188,10 @@ async function main() {
|
|
|
186
188
|
codexSv = main.tokens.total_tokens / ctxCapacity;
|
|
187
189
|
}
|
|
188
190
|
let geminiSv = null;
|
|
191
|
+
let geminiAccumulatorSv = null;
|
|
189
192
|
if (svAccumulator?.gemini?.tokens > 0) {
|
|
190
|
-
|
|
193
|
+
geminiAccumulatorSv = svAccumulator.gemini.tokens / ctxCapacity;
|
|
194
|
+
geminiSv = geminiAccumulatorSv;
|
|
191
195
|
} else {
|
|
192
196
|
const geminiTokens = geminiSession?.total || null;
|
|
193
197
|
geminiSv = geminiTokens ? geminiTokens / ctxCapacity : null;
|
|
@@ -216,8 +220,15 @@ async function main() {
|
|
|
216
220
|
const antigravitySlot1Bucket =
|
|
217
221
|
antigravityModelFamily === "gemini" ? antigravityFamilyBucket : null;
|
|
218
222
|
|
|
219
|
-
// 합산
|
|
220
|
-
|
|
223
|
+
// 합산 절약은 svAccumulator 기반일 때만 표시한다.
|
|
224
|
+
// Codex bucket fallback은 account-window 누적 토큰이고 Gemini fallback은 latest
|
|
225
|
+
// session 토큰이라 서로 의미가 달라 합산하면 misleading cross-provider total이 된다.
|
|
226
|
+
const hasComparableSvAccumulator = Boolean(
|
|
227
|
+
codexAccumulatorSv != null || geminiAccumulatorSv != null,
|
|
228
|
+
);
|
|
229
|
+
const combinedSvPct = hasComparableSvAccumulator
|
|
230
|
+
? Math.round(((codexAccumulatorSv ?? 0) + (geminiAccumulatorSv ?? 0)) * 100)
|
|
231
|
+
: null;
|
|
221
232
|
|
|
222
233
|
// 인디케이터 인식 tier 선택 (stdin + Claude 사용량 기반)
|
|
223
234
|
const CURRENT_TIER = selectTier(stdin, claudeUsageSnapshot.data);
|
|
@@ -230,7 +241,7 @@ async function main() {
|
|
|
230
241
|
codexBuckets,
|
|
231
242
|
antigravityReady ? null : geminiSession,
|
|
232
243
|
antigravityReady ? null : geminiBucket,
|
|
233
|
-
|
|
244
|
+
combinedSvPct,
|
|
234
245
|
antigravityReady ? "a" : "g",
|
|
235
246
|
);
|
|
236
247
|
process.stdout.write(`\x1b[0m${microLine}\n`);
|
package/hud/providers/gemini.mjs
CHANGED
|
@@ -197,6 +197,8 @@ export function deriveGeminiFamilyBucket(buckets) {
|
|
|
197
197
|
|
|
198
198
|
export function buildGeminiAuthContext(accountId) {
|
|
199
199
|
let oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
200
|
+
let authSource = oauth?.access_token ? "gemini-file" : "none";
|
|
201
|
+
let expiryMissing = oauth?.access_token ? oauth.expiry_date == null : false;
|
|
200
202
|
const fileExpired =
|
|
201
203
|
oauth?.expiry_date != null && oauth.expiry_date < Date.now();
|
|
202
204
|
// Preserve a valid Gemini file token; agy Keychain is only a missing/expired fallback.
|
|
@@ -209,13 +211,15 @@ export function buildGeminiAuthContext(accountId) {
|
|
|
209
211
|
...fileRest
|
|
210
212
|
} = oauth || {};
|
|
211
213
|
oauth = { ...fileRest, ...keychainOAuth };
|
|
214
|
+
authSource = "antigravity-keychain";
|
|
215
|
+
expiryMissing = keychainOAuth.expiry_date == null;
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
const tokenSource =
|
|
215
219
|
oauth?.refresh_token || oauth?.id_token || oauth?.access_token || "";
|
|
216
220
|
const tokenFingerprint = tokenSource ? makeHash(tokenSource) : "none";
|
|
217
221
|
const cacheKey = `${accountId || "gemini-main"}::${tokenFingerprint}`;
|
|
218
|
-
return { oauth, tokenFingerprint, cacheKey };
|
|
222
|
+
return { oauth, tokenFingerprint, cacheKey, authSource, expiryMissing };
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
function firstPresent(...values) {
|
|
@@ -305,12 +309,76 @@ function getAntigravityTokenFromKeychain() {
|
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
export function classifyGeminiQuotaFailure(response, authContext = {}) {
|
|
313
|
+
if (!response) return "network";
|
|
314
|
+
const error = response?.error || {};
|
|
315
|
+
const code = Number(error.code ?? response.code ?? response.status);
|
|
316
|
+
const status = String(error.status || response.status || "").toUpperCase();
|
|
317
|
+
const message = String(
|
|
318
|
+
error.message || error.code || response.error || response.message || "",
|
|
319
|
+
);
|
|
320
|
+
if (
|
|
321
|
+
code === 401 ||
|
|
322
|
+
code === 403 ||
|
|
323
|
+
status === "UNAUTHENTICATED" ||
|
|
324
|
+
status === "PERMISSION_DENIED" ||
|
|
325
|
+
/(unauthorized|forbidden|invalid authentication|invalid credentials|oauth|token|credential|auth)/i.test(
|
|
326
|
+
message,
|
|
327
|
+
)
|
|
328
|
+
) {
|
|
329
|
+
return "auth";
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
authContext.authSource === "antigravity-keychain" &&
|
|
333
|
+
authContext.expiryMissing === true &&
|
|
334
|
+
/(expired|invalid|unauthenticated|permission denied)/i.test(message)
|
|
335
|
+
) {
|
|
336
|
+
return "auth";
|
|
337
|
+
}
|
|
338
|
+
return "api";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function formatGeminiQuotaFailure(response, authContext = {}, stage = "quota") {
|
|
342
|
+
const error =
|
|
343
|
+
response?.error?.message ||
|
|
344
|
+
response?.error?.status ||
|
|
345
|
+
response?.error?.code ||
|
|
346
|
+
response?.error ||
|
|
347
|
+
response?.message ||
|
|
348
|
+
"no buckets in response";
|
|
349
|
+
const base = String(error);
|
|
350
|
+
if (
|
|
351
|
+
authContext.authSource === "antigravity-keychain" &&
|
|
352
|
+
authContext.expiryMissing === true
|
|
353
|
+
) {
|
|
354
|
+
return `expiry-less Antigravity Keychain token failed bounded ${stage} freshness probe: ${base}`;
|
|
355
|
+
}
|
|
356
|
+
return base;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function writeGeminiQuotaErrorCache(cache, authContext, errorType, errorHint) {
|
|
360
|
+
const sameKey = cache?.cacheKey === authContext.cacheKey;
|
|
361
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
362
|
+
...(sameKey ? cache || {} : {}),
|
|
363
|
+
timestamp: sameKey && cache?.timestamp ? cache.timestamp : Date.now(),
|
|
364
|
+
cacheKey: authContext.cacheKey,
|
|
365
|
+
accountId: authContext.accountId || "gemini-main",
|
|
366
|
+
tokenFingerprint: authContext.tokenFingerprint,
|
|
367
|
+
authSource: authContext.authSource,
|
|
368
|
+
expiryMissing: authContext.expiryMissing === true,
|
|
369
|
+
error: true,
|
|
370
|
+
errorType,
|
|
371
|
+
errorHint,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
// ============================================================================
|
|
309
376
|
// Gemini 쿼터 API 호출 (5분 캐시)
|
|
310
377
|
// ============================================================================
|
|
311
378
|
export async function fetchGeminiQuota(accountId, options = {}) {
|
|
312
379
|
const authContext = options.authContext || buildGeminiAuthContext(accountId);
|
|
313
380
|
const { oauth, tokenFingerprint, cacheKey } = authContext;
|
|
381
|
+
authContext.accountId = accountId || "gemini-main";
|
|
314
382
|
const forceRefresh = options.forceRefresh === true;
|
|
315
383
|
|
|
316
384
|
// 1. 캐시 확인 (계정/토큰별)
|
|
@@ -330,35 +398,34 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
330
398
|
|
|
331
399
|
if (!oauth?.access_token) {
|
|
332
400
|
// access_token 없음: 에러 힌트를 캐시에 기록하고 stale 캐시 반환
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
});
|
|
401
|
+
writeGeminiQuotaErrorCache(
|
|
402
|
+
cache,
|
|
403
|
+
authContext,
|
|
404
|
+
"auth",
|
|
405
|
+
"no access_token in oauth_creds.json",
|
|
406
|
+
);
|
|
340
407
|
return cache;
|
|
341
408
|
}
|
|
342
409
|
if (oauth.expiry_date && oauth.expiry_date < Date.now()) {
|
|
343
410
|
// OAuth 토큰 만료: 에러 힌트를 캐시에 기록 (refresh_token 갱신은 Gemini CLI 담당)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
});
|
|
411
|
+
writeGeminiQuotaErrorCache(
|
|
412
|
+
cache,
|
|
413
|
+
authContext,
|
|
414
|
+
"auth",
|
|
415
|
+
`token expired at ${new Date(oauth.expiry_date).toISOString()}`,
|
|
416
|
+
);
|
|
351
417
|
return cache;
|
|
352
418
|
}
|
|
353
419
|
|
|
354
420
|
// 3. projectId (캐시 or API)
|
|
421
|
+
let loadCodeAssistResponse = null;
|
|
355
422
|
const fetchProjectId = async () => {
|
|
356
|
-
|
|
423
|
+
loadCodeAssistResponse = await httpsPost(
|
|
357
424
|
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
|
358
425
|
{ metadata: { pluginType: "GEMINI" } },
|
|
359
426
|
oauth.access_token,
|
|
360
427
|
);
|
|
361
|
-
const id =
|
|
428
|
+
const id = loadCodeAssistResponse?.cloudaicompanionProject;
|
|
362
429
|
if (id)
|
|
363
430
|
writeJsonSafe(GEMINI_PROJECT_CACHE_PATH, {
|
|
364
431
|
cacheKey,
|
|
@@ -376,7 +443,19 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
376
443
|
let projectId =
|
|
377
444
|
projCache?.cacheKey === cacheKey ? projCache?.projectId : null;
|
|
378
445
|
if (!projectId) projectId = await fetchProjectId();
|
|
379
|
-
if (!projectId)
|
|
446
|
+
if (!projectId) {
|
|
447
|
+
writeGeminiQuotaErrorCache(
|
|
448
|
+
cache,
|
|
449
|
+
authContext,
|
|
450
|
+
classifyGeminiQuotaFailure(loadCodeAssistResponse, authContext),
|
|
451
|
+
formatGeminiQuotaFailure(
|
|
452
|
+
loadCodeAssistResponse,
|
|
453
|
+
authContext,
|
|
454
|
+
"loadCodeAssist",
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
return cache;
|
|
458
|
+
}
|
|
380
459
|
|
|
381
460
|
// 4. retrieveUserQuota 호출
|
|
382
461
|
let quotaRes = await httpsPost(
|
|
@@ -388,7 +467,19 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
388
467
|
// projectId 캐시가 만료/변경된 경우 1회 재시도
|
|
389
468
|
if (!quotaRes?.buckets && projCache?.projectId) {
|
|
390
469
|
projectId = await fetchProjectId();
|
|
391
|
-
if (!projectId)
|
|
470
|
+
if (!projectId) {
|
|
471
|
+
writeGeminiQuotaErrorCache(
|
|
472
|
+
cache,
|
|
473
|
+
authContext,
|
|
474
|
+
classifyGeminiQuotaFailure(loadCodeAssistResponse, authContext),
|
|
475
|
+
formatGeminiQuotaFailure(
|
|
476
|
+
loadCodeAssistResponse,
|
|
477
|
+
authContext,
|
|
478
|
+
"loadCodeAssist",
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
return cache;
|
|
482
|
+
}
|
|
392
483
|
quotaRes = await httpsPost(
|
|
393
484
|
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
|
|
394
485
|
{ project: projectId },
|
|
@@ -398,18 +489,12 @@ export async function fetchGeminiQuota(accountId, options = {}) {
|
|
|
398
489
|
|
|
399
490
|
if (!quotaRes?.buckets) {
|
|
400
491
|
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
quotaRes
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
...(cache || {}),
|
|
408
|
-
timestamp: cache?.timestamp || Date.now(),
|
|
409
|
-
error: true,
|
|
410
|
-
errorType: "api",
|
|
411
|
-
errorHint: String(apiError),
|
|
412
|
-
});
|
|
492
|
+
writeGeminiQuotaErrorCache(
|
|
493
|
+
cache,
|
|
494
|
+
authContext,
|
|
495
|
+
classifyGeminiQuotaFailure(quotaRes, authContext),
|
|
496
|
+
formatGeminiQuotaFailure(quotaRes, authContext, "quota"),
|
|
497
|
+
);
|
|
413
498
|
return cache;
|
|
414
499
|
}
|
|
415
500
|
|
package/hud/renderers.mjs
CHANGED
|
@@ -326,7 +326,7 @@ export function getMicroLine(
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
// sv
|
|
329
|
-
const sv = formatSvPct(combinedSvPct
|
|
329
|
+
const sv = formatSvPct(combinedSvPct).trim();
|
|
330
330
|
|
|
331
331
|
const cols = getTerminalColumns() || 120;
|
|
332
332
|
const line =
|
|
@@ -350,7 +350,7 @@ export function getClaudeRows(
|
|
|
350
350
|
const ctxView = contextView || buildContextUsageView({}, null);
|
|
351
351
|
const prefix = `${bold(claudeOrange("c"))}:`;
|
|
352
352
|
// 절약 퍼센트
|
|
353
|
-
const svStr = formatSvPct(combinedSvPct
|
|
353
|
+
const svStr = formatSvPct(combinedSvPct);
|
|
354
354
|
const svSuffix = `${dim("sv:")}${svStr}`;
|
|
355
355
|
|
|
356
356
|
// API 실측 데이터
|
package/package.json
CHANGED
package/scripts/lib/cli-agy.mjs
CHANGED
|
@@ -10,6 +10,8 @@ export const id = "agy";
|
|
|
10
10
|
export const cliType = "antigravity";
|
|
11
11
|
export const command = "agy";
|
|
12
12
|
|
|
13
|
+
// #310: agent-map.json normalizes upstream callers to antigravity,
|
|
14
|
+
// but this adapter also accepts the direct `agy` route for compatibility.
|
|
13
15
|
const AGENT_PROFILES = {
|
|
14
16
|
antigravity: {
|
|
15
17
|
profile: "agy_v1",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const PROFILE_SECTION_PREFIX = "profiles.";
|
|
5
|
+
const SAFE_PROFILE_FILE_RE = /^[A-Za-z0-9_.-]+$/;
|
|
6
|
+
|
|
7
|
+
function lineChunks(source = "") {
|
|
8
|
+
return String(source).match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseSectionHeader(line) {
|
|
12
|
+
const match = String(line)
|
|
13
|
+
.trimEnd()
|
|
14
|
+
.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
15
|
+
return match ? match[1].trim() : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function profileNameFromSection(section) {
|
|
19
|
+
if (!section?.startsWith(PROFILE_SECTION_PREFIX)) return null;
|
|
20
|
+
return section.slice(PROFILE_SECTION_PREFIX.length).trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isTopLevelProfileSelector(line) {
|
|
24
|
+
return /^\s*profile\s*=/.test(line);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeProfileBody(lines) {
|
|
28
|
+
const body = lines.join("").replace(/^\s+/, "").replace(/\s+$/, "");
|
|
29
|
+
return body ? `${body}\n` : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listLegacyCodexProfileSections(source = "") {
|
|
33
|
+
const names = [];
|
|
34
|
+
for (const line of lineChunks(source)) {
|
|
35
|
+
const name = profileNameFromSection(parseSectionHeader(line));
|
|
36
|
+
if (name && !names.includes(name)) names.push(name);
|
|
37
|
+
}
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sanitizeCodexProfileConfig(
|
|
42
|
+
source = "",
|
|
43
|
+
{ codexHome = null, migrateProfileFiles = true } = {},
|
|
44
|
+
) {
|
|
45
|
+
const output = [];
|
|
46
|
+
const removedProfiles = [];
|
|
47
|
+
const migratedProfiles = [];
|
|
48
|
+
const skippedProfileFiles = [];
|
|
49
|
+
const captured = new Map();
|
|
50
|
+
let currentProfile = null;
|
|
51
|
+
let seenSection = false;
|
|
52
|
+
let removedTopLevelProfileSelector = false;
|
|
53
|
+
|
|
54
|
+
for (const line of lineChunks(source)) {
|
|
55
|
+
const section = parseSectionHeader(line);
|
|
56
|
+
if (section) {
|
|
57
|
+
seenSection = true;
|
|
58
|
+
const profileName = profileNameFromSection(section);
|
|
59
|
+
if (profileName) {
|
|
60
|
+
currentProfile = profileName;
|
|
61
|
+
if (!removedProfiles.includes(profileName))
|
|
62
|
+
removedProfiles.push(profileName);
|
|
63
|
+
if (!captured.has(profileName)) captured.set(profileName, []);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
currentProfile = null;
|
|
67
|
+
output.push(line);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!seenSection && isTopLevelProfileSelector(line)) {
|
|
72
|
+
removedTopLevelProfileSelector = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (currentProfile) {
|
|
77
|
+
captured.get(currentProfile)?.push(line);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
output.push(line);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (codexHome && migrateProfileFiles) {
|
|
85
|
+
mkdirSync(codexHome, { recursive: true });
|
|
86
|
+
for (const [profileName, bodyLines] of captured.entries()) {
|
|
87
|
+
if (!SAFE_PROFILE_FILE_RE.test(profileName)) {
|
|
88
|
+
skippedProfileFiles.push(profileName);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const body = normalizeProfileBody(bodyLines);
|
|
92
|
+
if (!body) {
|
|
93
|
+
skippedProfileFiles.push(profileName);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const profilePath = join(codexHome, `${profileName}.config.toml`);
|
|
97
|
+
if (existsSync(profilePath)) {
|
|
98
|
+
skippedProfileFiles.push(profileName);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(profilePath, body, "utf8");
|
|
102
|
+
migratedProfiles.push(profileName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const toml = output.join("");
|
|
107
|
+
return {
|
|
108
|
+
changed: toml !== String(source),
|
|
109
|
+
toml,
|
|
110
|
+
removedProfiles,
|
|
111
|
+
migratedProfiles,
|
|
112
|
+
skippedProfileFiles,
|
|
113
|
+
removedTopLevelProfileSelector,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stamp(now = () => new Date()) {
|
|
118
|
+
return now()
|
|
119
|
+
.toISOString()
|
|
120
|
+
.replace(/[-:]/g, "")
|
|
121
|
+
.replace(/\..*$/, "")
|
|
122
|
+
.replace("T", "-");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function sanitizeCodexProfileConfigFile(
|
|
126
|
+
configPath,
|
|
127
|
+
{
|
|
128
|
+
codexHome = dirname(configPath),
|
|
129
|
+
backupPrefix = "bak-codex-profile-sanitize",
|
|
130
|
+
now = () => new Date(),
|
|
131
|
+
} = {},
|
|
132
|
+
) {
|
|
133
|
+
if (!configPath || !existsSync(configPath)) {
|
|
134
|
+
return { changed: false, removedProfiles: [], migratedProfiles: [] };
|
|
135
|
+
}
|
|
136
|
+
const source = readFileSync(configPath, "utf8");
|
|
137
|
+
const sanitized = sanitizeCodexProfileConfig(source, { codexHome });
|
|
138
|
+
if (!sanitized.changed) return sanitized;
|
|
139
|
+
writeFileSync(`${configPath}.${backupPrefix}-${stamp(now)}`, source, "utf8");
|
|
140
|
+
writeFileSync(configPath, sanitized.toml, "utf8");
|
|
141
|
+
return sanitized;
|
|
142
|
+
}
|
|
@@ -182,6 +182,15 @@ function isAntigravityConfig(filePath) {
|
|
|
182
182
|
return normalized.endsWith("/.gemini/config/mcp_config.json");
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function isMigratedAntigravityConfig(filePath, raw = null) {
|
|
186
|
+
if (!isAntigravityConfig(filePath)) return false;
|
|
187
|
+
const resolvedPath = resolveFilePath(filePath);
|
|
188
|
+
const markerPath = join(dirname(resolvedPath), ".migrated");
|
|
189
|
+
if (!existsSync(markerPath)) return false;
|
|
190
|
+
const contents = raw === null ? readFileSync(resolvedPath, "utf8") : raw;
|
|
191
|
+
return String(contents || "").trim() === "";
|
|
192
|
+
}
|
|
193
|
+
|
|
185
194
|
function plaintextSecretHeaderClient(filePath) {
|
|
186
195
|
const client = detectClient(filePath);
|
|
187
196
|
return client === "gemini" || client === "antigravity" ? client : null;
|
|
@@ -970,7 +979,23 @@ function scanJsonConfig(filePath) {
|
|
|
970
979
|
}
|
|
971
980
|
|
|
972
981
|
try {
|
|
973
|
-
const
|
|
982
|
+
const raw = readFileSync(filePath, "utf8");
|
|
983
|
+
if (isMigratedAntigravityConfig(filePath, raw)) {
|
|
984
|
+
return {
|
|
985
|
+
filePath,
|
|
986
|
+
client: detectClient(filePath),
|
|
987
|
+
label: detectLabel(filePath),
|
|
988
|
+
exists: true,
|
|
989
|
+
parseError: null,
|
|
990
|
+
servers: [],
|
|
991
|
+
stdioServers: [],
|
|
992
|
+
migrated: true,
|
|
993
|
+
message:
|
|
994
|
+
"Antigravity migrated MCP config; deprecated mcp_config.json left empty",
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const parsed = JSON.parse(raw);
|
|
974
999
|
const mcpServers = parsed?.mcpServers;
|
|
975
1000
|
const servers =
|
|
976
1001
|
!mcpServers || typeof mcpServers !== "object"
|
|
@@ -1705,6 +1730,8 @@ export function inspectRegistryStatus(registry = loadRegistryOrDefault()) {
|
|
|
1705
1730
|
status = "missing-file";
|
|
1706
1731
|
} else if (config.parseError) {
|
|
1707
1732
|
status = "invalid-config";
|
|
1733
|
+
} else if (config.migrated) {
|
|
1734
|
+
status = "skipped";
|
|
1708
1735
|
} else if (!actual) {
|
|
1709
1736
|
status = "missing";
|
|
1710
1737
|
} else if (expectedCommand) {
|
|
@@ -1733,6 +1760,8 @@ export function inspectRegistryStatus(registry = loadRegistryOrDefault()) {
|
|
|
1733
1760
|
actualUrl: actual?.url || "",
|
|
1734
1761
|
actualCommand: actual?.command || "",
|
|
1735
1762
|
status,
|
|
1763
|
+
migrated: Boolean(config.migrated),
|
|
1764
|
+
message: config.message || "",
|
|
1736
1765
|
headerStatus: currentHeaderStatus,
|
|
1737
1766
|
expectedHeaderNames: headerNames(expectedHeaders),
|
|
1738
1767
|
actualHeaderNames: headerNames(actualHeaders),
|
|
@@ -2015,6 +2044,19 @@ export function removeServerFromTargets(name, options = {}) {
|
|
|
2015
2044
|
if (targetsFilter && !targetsFilter.has(target.client)) continue;
|
|
2016
2045
|
|
|
2017
2046
|
const snapshot = scanConfig(target.filePath);
|
|
2047
|
+
if (snapshot.migrated) {
|
|
2048
|
+
actions.push({
|
|
2049
|
+
type: "remove",
|
|
2050
|
+
name: trimmedName,
|
|
2051
|
+
filePath: target.filePath,
|
|
2052
|
+
label: target.label,
|
|
2053
|
+
status: "skipped",
|
|
2054
|
+
migrated: true,
|
|
2055
|
+
message: snapshot.message,
|
|
2056
|
+
});
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2018
2060
|
if (snapshot.parseError) {
|
|
2019
2061
|
actions.push({
|
|
2020
2062
|
type: "remove",
|
|
@@ -2058,10 +2100,24 @@ export function syncRegistryTargets(options = {}) {
|
|
|
2058
2100
|
);
|
|
2059
2101
|
const actions = [];
|
|
2060
2102
|
const invalidConfigs = new Set();
|
|
2103
|
+
const skippedConfigs = new Set();
|
|
2061
2104
|
const denylist = syncDenylistEntries(registry.policies);
|
|
2062
2105
|
|
|
2063
2106
|
for (const target of syncTargets) {
|
|
2064
2107
|
const snapshot = scanConfig(target.filePath);
|
|
2108
|
+
if (snapshot.migrated) {
|
|
2109
|
+
skippedConfigs.add(normalizeForMatch(target.filePath));
|
|
2110
|
+
actions.push({
|
|
2111
|
+
type: "sync",
|
|
2112
|
+
filePath: target.filePath,
|
|
2113
|
+
label: target.label,
|
|
2114
|
+
status: "skipped",
|
|
2115
|
+
migrated: true,
|
|
2116
|
+
message: snapshot.message,
|
|
2117
|
+
});
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2065
2121
|
if (snapshot.parseError) {
|
|
2066
2122
|
invalidConfigs.add(normalizeForMatch(target.filePath));
|
|
2067
2123
|
actions.push({
|
|
@@ -2097,6 +2153,10 @@ export function syncRegistryTargets(options = {}) {
|
|
|
2097
2153
|
for (const target of primaryTargets) {
|
|
2098
2154
|
const snapshot = scanConfig(target.filePath);
|
|
2099
2155
|
const targetKey = normalizeForMatch(target.filePath);
|
|
2156
|
+
if (snapshot.migrated || skippedConfigs.has(targetKey)) {
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2100
2160
|
if (snapshot.parseError) {
|
|
2101
2161
|
if (!invalidConfigs.has(targetKey)) {
|
|
2102
2162
|
actions.push({
|
package/scripts/lib/toml.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { sanitizeCodexProfileConfig } from "./codex-profile-config.mjs";
|
|
3
5
|
|
|
4
6
|
const require = createRequire(import.meta.url);
|
|
5
7
|
let TOML = null;
|
|
@@ -100,14 +102,31 @@ export function patchMcpApprovalMode(source) {
|
|
|
100
102
|
export function patchCodexConfigFile(path, { now = () => new Date() } = {}) {
|
|
101
103
|
if (!path || !existsSync(path)) return { changed: false, count: 0 };
|
|
102
104
|
const source = readFileSync(path, "utf8");
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
+
}
|
|
105
118
|
const stamp = now()
|
|
106
119
|
.toISOString()
|
|
107
120
|
.replace(/[-:]/g, "")
|
|
108
121
|
.replace(/\..*$/, "")
|
|
109
122
|
.replace("T", "-");
|
|
110
123
|
writeFileSync(`${path}.bak-${stamp}`, source);
|
|
111
|
-
writeFileSync(path,
|
|
112
|
-
return
|
|
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
|
+
};
|
|
113
132
|
}
|