@triflux/core 10.49.0 → 10.50.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 +1 -1
- package/hooks/claude-cwd-projection-refresh.mjs +7 -79
- package/hooks/codex-session-hook.mjs +3 -8
- package/hooks/hook-orchestrator.mjs +8 -648
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hub/bridge.mjs +290 -51
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/session-context.mjs +203 -0
- package/hud/constants.mjs +10 -0
- package/hud/hud-qos-status.mjs +23 -4
- package/hud/providers/gemini.mjs +4 -1
- package/hud/renderers.mjs +10 -6
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/session-presence.mjs +249 -0
- package/hooks/agent-route-guard.mjs +0 -113
- package/hooks/cross-review-tracker.mjs +0 -135
- package/hooks/cto-north-star-brief.mjs +0 -133
- package/hooks/error-context.mjs +0 -185
- package/hooks/hook-adaptive-collector.mjs +0 -88
- package/hooks/hook-manager.mjs +0 -426
- package/hooks/hook-registry.json +0 -367
- package/hooks/hooks.json +0 -149
- package/hooks/keyword-rules.json +0 -676
- package/hooks/mcp-config-watcher.mjs +0 -84
- package/hooks/permission-safe-allow.mjs +0 -267
- package/hooks/pipeline-stop.mjs +0 -435
- package/hooks/post-tool-tips.mjs +0 -306
- package/hooks/pre-compact-snapshot.mjs +0 -181
- package/hooks/safety-guard.mjs +0 -466
- package/hooks/session-end-cleanup.mjs +0 -360
- package/hooks/session-start-fast.mjs +0 -579
- package/hooks/session-start-lake.mjs +0 -81
- package/hooks/subagent-tracker.mjs +0 -281
- package/hooks/subagent-verifier.mjs +0 -85
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -296
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
// 설치된 Triflux 실행 대상만 식별한다.
|
|
17
|
+
const MANAGED_FILENAMES = [
|
|
18
|
+
"hook-orchestrator.mjs",
|
|
19
|
+
"claude-cwd-projection-refresh.mjs",
|
|
20
|
+
"permission-safe-allow.mjs",
|
|
21
|
+
"safety-guard.mjs",
|
|
22
|
+
"pipeline-stop.mjs",
|
|
23
|
+
"agent-route-guard.mjs",
|
|
24
|
+
"cross-review-tracker.mjs",
|
|
25
|
+
"cross-review-gate.mjs",
|
|
26
|
+
"tfx-gate-activate.mjs",
|
|
27
|
+
"subagent-tracker.mjs",
|
|
28
|
+
"subagent-verifier.mjs",
|
|
29
|
+
"error-context.mjs",
|
|
30
|
+
"hook-adaptive-collector.mjs",
|
|
31
|
+
"post-tool-tips.mjs",
|
|
32
|
+
"pre-compact-snapshot.mjs",
|
|
33
|
+
"cto-north-star-brief.mjs",
|
|
34
|
+
"session-start-lake.mjs",
|
|
35
|
+
"session-start-fast.mjs",
|
|
36
|
+
"session-end-cleanup.mjs",
|
|
37
|
+
"mcp-config-watcher.mjs",
|
|
38
|
+
"keyword-detector.mjs",
|
|
39
|
+
"setup.mjs",
|
|
40
|
+
"hub-ensure.mjs",
|
|
41
|
+
"preflight-cache.mjs",
|
|
42
|
+
"mcp-gateway-ensure.mjs",
|
|
43
|
+
"mcp-safety-guard.mjs",
|
|
44
|
+
"session-stale-cleanup.mjs",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const targetNames = MANAGED_FILENAMES.join("|").replaceAll(".", "\\.");
|
|
48
|
+
const uniqueTargetNames = MANAGED_FILENAMES.filter(
|
|
49
|
+
(name) =>
|
|
50
|
+
![
|
|
51
|
+
"setup.mjs",
|
|
52
|
+
"hub-ensure.mjs",
|
|
53
|
+
"preflight-cache.mjs",
|
|
54
|
+
"mcp-gateway-ensure.mjs",
|
|
55
|
+
"mcp-safety-guard.mjs",
|
|
56
|
+
"session-stale-cleanup.mjs",
|
|
57
|
+
].includes(name),
|
|
58
|
+
)
|
|
59
|
+
.join("|")
|
|
60
|
+
.replaceAll(".", "\\.");
|
|
61
|
+
const packageTarget = new RegExp(
|
|
62
|
+
String.raw`[/\\]triflux(?:[/\\](?:\.worktrees[/\\][^/\\]+|v?\d+\.\d+\.\d+[^/\\]*))?[/\\](?:hooks|scripts)[/\\](?:${targetNames})$`,
|
|
63
|
+
);
|
|
64
|
+
const pluginRootTarget = new RegExp(
|
|
65
|
+
String.raw`^\$\{(?:CLAUDE_)?PLUGIN_ROOT\}[/\\](?:hooks|scripts)[/\\](?:${uniqueTargetNames})$`,
|
|
66
|
+
);
|
|
67
|
+
const gateSuffix = ".claude/scripts/tfx-gate-activate.mjs";
|
|
68
|
+
|
|
69
|
+
function isInstalledGateTarget(target) {
|
|
70
|
+
const normalized = target.replaceAll("\\", "/");
|
|
71
|
+
if (
|
|
72
|
+
["${HOME}", "$HOME", "~"].some(
|
|
73
|
+
(root) => normalized === `${root}/${gateSuffix}`,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return true;
|
|
77
|
+
const installedTargets = [join(homedir(), gateSuffix)];
|
|
78
|
+
if (process.env.CLAUDE_CONFIG_DIR)
|
|
79
|
+
installedTargets.push(
|
|
80
|
+
join(process.env.CLAUDE_CONFIG_DIR, "scripts/tfx-gate-activate.mjs"),
|
|
81
|
+
);
|
|
82
|
+
return installedTargets.some(
|
|
83
|
+
(installed) => normalized === installed.replaceAll("\\", "/"),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readCommandToken(command) {
|
|
88
|
+
const match = command.match(/^\s*(?:"([^"]*)"|'([^']*)'|(\S+))(.*)$/s);
|
|
89
|
+
return match
|
|
90
|
+
? { value: match[1] ?? match[2] ?? match[3], rest: match[4] }
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchesTarget(target) {
|
|
95
|
+
const resolvedFallback = target.replace(
|
|
96
|
+
/^\$\{(?:CLAUDE_)?PLUGIN_ROOT:-([^}]+)\}/,
|
|
97
|
+
"$1",
|
|
98
|
+
);
|
|
99
|
+
return (
|
|
100
|
+
packageTarget.test(resolvedFallback) ||
|
|
101
|
+
pluginRootTarget.test(target) ||
|
|
102
|
+
isInstalledGateTarget(target)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isLegacyTrifluxHook(hook) {
|
|
107
|
+
if (hook?.type !== "command" || typeof hook.command !== "string")
|
|
108
|
+
return false;
|
|
109
|
+
const command = hook.command;
|
|
110
|
+
const executable = readCommandToken(command);
|
|
111
|
+
if (!executable || !/(?:^|[/\\])node(?:\.exe)?$/.test(executable.value))
|
|
112
|
+
return false;
|
|
113
|
+
const target = readCommandToken(executable.rest);
|
|
114
|
+
if (!target) return false;
|
|
115
|
+
// 옛 inline bootstrap만 제거한다.
|
|
116
|
+
if (target.value === "-e" || target.value === "--eval")
|
|
117
|
+
return (
|
|
118
|
+
command.includes(".tfx-pkg-root") &&
|
|
119
|
+
command.includes("hook-orchestrator.mjs") &&
|
|
120
|
+
command.includes("spawnSync") &&
|
|
121
|
+
command.includes("path.join(root")
|
|
122
|
+
);
|
|
123
|
+
if (matchesTarget(target.value)) return true;
|
|
124
|
+
// run.cjs의 keyword-detector 인자를 확인한다.
|
|
125
|
+
if (/[/\\]scripts[/\\]run\.cjs$/.test(target.value)) {
|
|
126
|
+
const script = readCommandToken(target.rest);
|
|
127
|
+
return (
|
|
128
|
+
script?.value.endsWith("/scripts/keyword-detector.mjs") &&
|
|
129
|
+
(matchesTarget(script.value) || pluginRootTarget.test(script.value))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function removeManagedHooks(settings) {
|
|
136
|
+
let removed = 0;
|
|
137
|
+
const events = settings?.hooks;
|
|
138
|
+
if (!events || typeof events !== "object" || Array.isArray(events))
|
|
139
|
+
return removed;
|
|
140
|
+
for (const [event, entries] of Object.entries(events)) {
|
|
141
|
+
if (!Array.isArray(entries)) continue;
|
|
142
|
+
const kept = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
145
|
+
kept.push(entry);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const hooks = entry.hooks.filter((hook) => {
|
|
149
|
+
if (!isLegacyTrifluxHook(hook)) return true;
|
|
150
|
+
removed++;
|
|
151
|
+
return false;
|
|
152
|
+
});
|
|
153
|
+
if (hooks.length)
|
|
154
|
+
kept.push(
|
|
155
|
+
hooks.length === entry.hooks.length ? entry : { ...entry, hooks },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (kept.length) events[event] = kept;
|
|
159
|
+
else delete events[event];
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(events).length === 0) delete settings.hooks;
|
|
162
|
+
return removed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Claude 설정의 이전 Triflux command hook을 정리한다. */
|
|
166
|
+
export function cleanupLegacyHooks({
|
|
167
|
+
settingsPath = join(
|
|
168
|
+
process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
|
|
169
|
+
"settings.json",
|
|
170
|
+
),
|
|
171
|
+
dryRun = false,
|
|
172
|
+
} = {}) {
|
|
173
|
+
const result = {
|
|
174
|
+
ok: true,
|
|
175
|
+
changed: false,
|
|
176
|
+
wouldChange: false,
|
|
177
|
+
removed: 0,
|
|
178
|
+
backupPath: null,
|
|
179
|
+
error: null,
|
|
180
|
+
};
|
|
181
|
+
let original;
|
|
182
|
+
let settings;
|
|
183
|
+
try {
|
|
184
|
+
const pathInfo = lstatSync(settingsPath, { throwIfNoEntry: false });
|
|
185
|
+
if (!pathInfo) return result;
|
|
186
|
+
// symlink는 유지하고 실제 파일을 교체한다.
|
|
187
|
+
const targetPath = pathInfo.isSymbolicLink()
|
|
188
|
+
? realpathSync(settingsPath)
|
|
189
|
+
: settingsPath;
|
|
190
|
+
original = readFileSync(targetPath, "utf8");
|
|
191
|
+
settings = JSON.parse(original);
|
|
192
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
193
|
+
throw new Error("settings.json must contain an object");
|
|
194
|
+
result.removed = removeManagedHooks(settings);
|
|
195
|
+
result.wouldChange = result.removed > 0;
|
|
196
|
+
if (!result.wouldChange || dryRun) return result;
|
|
197
|
+
|
|
198
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
199
|
+
let backupPath = `${settingsPath}.tfx-bak-${stamp}`;
|
|
200
|
+
for (let suffix = 1; existsSync(backupPath); suffix++)
|
|
201
|
+
backupPath = `${settingsPath}.tfx-bak-${stamp}-${suffix}`;
|
|
202
|
+
copyFileSync(settingsPath, backupPath, constants.COPYFILE_EXCL);
|
|
203
|
+
result.backupPath = backupPath;
|
|
204
|
+
|
|
205
|
+
const temporary = join(
|
|
206
|
+
dirname(targetPath),
|
|
207
|
+
`.settings.json.tfx-${process.pid}-${Date.now()}.tmp`,
|
|
208
|
+
);
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
flag: "wx",
|
|
213
|
+
mode: statSync(targetPath).mode,
|
|
214
|
+
});
|
|
215
|
+
renameSync(temporary, targetPath);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(temporary);
|
|
219
|
+
} catch {
|
|
220
|
+
/* 임시 파일 없음 */
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
result.changed = true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
result.ok = false;
|
|
227
|
+
result.error = error.message;
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|