@triflux/core 10.35.3 → 10.37.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 +2 -2
- 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/hub/routing/q-learning.mjs +5 -2
- package/hub/team/claude-daemon-control.mjs +27 -2
- package/hud/hud-qos-status.mjs +16 -5
- 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/stealth-fetch.mjs +176 -0
- package/scripts/lib/toml.mjs +23 -4
|
@@ -45,7 +45,7 @@ export function toSessionPayload(payload) {
|
|
|
45
45
|
typeof workspacePaths[0] === "string" && workspacePaths[0]
|
|
46
46
|
? workspacePaths[0]
|
|
47
47
|
: process.cwd();
|
|
48
|
-
return JSON.stringify({ session_id: sessionId, cwd });
|
|
48
|
+
return JSON.stringify({ session_id: sessionId, cwd, actor_cli: "agy" });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// agy has no distinct SessionStart / UserPromptSubmit events. PreInvocation
|
|
@@ -98,7 +98,7 @@ export async function runAgySessionHook(stdinData, opts = {}) {
|
|
|
98
98
|
await hubEnsureRun(sessionPayload);
|
|
99
99
|
} catch {}
|
|
100
100
|
try {
|
|
101
|
-
registerInteractiveSession(sessionPayload);
|
|
101
|
+
await Promise.resolve(registerInteractiveSession(sessionPayload));
|
|
102
102
|
} catch {}
|
|
103
103
|
try {
|
|
104
104
|
await drainPendingSynapse(1000);
|
|
@@ -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(
|
|
@@ -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
|
}
|
|
@@ -459,6 +481,7 @@ export async function readDaemonControlKey(
|
|
|
459
481
|
}
|
|
460
482
|
|
|
461
483
|
export async function buildDaemonControlAuth(configDir) {
|
|
484
|
+
if (!configDir) return {};
|
|
462
485
|
const auth = await readDaemonControlKey(configDir);
|
|
463
486
|
return auth ? { auth } : {};
|
|
464
487
|
}
|
|
@@ -1223,7 +1246,7 @@ export function attachClaudeDaemonSession({
|
|
|
1223
1246
|
cancelCompletionTimer();
|
|
1224
1247
|
}
|
|
1225
1248
|
});
|
|
1226
|
-
|
|
1249
|
+
const finishClosed = () => {
|
|
1227
1250
|
if (!settled) {
|
|
1228
1251
|
if (pendingCompletion) {
|
|
1229
1252
|
finish(pendingCompletion);
|
|
@@ -1231,7 +1254,9 @@ export function attachClaudeDaemonSession({
|
|
|
1231
1254
|
}
|
|
1232
1255
|
finish({ timedOut: false, matchedCompletion: false, closed: true });
|
|
1233
1256
|
}
|
|
1234
|
-
}
|
|
1257
|
+
};
|
|
1258
|
+
socket.on("end", finishClosed);
|
|
1259
|
+
socket.on("close", finishClosed);
|
|
1235
1260
|
});
|
|
1236
1261
|
}
|
|
1237
1262
|
|
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/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({
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SSRF boundary: stealthFetch only accepts http: and https: URLs. It does not
|
|
3
|
+
// guard private IPs, metadata hosts such as 169.254.x.x, or localhost; callers
|
|
4
|
+
// must treat the URL as already inside their trust boundary.
|
|
5
|
+
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export class CloakBrowserUnavailableError extends Error {}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
11
|
+
const DEFAULT_WAIT_UNTIL = "load";
|
|
12
|
+
const SUPPORTED_PLATFORMS = new Set([
|
|
13
|
+
"linux:x64",
|
|
14
|
+
"linux:arm64",
|
|
15
|
+
"darwin:x64",
|
|
16
|
+
"darwin:arm64",
|
|
17
|
+
"win32:x64",
|
|
18
|
+
]);
|
|
19
|
+
const EXIT_CODES = {
|
|
20
|
+
not_installed: 3,
|
|
21
|
+
unsupported_platform: 4,
|
|
22
|
+
runtime_error: 5,
|
|
23
|
+
blocked_scheme: 6,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function isSupportedPlatform(platform, arch) {
|
|
27
|
+
return SUPPORTED_PLATFORMS.has(`${platform}:${arch}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isModuleNotFound(error) {
|
|
31
|
+
return (
|
|
32
|
+
error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function htmlToText(html) {
|
|
37
|
+
return String(html || "")
|
|
38
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/giu, " ")
|
|
39
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/giu, " ")
|
|
40
|
+
.replace(/<[^>]+>/gu, " ")
|
|
41
|
+
.replace(/ /giu, " ")
|
|
42
|
+
.replace(/&/giu, "&")
|
|
43
|
+
.replace(/</giu, "<")
|
|
44
|
+
.replace(/>/giu, ">")
|
|
45
|
+
.replace(/"/giu, '"')
|
|
46
|
+
.replace(/'/gu, "'")
|
|
47
|
+
.replace(/\s+/gu, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function responseStatus(response) {
|
|
52
|
+
if (!response) return null;
|
|
53
|
+
if (typeof response.status === "function") return response.status();
|
|
54
|
+
return response.status ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function responseUrl(response, fallbackUrl) {
|
|
58
|
+
if (!response) return fallbackUrl;
|
|
59
|
+
if (typeof response.url === "function") return response.url();
|
|
60
|
+
return response.url ?? fallbackUrl;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAllowedFetchUrl(url) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(url);
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: false, scheme: null };
|
|
69
|
+
}
|
|
70
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
71
|
+
return { ok: false, scheme: parsed.protocol };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, href: parsed.href };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function closeBrowser(browser) {
|
|
77
|
+
if (!browser || typeof browser.close !== "function") return;
|
|
78
|
+
try {
|
|
79
|
+
await browser.close();
|
|
80
|
+
} catch {
|
|
81
|
+
// best effort: fetch callers only need the primary result signal
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function stealthFetch(url, opts = {}) {
|
|
86
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
87
|
+
const waitUntil = opts.waitUntil ?? DEFAULT_WAIT_UNTIL;
|
|
88
|
+
const loadCloakBrowser = opts._import ?? (() => import("cloakbrowser"));
|
|
89
|
+
const platform = opts._platform ?? process.platform;
|
|
90
|
+
const arch = opts._arch ?? process.arch;
|
|
91
|
+
|
|
92
|
+
if (!isSupportedPlatform(platform, arch)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "unsupported_platform",
|
|
96
|
+
platform,
|
|
97
|
+
arch,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsedUrl = parseAllowedFetchUrl(url);
|
|
102
|
+
if (!parsedUrl.ok) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: "blocked_scheme",
|
|
106
|
+
scheme: parsedUrl.scheme,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cloakbrowser;
|
|
111
|
+
try {
|
|
112
|
+
cloakbrowser = await loadCloakBrowser();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isModuleNotFound(error)) return { ok: false, reason: "not_installed" };
|
|
115
|
+
return { ok: false, reason: "runtime_error", error: String(error) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let browser;
|
|
119
|
+
try {
|
|
120
|
+
const launch = cloakbrowser?.launch;
|
|
121
|
+
if (typeof launch !== "function") {
|
|
122
|
+
throw new CloakBrowserUnavailableError("cloakbrowser launch unavailable");
|
|
123
|
+
}
|
|
124
|
+
browser = await launch();
|
|
125
|
+
const page = await browser.newPage();
|
|
126
|
+
const response = await page.goto(parsedUrl.href, {
|
|
127
|
+
waitUntil,
|
|
128
|
+
timeout: timeoutMs,
|
|
129
|
+
});
|
|
130
|
+
const html = await page.content();
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
engine: "cloakbrowser",
|
|
134
|
+
url: parsedUrl.href,
|
|
135
|
+
finalUrl: responseUrl(response, parsedUrl.href),
|
|
136
|
+
status: responseStatus(response),
|
|
137
|
+
html,
|
|
138
|
+
text: htmlToText(html),
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { ok: false, reason: "runtime_error", error: String(error) };
|
|
142
|
+
} finally {
|
|
143
|
+
await closeBrowser(browser);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function exitCodeFor(result) {
|
|
148
|
+
return EXIT_CODES[result?.reason] ?? 5;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function main(argv = process.argv) {
|
|
152
|
+
const url = argv[2];
|
|
153
|
+
if (!url) {
|
|
154
|
+
console.error("usage: stealth-fetch <url>");
|
|
155
|
+
process.exitCode = 2;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await stealthFetch(url);
|
|
160
|
+
if (result.ok) {
|
|
161
|
+
console.log(JSON.stringify(result));
|
|
162
|
+
process.exitCode = 0;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error(`[stealth-fetch] 폴백: ${result.reason}`);
|
|
167
|
+
console.log(JSON.stringify(result));
|
|
168
|
+
process.exitCode = exitCodeFor(result);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
process.argv[1] &&
|
|
173
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
174
|
+
) {
|
|
175
|
+
await main();
|
|
176
|
+
}
|
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
|
}
|