@triflux/core 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
|
|
3
|
+
*
|
|
4
|
+
* Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
|
|
5
|
+
* 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* import { getLogger, getCorrelationId, withRequestContext } from './lib/context.mjs';
|
|
9
|
+
*
|
|
10
|
+
* // 미들웨어에서 컨텍스트 생성
|
|
11
|
+
* withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
|
|
12
|
+
* const log = getLogger();
|
|
13
|
+
* log.info({ agentId }, 'bridge.result_received');
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // 내부 함수에서 자동 상관 ID
|
|
17
|
+
* function processResult() {
|
|
18
|
+
* const log = getLogger();
|
|
19
|
+
* log.info('result.processed'); // correlationId 자동 포함
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
import { logger } from './logger.mjs';
|
|
26
|
+
|
|
27
|
+
/** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
|
|
28
|
+
export const asyncLocalStorage = new AsyncLocalStorage();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 현재 요청 컨텍스트의 로거를 반환한다.
|
|
32
|
+
* 요청 컨텍스트 밖에서 호출하면 기본 로거를 반환한다.
|
|
33
|
+
*
|
|
34
|
+
* @returns {import('pino').Logger}
|
|
35
|
+
*/
|
|
36
|
+
export function getLogger() {
|
|
37
|
+
return asyncLocalStorage.getStore()?.logger || logger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 현재 요청의 상관 ID를 반환한다.
|
|
42
|
+
*
|
|
43
|
+
* @returns {string|undefined}
|
|
44
|
+
*/
|
|
45
|
+
export function getCorrelationId() {
|
|
46
|
+
return asyncLocalStorage.getStore()?.correlationId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 요청 컨텍스트를 생성하고 콜백을 실행한다.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} context — 컨텍스트 필드 (method, path 등)
|
|
53
|
+
* @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
|
|
54
|
+
* @param {function} callback — 컨텍스트 내에서 실행할 함수
|
|
55
|
+
* @returns {*}
|
|
56
|
+
*/
|
|
57
|
+
export function withRequestContext(context, callback) {
|
|
58
|
+
const correlationId = context.correlationId || randomUUID();
|
|
59
|
+
const { correlationId: _, ...rest } = context;
|
|
60
|
+
|
|
61
|
+
const store = {
|
|
62
|
+
correlationId,
|
|
63
|
+
logger: logger.child({ correlationId, ...rest }),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return asyncLocalStorage.run(store, callback);
|
|
67
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SESSION_TTL_SEC = 30 * 60;
|
|
4
|
+
export const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
|
|
5
|
+
|
|
6
|
+
export function readStdin() {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let raw = "";
|
|
9
|
+
process.stdin.setEncoding("utf8");
|
|
10
|
+
process.stdin.on("data", (chunk) => {
|
|
11
|
+
raw += chunk;
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on("end", () => resolve(raw));
|
|
14
|
+
process.stdin.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseJson(raw) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function nowSec() {
|
|
27
|
+
return Math.floor(Date.now() / 1000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBaseDir(payload) {
|
|
31
|
+
if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
|
|
32
|
+
if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function shouldTrackPath(filePath) {
|
|
37
|
+
if (typeof filePath !== "string" || !filePath.trim()) return false;
|
|
38
|
+
|
|
39
|
+
const lower = filePath.toLowerCase();
|
|
40
|
+
if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
|
|
41
|
+
if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
|
|
42
|
+
if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expectedReviewer(author) {
|
|
47
|
+
if (author === "claude") return "codex";
|
|
48
|
+
if (author === "codex") return "claude";
|
|
49
|
+
if (author === "gemini") return "claude";
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { whichCommand, whichCommandAsync } from "../../hub/platform.mjs";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
|
|
10
|
+
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
+
const CLI_PROBE_CACHE = new Map();
|
|
12
|
+
const CLI_PROBE_PROMISES = new Map();
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const DEFAULT_PKG_ROOT = join(dirname(__filename), "..", "..");
|
|
16
|
+
|
|
17
|
+
function sleepSync(ms) {
|
|
18
|
+
Atomics.wait(_sab, 0, 0, ms);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fetchHubStatus({
|
|
22
|
+
execSyncFn = execSync,
|
|
23
|
+
statusUrl = DEFAULT_STATUS_URL,
|
|
24
|
+
timeout = 3000,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const response = execSyncFn(`curl -sf ${statusUrl}`, {
|
|
27
|
+
timeout,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
const data = JSON.parse(response);
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
state: data?.hub?.state || "unknown",
|
|
35
|
+
pid: data?.pid,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeCliName(name) {
|
|
40
|
+
return String(name ?? "").trim() || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toCliResult(path) {
|
|
44
|
+
return path ? { ok: true, path } : { ok: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cloneCliResult(result) {
|
|
48
|
+
return result?.ok ? { ...result } : { ok: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readCachedCliResult(name) {
|
|
52
|
+
const cached = CLI_PROBE_CACHE.get(name);
|
|
53
|
+
return cached ? cloneCliResult(cached) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function storeCliResult(name, result) {
|
|
57
|
+
const snapshot = cloneCliResult(result);
|
|
58
|
+
CLI_PROBE_CACHE.set(name, snapshot);
|
|
59
|
+
return cloneCliResult(snapshot);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildCliProbeOptions(options = {}) {
|
|
63
|
+
return {
|
|
64
|
+
timeout: options.timeout ?? 2000,
|
|
65
|
+
env: options.env,
|
|
66
|
+
cwd: options.cwd,
|
|
67
|
+
platform: options.platform,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeCliNames(names) {
|
|
72
|
+
return [...new Set((names || []).map(normalizeCliName).filter(Boolean))];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function mapCliResults(names, results) {
|
|
76
|
+
return names.reduce((acc, name, index) => ({
|
|
77
|
+
...acc,
|
|
78
|
+
[name]: results[index],
|
|
79
|
+
}), {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resolveCliProbe(name, options = {}) {
|
|
83
|
+
const path = await (options.whichCommandAsyncFn || whichCommandAsync)(name, {
|
|
84
|
+
...buildCliProbeOptions(options),
|
|
85
|
+
execFileFn: options.execFileFn,
|
|
86
|
+
});
|
|
87
|
+
return toCliResult(path);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function checkCli(name, options = {}) {
|
|
91
|
+
const cliName = normalizeCliName(name);
|
|
92
|
+
if (!cliName) return { ok: false };
|
|
93
|
+
|
|
94
|
+
const cached = readCachedCliResult(cliName);
|
|
95
|
+
if (cached) return cached;
|
|
96
|
+
|
|
97
|
+
const pending = CLI_PROBE_PROMISES.get(cliName);
|
|
98
|
+
if (pending) return pending.then(cloneCliResult);
|
|
99
|
+
|
|
100
|
+
const nextProbe = resolveCliProbe(cliName, options)
|
|
101
|
+
.then((result) => storeCliResult(cliName, result))
|
|
102
|
+
.catch(() => storeCliResult(cliName, { ok: false }))
|
|
103
|
+
.finally(() => {
|
|
104
|
+
CLI_PROBE_PROMISES.delete(cliName);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
CLI_PROBE_PROMISES.set(cliName, nextProbe);
|
|
108
|
+
return nextProbe.then(cloneCliResult);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function checkCliSync(name, options = {}) {
|
|
112
|
+
const cliName = normalizeCliName(name);
|
|
113
|
+
if (!cliName) return { ok: false };
|
|
114
|
+
|
|
115
|
+
const cached = readCachedCliResult(cliName);
|
|
116
|
+
if (cached) return cached;
|
|
117
|
+
|
|
118
|
+
const path = (options.whichCommandFn || whichCommand)(cliName, buildCliProbeOptions(options));
|
|
119
|
+
return storeCliResult(cliName, toCliResult(path));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function probeClis(names, options = {}) {
|
|
123
|
+
const cliNames = normalizeCliNames(names);
|
|
124
|
+
const results = await Promise.all(cliNames.map((name) => checkCli(name, options)));
|
|
125
|
+
return mapCliResults(cliNames, results);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resetCliProbeCache() {
|
|
129
|
+
CLI_PROBE_CACHE.clear();
|
|
130
|
+
CLI_PROBE_PROMISES.clear();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function detectCodexAuthState({
|
|
134
|
+
homeDir = homedir(),
|
|
135
|
+
existsSyncFn = existsSync,
|
|
136
|
+
readFileSyncFn = readFileSync,
|
|
137
|
+
} = {}) {
|
|
138
|
+
try {
|
|
139
|
+
const authPath = join(homeDir, ".codex", "auth.json");
|
|
140
|
+
if (!existsSyncFn(authPath)) return { plan: "unknown", source: "no_auth", fingerprint: "no_auth" };
|
|
141
|
+
|
|
142
|
+
const auth = JSON.parse(readFileSyncFn(authPath, "utf8"));
|
|
143
|
+
if (auth.auth_mode !== "chatgpt") {
|
|
144
|
+
const fingerprint = createHash("sha256")
|
|
145
|
+
.update(JSON.stringify({
|
|
146
|
+
auth_mode: auth.auth_mode || "api_key",
|
|
147
|
+
has_api_key: Boolean(auth.api_key || auth.apiKey),
|
|
148
|
+
}))
|
|
149
|
+
.digest("hex");
|
|
150
|
+
return { plan: "api", source: "api_key", fingerprint };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const token = auth.tokens?.id_token || auth.tokens?.access_token;
|
|
154
|
+
if (!token) {
|
|
155
|
+
return {
|
|
156
|
+
plan: "unknown",
|
|
157
|
+
source: "no_token",
|
|
158
|
+
fingerprint: createHash("sha256")
|
|
159
|
+
.update(JSON.stringify({ auth_mode: auth.auth_mode || "chatgpt", token: null }))
|
|
160
|
+
.digest("hex"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
165
|
+
const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
|
|
166
|
+
const fingerprint = createHash("sha256")
|
|
167
|
+
.update(JSON.stringify({
|
|
168
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
169
|
+
plan,
|
|
170
|
+
sub: payload?.sub || null,
|
|
171
|
+
exp: payload?.exp || null,
|
|
172
|
+
}))
|
|
173
|
+
.digest("hex");
|
|
174
|
+
return { plan, source: "jwt", fingerprint };
|
|
175
|
+
} catch {
|
|
176
|
+
return { plan: "unknown", source: "error", fingerprint: "error" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function detectCodexPlan(options = {}) {
|
|
181
|
+
const { plan, source } = detectCodexAuthState(options);
|
|
182
|
+
return { plan, source };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function checkHub({
|
|
186
|
+
pkgRoot = DEFAULT_PKG_ROOT,
|
|
187
|
+
statusUrl = DEFAULT_STATUS_URL,
|
|
188
|
+
restart = true,
|
|
189
|
+
requestTimeoutMs = 3000,
|
|
190
|
+
pollAttempts = 8,
|
|
191
|
+
pollIntervalMs = 500,
|
|
192
|
+
execSyncFn = execSync,
|
|
193
|
+
spawnFn = spawn,
|
|
194
|
+
existsSyncFn = existsSync,
|
|
195
|
+
sleepSyncFn = sleepSync,
|
|
196
|
+
} = {}) {
|
|
197
|
+
try {
|
|
198
|
+
return fetchHubStatus({
|
|
199
|
+
execSyncFn,
|
|
200
|
+
statusUrl,
|
|
201
|
+
timeout: requestTimeoutMs,
|
|
202
|
+
});
|
|
203
|
+
} catch {}
|
|
204
|
+
|
|
205
|
+
if (!restart) return { ok: false, state: "unreachable", restart: "disabled" };
|
|
206
|
+
|
|
207
|
+
const serverPath = join(pkgRoot, "hub", "server.mjs");
|
|
208
|
+
if (!existsSyncFn(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const child = spawnFn(process.execPath, [serverPath], {
|
|
212
|
+
detached: true,
|
|
213
|
+
stdio: "ignore",
|
|
214
|
+
windowsHide: true,
|
|
215
|
+
});
|
|
216
|
+
child.unref();
|
|
217
|
+
} catch {
|
|
218
|
+
return { ok: false, state: "unreachable", restart: "spawn_failed" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < pollAttempts; i++) {
|
|
222
|
+
sleepSyncFn(pollIntervalMs);
|
|
223
|
+
try {
|
|
224
|
+
const status = fetchHubStatus({
|
|
225
|
+
execSyncFn,
|
|
226
|
+
statusUrl,
|
|
227
|
+
timeout: Math.min(requestTimeoutMs, 1000),
|
|
228
|
+
});
|
|
229
|
+
if (status.state === "healthy") {
|
|
230
|
+
return { ...status, restarted: true };
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
DEFAULT_PKG_ROOT,
|
|
240
|
+
DEFAULT_STATUS_URL,
|
|
241
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_GEMINI_PROFILES = {
|
|
6
|
+
model: "gemini-3.1-pro-preview",
|
|
7
|
+
profiles: {
|
|
8
|
+
pro31: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
|
|
9
|
+
flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
|
|
10
|
+
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
11
|
+
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
12
|
+
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PROFILE_COUNT = Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length;
|
|
17
|
+
|
|
18
|
+
function ensureGeminiProfiles({
|
|
19
|
+
geminiDir = join(homedir(), ".gemini"),
|
|
20
|
+
profilesPath = join(geminiDir, "triflux-profiles.json"),
|
|
21
|
+
} = {}) {
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
if (!existsSync(profilesPath)) {
|
|
26
|
+
writeFileSync(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
created: true,
|
|
30
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
31
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
32
|
+
message: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cfg;
|
|
37
|
+
try {
|
|
38
|
+
cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
try { copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`); } catch {}
|
|
41
|
+
writeFileSync(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
created: true,
|
|
45
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
46
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
47
|
+
message: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
52
|
+
if (!cfg.profiles || typeof cfg.profiles !== "object" || Array.isArray(cfg.profiles)) cfg.profiles = {};
|
|
53
|
+
|
|
54
|
+
let added = 0;
|
|
55
|
+
for (const [name, value] of Object.entries(DEFAULT_GEMINI_PROFILES.profiles)) {
|
|
56
|
+
if (!cfg.profiles[name]) {
|
|
57
|
+
cfg.profiles[name] = value;
|
|
58
|
+
added++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
62
|
+
|
|
63
|
+
if (added > 0) {
|
|
64
|
+
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
created: false,
|
|
70
|
+
added,
|
|
71
|
+
count: Object.keys(cfg.profiles).length,
|
|
72
|
+
message: null,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
created: false,
|
|
78
|
+
added: 0,
|
|
79
|
+
count: 0,
|
|
80
|
+
message: error.message,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { DEFAULT_GEMINI_PROFILES, ensureGeminiProfiles };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function nudge(message) {
|
|
2
|
+
process.stdout.write(JSON.stringify({
|
|
3
|
+
hookSpecificOutput: {
|
|
4
|
+
hookEventName: "PreToolUse",
|
|
5
|
+
additionalContext: message,
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deny(reason) {
|
|
12
|
+
process.stderr.write(reason);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const VALID_MCP_ROUTES = new Set(["codex", "gemini", "claude"]);
|
|
4
|
+
|
|
5
|
+
function logRuleError(message, error) {
|
|
6
|
+
if (error) {
|
|
7
|
+
console.error(`[triflux-keyword-rules] ${message}: ${error.message}`);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.error(`[triflux-keyword-rules] ${message}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizePattern(pattern) {
|
|
14
|
+
if (!pattern || typeof pattern.source !== "string") return null;
|
|
15
|
+
if (typeof pattern.flags !== "string") return null;
|
|
16
|
+
return { source: pattern.source, flags: pattern.flags };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeState(state) {
|
|
20
|
+
if (state == null) return null;
|
|
21
|
+
if (typeof state !== "object") return null;
|
|
22
|
+
if (typeof state.activate !== "boolean") return null;
|
|
23
|
+
if (typeof state.name !== "string" || !state.name.trim()) return null;
|
|
24
|
+
return { activate: state.activate, name: state.name.trim() };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeRule(rule) {
|
|
28
|
+
if (!rule || typeof rule !== "object") return null;
|
|
29
|
+
if (typeof rule.id !== "string" || !rule.id.trim()) return null;
|
|
30
|
+
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) return null;
|
|
31
|
+
if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority)) return null;
|
|
32
|
+
|
|
33
|
+
const patterns = rule.patterns.map(normalizePattern).filter(Boolean);
|
|
34
|
+
if (patterns.length === 0) return null;
|
|
35
|
+
|
|
36
|
+
const skill = typeof rule.skill === "string" && rule.skill.trim() ? rule.skill.trim() : null;
|
|
37
|
+
const action = typeof rule.action === "string" && rule.action.trim() ? rule.action.trim() : null;
|
|
38
|
+
const mcpRoute = typeof rule.mcp_route === "string" && VALID_MCP_ROUTES.has(rule.mcp_route)
|
|
39
|
+
? rule.mcp_route
|
|
40
|
+
: null;
|
|
41
|
+
|
|
42
|
+
if (!skill && !mcpRoute && !action) return null;
|
|
43
|
+
|
|
44
|
+
const supersedes = Array.isArray(rule.supersedes)
|
|
45
|
+
? rule.supersedes.filter((id) => typeof id === "string" && id.trim()).map((id) => id.trim())
|
|
46
|
+
: [];
|
|
47
|
+
|
|
48
|
+
const state = normalizeState(rule.state);
|
|
49
|
+
if (rule.state != null && state == null) return null;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: rule.id.trim(),
|
|
53
|
+
patterns,
|
|
54
|
+
skill,
|
|
55
|
+
action,
|
|
56
|
+
priority: rule.priority,
|
|
57
|
+
supersedes,
|
|
58
|
+
exclusive: rule.exclusive === true,
|
|
59
|
+
state,
|
|
60
|
+
mcp_route: mcpRoute
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 외부 JSON 규칙 로드 + 스키마 검증
|
|
65
|
+
export function loadRules(rulesPath) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(rulesPath, "utf8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (!parsed || !Array.isArray(parsed.rules)) {
|
|
70
|
+
logRuleError(`규칙 형식이 올바르지 않습니다: ${rulesPath}`);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalized = parsed.rules.map(normalizeRule).filter(Boolean);
|
|
75
|
+
return normalized;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logRuleError(`규칙 파일을 읽을 수 없습니다: ${rulesPath}`, error);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// pattern.source / flags를 RegExp로 컴파일
|
|
83
|
+
export function compileRules(rules) {
|
|
84
|
+
return rules.map((rule) => {
|
|
85
|
+
try {
|
|
86
|
+
return { ...rule, compiledPatterns: rule.patterns.map((p) => new RegExp(p.source, p.flags)) };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logRuleError(`정규식 컴파일 실패: ${rule.id}`, error);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 입력 텍스트에서 매칭된 규칙 목록 반환
|
|
95
|
+
export function matchRules(compiledRules, cleanText) {
|
|
96
|
+
if (!Array.isArray(compiledRules) || typeof cleanText !== "string" || !cleanText) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const matches = [];
|
|
101
|
+
|
|
102
|
+
for (const rule of compiledRules) {
|
|
103
|
+
if (!Array.isArray(rule.compiledPatterns) || rule.compiledPatterns.length === 0) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const matched = rule.compiledPatterns.some((pattern) => {
|
|
108
|
+
pattern.lastIndex = 0;
|
|
109
|
+
return pattern.test(cleanText);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!matched) continue;
|
|
113
|
+
|
|
114
|
+
matches.push({
|
|
115
|
+
id: rule.id,
|
|
116
|
+
skill: rule.skill,
|
|
117
|
+
action: rule.action,
|
|
118
|
+
priority: rule.priority,
|
|
119
|
+
supersedes: rule.supersedes || [],
|
|
120
|
+
exclusive: rule.exclusive === true,
|
|
121
|
+
state: rule.state || null,
|
|
122
|
+
mcp_route: rule.mcp_route || null
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return matches;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// priority 정렬 + supersedes + exclusive 처리
|
|
130
|
+
export function resolveConflicts(matches) {
|
|
131
|
+
try {
|
|
132
|
+
if (!Array.isArray(matches) || matches.length === 0) return [];
|
|
133
|
+
|
|
134
|
+
const sorted = [...matches].sort((a, b) => {
|
|
135
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
136
|
+
return String(a.id).localeCompare(String(b.id));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const deduped = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const match of sorted) {
|
|
142
|
+
if (seen.has(match.id)) continue;
|
|
143
|
+
deduped.push(match);
|
|
144
|
+
seen.add(match.id);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const superseded = new Set();
|
|
148
|
+
const resolved = [];
|
|
149
|
+
|
|
150
|
+
for (const match of deduped) {
|
|
151
|
+
if (superseded.has(match.id)) continue;
|
|
152
|
+
resolved.push(match);
|
|
153
|
+
for (const targetId of match.supersedes || []) {
|
|
154
|
+
superseded.add(targetId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const exclusiveMatch = resolved.find((match) => match.exclusive === true);
|
|
159
|
+
if (exclusiveMatch) return [exclusiveMatch];
|
|
160
|
+
|
|
161
|
+
return resolved;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logRuleError("규칙 충돌 해결 실패", error);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|