@triflux/core 10.1.0 → 10.25.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 +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function commandExists(name) {
|
|
4
|
+
try {
|
|
5
|
+
execFileSync("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", name], {
|
|
6
|
+
stdio: "ignore",
|
|
7
|
+
timeout: 2000,
|
|
8
|
+
});
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function inspectMacTimeoutDependency({
|
|
16
|
+
platform = process.platform,
|
|
17
|
+
commandExists: exists = commandExists,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (platform !== "darwin") {
|
|
20
|
+
return { ok: true, status: "skipped", platform };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (exists("gtimeout")) {
|
|
24
|
+
return { ok: true, status: "ok", platform, provider: "gtimeout" };
|
|
25
|
+
}
|
|
26
|
+
if (exists("timeout")) {
|
|
27
|
+
return { ok: true, status: "ok", platform, provider: "timeout" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
status: "missing",
|
|
33
|
+
platform,
|
|
34
|
+
fix: "brew install coreutils",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function addPluginRootFallbackToCommand(command, pluginRoot) {
|
|
39
|
+
if (typeof command !== "string") return command;
|
|
40
|
+
const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
|
|
41
|
+
if (!fallbackRoot) return command;
|
|
42
|
+
return command
|
|
43
|
+
.replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
|
|
44
|
+
.replaceAll(
|
|
45
|
+
"${CLAUDE_PLUGIN_ROOT}",
|
|
46
|
+
`\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
|
|
47
|
+
)
|
|
48
|
+
.replace(
|
|
49
|
+
/(^|[\s"'])\/(hooks|scripts)\//g,
|
|
50
|
+
`$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasBarePluginRootReference(command) {
|
|
55
|
+
if (typeof command !== "string") return false;
|
|
56
|
+
return (
|
|
57
|
+
command.includes("${PLUGIN_ROOT}/") ||
|
|
58
|
+
command.includes("${CLAUDE_PLUGIN_ROOT}/")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasCollapsedRootHookPath(command) {
|
|
63
|
+
if (typeof command !== "string") return false;
|
|
64
|
+
return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function findPluginRootHookIssues(settings) {
|
|
68
|
+
const hooksByEvent =
|
|
69
|
+
settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
for (const [event, entries] of Object.entries(hooksByEvent)) {
|
|
73
|
+
if (!Array.isArray(entries)) continue;
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
76
|
+
for (const hook of hooks) {
|
|
77
|
+
const command = hook?.command;
|
|
78
|
+
if (
|
|
79
|
+
!hasBarePluginRootReference(command) &&
|
|
80
|
+
!hasCollapsedRootHookPath(command)
|
|
81
|
+
) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
issues.push({
|
|
85
|
+
event,
|
|
86
|
+
matcher: entry?.matcher || "*",
|
|
87
|
+
command,
|
|
88
|
+
reason: hasCollapsedRootHookPath(command)
|
|
89
|
+
? "collapsed-root-path"
|
|
90
|
+
: "missing-plugin-root-fallback",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return issues;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
|
|
100
|
+
if (!settings?.hooks || typeof settings.hooks !== "object") {
|
|
101
|
+
return { changed: false, count: 0, settings };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
106
|
+
if (!Array.isArray(entries)) continue;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
109
|
+
for (const hook of hooks) {
|
|
110
|
+
if (typeof hook?.command !== "string") continue;
|
|
111
|
+
const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
|
|
112
|
+
if (next !== hook.command) {
|
|
113
|
+
hook.command = next;
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { changed: count > 0, count, settings };
|
|
121
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/lib/dynamic-route-cli.mjs
|
|
3
|
+
//
|
|
4
|
+
// Phase 1 Wire-up 3 helper — tfx-route.sh 등 sh 경로에서 dynamic routing
|
|
5
|
+
// decision 을 받아 단일 CLI 문자열 (또는 JSON) 으로 출력한다.
|
|
6
|
+
//
|
|
7
|
+
// 사용:
|
|
8
|
+
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex
|
|
9
|
+
// → "codex"
|
|
10
|
+
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --json
|
|
11
|
+
// → {"scenario":"fallback","mode":"codex-default","shards":[{"cli":"codex",...}]}
|
|
12
|
+
// node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --field scenario
|
|
13
|
+
// → "fallback"
|
|
14
|
+
//
|
|
15
|
+
// 정책:
|
|
16
|
+
// - opt-in env TRIFLUX_DYNAMIC_ROUTING=1|true 미설정 시 즉시 silent exit 0
|
|
17
|
+
// (override 없음 = stdout empty, sh caller 가 graceful no-op).
|
|
18
|
+
// - error 발생 시 stderr 에 한 줄 + exit 0 (sh caller 가 fallback path 진행).
|
|
19
|
+
|
|
20
|
+
import { createDynamicRouter } from "../../hub/dynamic-routing-engine.mjs";
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const args = {
|
|
24
|
+
taskId: `tfx-route-${process.pid}-${Date.now()}`,
|
|
25
|
+
agentHint: "codex",
|
|
26
|
+
teamSize: 1,
|
|
27
|
+
json: false,
|
|
28
|
+
field: null,
|
|
29
|
+
};
|
|
30
|
+
for (let i = 2; i < argv.length; i++) {
|
|
31
|
+
const v = argv[i];
|
|
32
|
+
switch (v) {
|
|
33
|
+
case "--task-id":
|
|
34
|
+
args.taskId = argv[++i] || args.taskId;
|
|
35
|
+
break;
|
|
36
|
+
case "--agent-hint":
|
|
37
|
+
args.agentHint = argv[++i] || args.agentHint;
|
|
38
|
+
break;
|
|
39
|
+
case "--team-size":
|
|
40
|
+
args.teamSize = Number(argv[++i]) || args.teamSize;
|
|
41
|
+
break;
|
|
42
|
+
case "--json":
|
|
43
|
+
args.json = true;
|
|
44
|
+
break;
|
|
45
|
+
case "--field":
|
|
46
|
+
args.field = argv[++i] || null;
|
|
47
|
+
break;
|
|
48
|
+
case "--help":
|
|
49
|
+
case "-h":
|
|
50
|
+
process.stdout.write(
|
|
51
|
+
"Usage: dynamic-route-cli.mjs [--task-id ID] [--agent-hint CLI] [--team-size N] [--json|--field FIELD]\n",
|
|
52
|
+
);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const args = parseArgs(process.argv);
|
|
62
|
+
|
|
63
|
+
// opt-in env 미설정 시 silent no-op (sh caller graceful path)
|
|
64
|
+
const flag = process.env.TRIFLUX_DYNAMIC_ROUTING;
|
|
65
|
+
if (flag !== "1" && flag !== "true") return;
|
|
66
|
+
|
|
67
|
+
let decision;
|
|
68
|
+
try {
|
|
69
|
+
const router = createDynamicRouter();
|
|
70
|
+
decision = await router.routeRequest({
|
|
71
|
+
task_id: args.taskId,
|
|
72
|
+
team_size: args.teamSize,
|
|
73
|
+
agent_hint: args.agentHint,
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`[dynamic-route-cli] error: ${err?.message || String(err)}\n`,
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (args.json) {
|
|
83
|
+
process.stdout.write(JSON.stringify(decision));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (args.field) {
|
|
88
|
+
const value = decision?.[args.field];
|
|
89
|
+
if (value !== undefined && value !== null) {
|
|
90
|
+
process.stdout.write(String(value));
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 기본 — shards[0].cli 만 stdout 으로 (sh caller 가 즉시 비교)
|
|
96
|
+
const cli = decision?.shards?.[0]?.cli;
|
|
97
|
+
if (cli) {
|
|
98
|
+
process.stdout.write(String(cli));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
`[dynamic-route-cli] fatal: ${err?.message || String(err)}\n`,
|
|
105
|
+
);
|
|
106
|
+
process.exit(0); // sh caller 가 정상 fallback 하도록 always 0
|
|
107
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
1
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
7
|
import { whichCommand, whichCommandAsync } from "../../hub/platform.mjs";
|
|
8
8
|
|
|
9
|
+
const HUB_DEFAULT_PORT = 27888;
|
|
9
10
|
const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
|
|
10
11
|
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
11
12
|
const CLI_PROBE_CACHE = new Map();
|
|
@@ -20,7 +21,7 @@ function sleepSync(ms) {
|
|
|
20
21
|
|
|
21
22
|
function fetchHubStatus({
|
|
22
23
|
execSyncFn = execSync,
|
|
23
|
-
statusUrl =
|
|
24
|
+
statusUrl = resolveDefaultStatusUrl(),
|
|
24
25
|
timeout = 3000,
|
|
25
26
|
} = {}) {
|
|
26
27
|
const response = execSyncFn(`curl -sf ${statusUrl}`, {
|
|
@@ -36,6 +37,13 @@ function fetchHubStatus({
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export function resolveDefaultStatusUrl(env = process.env) {
|
|
41
|
+
const envPort = Number.parseInt(String(env?.TFX_HUB_PORT ?? ""), 10);
|
|
42
|
+
const port =
|
|
43
|
+
Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
|
|
44
|
+
return `http://127.0.0.1:${port}/status`;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function normalizeCliName(name) {
|
|
40
48
|
return String(name ?? "").trim() || null;
|
|
41
49
|
}
|
|
@@ -73,10 +81,13 @@ function normalizeCliNames(names) {
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
function mapCliResults(names, results) {
|
|
76
|
-
return names.reduce(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
return names.reduce(
|
|
85
|
+
(acc, name, index) => ({
|
|
86
|
+
...acc,
|
|
87
|
+
[name]: results[index],
|
|
88
|
+
}),
|
|
89
|
+
{},
|
|
90
|
+
);
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
async function resolveCliProbe(name, options = {}) {
|
|
@@ -115,13 +126,18 @@ export function checkCliSync(name, options = {}) {
|
|
|
115
126
|
const cached = readCachedCliResult(cliName);
|
|
116
127
|
if (cached) return cached;
|
|
117
128
|
|
|
118
|
-
const path = (options.whichCommandFn || whichCommand)(
|
|
129
|
+
const path = (options.whichCommandFn || whichCommand)(
|
|
130
|
+
cliName,
|
|
131
|
+
buildCliProbeOptions(options),
|
|
132
|
+
);
|
|
119
133
|
return storeCliResult(cliName, toCliResult(path));
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
export async function probeClis(names, options = {}) {
|
|
123
137
|
const cliNames = normalizeCliNames(names);
|
|
124
|
-
const results = await Promise.all(
|
|
138
|
+
const results = await Promise.all(
|
|
139
|
+
cliNames.map((name) => checkCli(name, options)),
|
|
140
|
+
);
|
|
125
141
|
return mapCliResults(cliNames, results);
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -137,15 +153,18 @@ export function detectCodexAuthState({
|
|
|
137
153
|
} = {}) {
|
|
138
154
|
try {
|
|
139
155
|
const authPath = join(homeDir, ".codex", "auth.json");
|
|
140
|
-
if (!existsSyncFn(authPath))
|
|
156
|
+
if (!existsSyncFn(authPath))
|
|
157
|
+
return { plan: "unknown", source: "no_auth", fingerprint: "no_auth" };
|
|
141
158
|
|
|
142
159
|
const auth = JSON.parse(readFileSyncFn(authPath, "utf8"));
|
|
143
160
|
if (auth.auth_mode !== "chatgpt") {
|
|
144
161
|
const fingerprint = createHash("sha256")
|
|
145
|
-
.update(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
.update(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
auth_mode: auth.auth_mode || "api_key",
|
|
165
|
+
has_api_key: Boolean(auth.api_key || auth.apiKey),
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
149
168
|
.digest("hex");
|
|
150
169
|
return { plan: "api", source: "api_key", fingerprint };
|
|
151
170
|
}
|
|
@@ -156,20 +175,30 @@ export function detectCodexAuthState({
|
|
|
156
175
|
plan: "unknown",
|
|
157
176
|
source: "no_token",
|
|
158
177
|
fingerprint: createHash("sha256")
|
|
159
|
-
.update(
|
|
178
|
+
.update(
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
181
|
+
token: null,
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
160
184
|
.digest("hex"),
|
|
161
185
|
};
|
|
162
186
|
}
|
|
163
187
|
|
|
164
|
-
const payload = JSON.parse(
|
|
165
|
-
|
|
188
|
+
const payload = JSON.parse(
|
|
189
|
+
Buffer.from(token.split(".")[1], "base64url").toString(),
|
|
190
|
+
);
|
|
191
|
+
const plan =
|
|
192
|
+
payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
|
|
166
193
|
const fingerprint = createHash("sha256")
|
|
167
|
-
.update(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
.update(
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
197
|
+
plan,
|
|
198
|
+
sub: payload?.sub || null,
|
|
199
|
+
exp: payload?.exp || null,
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
173
202
|
.digest("hex");
|
|
174
203
|
return { plan, source: "jwt", fingerprint };
|
|
175
204
|
} catch {
|
|
@@ -184,7 +213,7 @@ export function detectCodexPlan(options = {}) {
|
|
|
184
213
|
|
|
185
214
|
export function checkHub({
|
|
186
215
|
pkgRoot = DEFAULT_PKG_ROOT,
|
|
187
|
-
statusUrl =
|
|
216
|
+
statusUrl = resolveDefaultStatusUrl(),
|
|
188
217
|
restart = true,
|
|
189
218
|
requestTimeoutMs = 3000,
|
|
190
219
|
pollAttempts = 8,
|
|
@@ -205,10 +234,12 @@ export function checkHub({
|
|
|
205
234
|
if (!restart) return { ok: false, state: "unreachable", restart: "disabled" };
|
|
206
235
|
|
|
207
236
|
const serverPath = join(pkgRoot, "hub", "server.mjs");
|
|
208
|
-
if (!existsSyncFn(serverPath))
|
|
237
|
+
if (!existsSyncFn(serverPath))
|
|
238
|
+
return { ok: false, state: "unreachable", restart: "no_server" };
|
|
209
239
|
|
|
210
240
|
try {
|
|
211
241
|
const child = spawnFn(process.execPath, [serverPath], {
|
|
242
|
+
env: { ...process.env, TFX_HUB_PORT: String(new URL(statusUrl).port) },
|
|
212
243
|
detached: true,
|
|
213
244
|
stdio: "ignore",
|
|
214
245
|
windowsHide: true,
|
|
@@ -235,7 +266,4 @@ export function checkHub({
|
|
|
235
266
|
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
236
267
|
}
|
|
237
268
|
|
|
238
|
-
export {
|
|
239
|
-
DEFAULT_PKG_ROOT,
|
|
240
|
-
DEFAULT_STATUS_URL,
|
|
241
|
-
};
|
|
269
|
+
export { DEFAULT_PKG_ROOT, DEFAULT_STATUS_URL };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function buildPreflightEnv(env = process.env) {
|
|
2
|
+
return {
|
|
3
|
+
...env,
|
|
4
|
+
GIT_TERMINAL_PROMPT: env.GIT_TERMINAL_PROMPT ?? "0",
|
|
5
|
+
GIT_ASKPASS: env.GIT_ASKPASS ?? "false",
|
|
6
|
+
npm_config_yes: env.npm_config_yes ?? "true",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function shouldWarnGhAuth(env = process.env, checks = {}) {
|
|
11
|
+
if (!checks.ghExists) return false;
|
|
12
|
+
if (env.GH_TOKEN || env.GITHUB_TOKEN) return false;
|
|
13
|
+
return checks.ghAuthenticated === false;
|
|
14
|
+
}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { homedir } from "node:os";
|
|
3
9
|
import { join } from "node:path";
|
|
4
10
|
|
|
5
11
|
const DEFAULT_GEMINI_PROFILES = {
|
|
6
12
|
model: "gemini-3.1-pro-preview",
|
|
7
13
|
profiles: {
|
|
8
|
-
pro31: {
|
|
9
|
-
|
|
14
|
+
pro31: {
|
|
15
|
+
model: "gemini-3.1-pro-preview",
|
|
16
|
+
hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)",
|
|
17
|
+
},
|
|
18
|
+
flash3: {
|
|
19
|
+
model: "gemini-3-flash-preview",
|
|
20
|
+
hint: "3.0 Flash — 빠른 응답, 비용 효율",
|
|
21
|
+
},
|
|
10
22
|
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
11
23
|
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
12
24
|
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
13
25
|
},
|
|
14
26
|
};
|
|
15
27
|
|
|
16
|
-
const DEFAULT_PROFILE_COUNT = Object.keys(
|
|
28
|
+
const DEFAULT_PROFILE_COUNT = Object.keys(
|
|
29
|
+
DEFAULT_GEMINI_PROFILES.profiles,
|
|
30
|
+
).length;
|
|
17
31
|
|
|
18
32
|
function ensureGeminiProfiles({
|
|
19
33
|
geminiDir = join(homedir(), ".gemini"),
|
|
@@ -23,7 +37,11 @@ function ensureGeminiProfiles({
|
|
|
23
37
|
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
24
38
|
|
|
25
39
|
if (!existsSync(profilesPath)) {
|
|
26
|
-
writeFileSync(
|
|
40
|
+
writeFileSync(
|
|
41
|
+
profilesPath,
|
|
42
|
+
JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n",
|
|
43
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
44
|
+
);
|
|
27
45
|
return {
|
|
28
46
|
ok: true,
|
|
29
47
|
created: true,
|
|
@@ -37,8 +55,14 @@ function ensureGeminiProfiles({
|
|
|
37
55
|
try {
|
|
38
56
|
cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
|
|
39
57
|
} catch {
|
|
40
|
-
try {
|
|
41
|
-
|
|
58
|
+
try {
|
|
59
|
+
copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`);
|
|
60
|
+
} catch {}
|
|
61
|
+
writeFileSync(
|
|
62
|
+
profilesPath,
|
|
63
|
+
JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n",
|
|
64
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
65
|
+
);
|
|
42
66
|
return {
|
|
43
67
|
ok: true,
|
|
44
68
|
created: true,
|
|
@@ -49,10 +73,17 @@ function ensureGeminiProfiles({
|
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
52
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
!cfg.profiles ||
|
|
78
|
+
typeof cfg.profiles !== "object" ||
|
|
79
|
+
Array.isArray(cfg.profiles)
|
|
80
|
+
)
|
|
81
|
+
cfg.profiles = {};
|
|
53
82
|
|
|
54
83
|
let added = 0;
|
|
55
|
-
for (const [name, value] of Object.entries(
|
|
84
|
+
for (const [name, value] of Object.entries(
|
|
85
|
+
DEFAULT_GEMINI_PROFILES.profiles,
|
|
86
|
+
)) {
|
|
56
87
|
if (!cfg.profiles[name]) {
|
|
57
88
|
cfg.profiles[name] = value;
|
|
58
89
|
added++;
|
|
@@ -61,7 +92,10 @@ function ensureGeminiProfiles({
|
|
|
61
92
|
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
62
93
|
|
|
63
94
|
if (added > 0) {
|
|
64
|
-
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
|
|
95
|
+
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
mode: 0o600,
|
|
98
|
+
});
|
|
65
99
|
}
|
|
66
100
|
|
|
67
101
|
return {
|
package/scripts/lib/handoff.mjs
CHANGED
|
@@ -18,7 +18,9 @@ function runCommand(command, cwd, executor = execSync) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function parseStatusLines(statusText) {
|
|
21
|
-
const lines = String(statusText || "")
|
|
21
|
+
const lines = String(statusText || "")
|
|
22
|
+
.split(/\r?\n/u)
|
|
23
|
+
.filter(Boolean);
|
|
22
24
|
const changedFiles = [];
|
|
23
25
|
const status = [];
|
|
24
26
|
|
|
@@ -28,7 +30,9 @@ function parseStatusLines(statusText) {
|
|
|
28
30
|
|
|
29
31
|
const rawStatus = normalized.slice(0, 2).trim() || "??";
|
|
30
32
|
const rawPath = normalized.slice(3).trim();
|
|
31
|
-
const filePath = rawPath.includes(" -> ")
|
|
33
|
+
const filePath = rawPath.includes(" -> ")
|
|
34
|
+
? rawPath.split(" -> ").at(-1)?.trim()
|
|
35
|
+
: rawPath;
|
|
32
36
|
if (!filePath) continue;
|
|
33
37
|
|
|
34
38
|
changedFiles.push(filePath);
|
|
@@ -82,22 +86,30 @@ function formatAheadBehind(value) {
|
|
|
82
86
|
|
|
83
87
|
export function collectHandoffContext(options = {}) {
|
|
84
88
|
const cwd = resolve(options.cwd || process.cwd());
|
|
85
|
-
const executor =
|
|
89
|
+
const executor =
|
|
90
|
+
typeof options.commandRunner === "function"
|
|
91
|
+
? options.commandRunner
|
|
92
|
+
: execSync;
|
|
86
93
|
const target = options.target === "local" ? "local" : "remote";
|
|
87
94
|
const decisions = normalizeDecisions(options.decisions, options.decisionFile);
|
|
88
95
|
const generatedAt = options.generatedAt || new Date().toISOString();
|
|
89
96
|
const claudeMdPaths = Array.isArray(options.claudeMdPaths)
|
|
90
97
|
? options.claudeMdPaths
|
|
91
98
|
: findAllClaudeMdPaths({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
cwd,
|
|
100
|
+
homeDir: options.homeDir,
|
|
101
|
+
});
|
|
95
102
|
|
|
96
103
|
const gitRoot = runCommand("git rev-parse --show-toplevel", cwd, executor);
|
|
97
|
-
const branch =
|
|
104
|
+
const branch =
|
|
105
|
+
runCommand("git rev-parse --abbrev-ref HEAD", cwd, executor) || null;
|
|
98
106
|
const shortStatus = runCommand("git status --short", cwd, executor);
|
|
99
107
|
const diffStat = runCommand("git diff --stat --no-color", cwd, executor);
|
|
100
|
-
const upstreamRaw = runCommand(
|
|
108
|
+
const upstreamRaw = runCommand(
|
|
109
|
+
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
110
|
+
cwd,
|
|
111
|
+
executor,
|
|
112
|
+
);
|
|
101
113
|
const parsedStatus = parseStatusLines(shortStatus);
|
|
102
114
|
|
|
103
115
|
return {
|
|
@@ -122,15 +134,19 @@ export function buildHandoffPrompt(context) {
|
|
|
122
134
|
const upstream = safeContext.upstream
|
|
123
135
|
? `ahead ${safeContext.upstream.ahead}, behind ${safeContext.upstream.behind}`
|
|
124
136
|
: "unknown";
|
|
125
|
-
const changedFiles =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
const changedFiles =
|
|
138
|
+
safeContext.changedFiles.length > 0
|
|
139
|
+
? safeContext.changedFiles.map((file) => `- ${file}`).join("\n")
|
|
140
|
+
: "- 변경 파일 없음";
|
|
141
|
+
const decisions =
|
|
142
|
+
safeContext.decisions.length > 0
|
|
143
|
+
? safeContext.decisions.map((decision) => `- ${decision}`).join("\n")
|
|
144
|
+
: "- 명시된 결정사항 없음";
|
|
145
|
+
const claudeMdList =
|
|
146
|
+
Array.isArray(safeContext.claudeMdPaths) &&
|
|
147
|
+
safeContext.claudeMdPaths.length > 0
|
|
148
|
+
? safeContext.claudeMdPaths.map((path) => `- ${path}`).join("\n")
|
|
149
|
+
: "- 자동 탐지된 CLAUDE.md 없음";
|
|
134
150
|
const diffStat = safeContext.diffStat || "(diff stat 없음)";
|
|
135
151
|
|
|
136
152
|
return [
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export function nudge(message) {
|
|
2
|
-
process.stdout.write(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
process.stdout.write(
|
|
3
|
+
JSON.stringify({
|
|
4
|
+
hookSpecificOutput: {
|
|
5
|
+
hookEventName: "PreToolUse",
|
|
6
|
+
additionalContext: message,
|
|
7
|
+
},
|
|
8
|
+
}),
|
|
9
|
+
);
|
|
8
10
|
process.exit(0);
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function parseHubPort(hubUrl = "") {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = new URL(hubUrl);
|
|
6
|
+
return parsed.port || "27888";
|
|
7
|
+
} catch {
|
|
8
|
+
return "27888";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function restartHub({
|
|
13
|
+
hubUrl = process.env.TFX_HUB_URL || "http://127.0.0.1:27888",
|
|
14
|
+
hubServer,
|
|
15
|
+
nodeBin = process.execPath,
|
|
16
|
+
spawnFn = spawn,
|
|
17
|
+
fetchFn = globalThis.fetch,
|
|
18
|
+
sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
19
|
+
attempts = 8,
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (!hubServer) return { ok: false, error: "hub_server_missing" };
|
|
22
|
+
const port = parseHubPort(hubUrl);
|
|
23
|
+
const child = spawnFn(nodeBin, [hubServer], {
|
|
24
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
25
|
+
stdio: "ignore",
|
|
26
|
+
detached: true,
|
|
27
|
+
});
|
|
28
|
+
child.unref?.();
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
31
|
+
await sleepMs(500);
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetchFn(`${hubUrl.replace(/\/$/, "")}/status`);
|
|
34
|
+
if (response?.ok) return { ok: true, pid: child.pid };
|
|
35
|
+
} catch {
|
|
36
|
+
// Keep polling until attempts are exhausted.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { ok: false, pid: child.pid };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function withHubRestart({ action, restart }) {
|
|
43
|
+
const first = await action();
|
|
44
|
+
if (first?.ok) return first;
|
|
45
|
+
const restarted = await restart();
|
|
46
|
+
if (!restarted?.ok) return first;
|
|
47
|
+
return action();
|
|
48
|
+
}
|