@triflux/core 10.0.3 → 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.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +47 -7
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +279 -33
  7. package/hooks/hook-registry.json +104 -7
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +308 -18
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +832 -291
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -3
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -17
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +171 -39
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +303 -107
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +116 -58
  75. package/hub/research.mjs +34 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +129 -63
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +76 -72
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +393 -249
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1282 -116
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +72 -12
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +36 -11
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. 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 { execSync, spawn } from "node:child_process";
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 = DEFAULT_STATUS_URL,
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((acc, name, index) => ({
77
- ...acc,
78
- [name]: results[index],
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)(cliName, buildCliProbeOptions(options));
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(cliNames.map((name) => checkCli(name, options)));
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)) return { plan: "unknown", source: "no_auth", fingerprint: "no_auth" };
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(JSON.stringify({
146
- auth_mode: auth.auth_mode || "api_key",
147
- has_api_key: Boolean(auth.api_key || auth.apiKey),
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(JSON.stringify({ auth_mode: auth.auth_mode || "chatgpt", token: null }))
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(Buffer.from(token.split(".")[1], "base64url").toString());
165
- const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
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(JSON.stringify({
168
- auth_mode: auth.auth_mode || "chatgpt",
169
- plan,
170
- sub: payload?.sub || null,
171
- exp: payload?.exp || null,
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 = DEFAULT_STATUS_URL,
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)) return { ok: false, state: "unreachable", restart: "no_server" };
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 { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
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: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
9
- flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
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(DEFAULT_GEMINI_PROFILES.profiles).length;
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(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
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 { copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`); } catch {}
41
- writeFileSync(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
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 (!cfg.profiles || typeof cfg.profiles !== "object" || Array.isArray(cfg.profiles)) cfg.profiles = {};
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(DEFAULT_GEMINI_PROFILES.profiles)) {
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", { encoding: "utf8", mode: 0o600 });
95
+ writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
96
+ encoding: "utf8",
97
+ mode: 0o600,
98
+ });
65
99
  }
66
100
 
67
101
  return {
@@ -18,7 +18,9 @@ function runCommand(command, cwd, executor = execSync) {
18
18
  }
19
19
 
20
20
  function parseStatusLines(statusText) {
21
- const lines = String(statusText || "").split(/\r?\n/u).filter(Boolean);
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(" -> ") ? rawPath.split(" -> ").at(-1)?.trim() : rawPath;
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 = typeof options.commandRunner === "function" ? options.commandRunner : execSync;
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
- cwd,
93
- homeDir: options.homeDir,
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 = runCommand("git rev-parse --abbrev-ref HEAD", cwd, executor) || null;
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("git rev-list --left-right --count @{upstream}...HEAD", cwd, executor);
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 = safeContext.changedFiles.length > 0
126
- ? safeContext.changedFiles.map((file) => `- ${file}`).join("\n")
127
- : "- 변경 파일 없음";
128
- const decisions = safeContext.decisions.length > 0
129
- ? safeContext.decisions.map((decision) => `- ${decision}`).join("\n")
130
- : "- 명시된 결정사항 없음";
131
- const claudeMdList = Array.isArray(safeContext.claudeMdPaths) && safeContext.claudeMdPaths.length > 0
132
- ? safeContext.claudeMdPaths.map((path) => `- ${path}`).join("\n")
133
- : "- 자동 탐지된 CLAUDE.md 없음";
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(JSON.stringify({
3
- hookSpecificOutput: {
4
- hookEventName: "PreToolUse",
5
- additionalContext: message,
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
+ }