@triflux/remote 10.0.1 → 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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- 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 +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- 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,269 @@
|
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { whichCommand, whichCommandAsync } from "@triflux/core/hub/platform.mjs";
|
|
8
|
+
|
|
9
|
+
const HUB_DEFAULT_PORT = 27888;
|
|
10
|
+
const DEFAULT_STATUS_URL = "http://127.0.0.1:27888/status";
|
|
11
|
+
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
12
|
+
const CLI_PROBE_CACHE = new Map();
|
|
13
|
+
const CLI_PROBE_PROMISES = new Map();
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const DEFAULT_PKG_ROOT = join(dirname(__filename), "..", "..");
|
|
17
|
+
|
|
18
|
+
function sleepSync(ms) {
|
|
19
|
+
Atomics.wait(_sab, 0, 0, ms);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fetchHubStatus({
|
|
23
|
+
execSyncFn = execSync,
|
|
24
|
+
statusUrl = resolveDefaultStatusUrl(),
|
|
25
|
+
timeout = 3000,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const response = execSyncFn(`curl -sf ${statusUrl}`, {
|
|
28
|
+
timeout,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
const data = JSON.parse(response);
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
state: data?.hub?.state || "unknown",
|
|
36
|
+
pid: data?.pid,
|
|
37
|
+
};
|
|
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
|
+
|
|
47
|
+
function normalizeCliName(name) {
|
|
48
|
+
return String(name ?? "").trim() || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toCliResult(path) {
|
|
52
|
+
return path ? { ok: true, path } : { ok: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cloneCliResult(result) {
|
|
56
|
+
return result?.ok ? { ...result } : { ok: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readCachedCliResult(name) {
|
|
60
|
+
const cached = CLI_PROBE_CACHE.get(name);
|
|
61
|
+
return cached ? cloneCliResult(cached) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function storeCliResult(name, result) {
|
|
65
|
+
const snapshot = cloneCliResult(result);
|
|
66
|
+
CLI_PROBE_CACHE.set(name, snapshot);
|
|
67
|
+
return cloneCliResult(snapshot);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildCliProbeOptions(options = {}) {
|
|
71
|
+
return {
|
|
72
|
+
timeout: options.timeout ?? 2000,
|
|
73
|
+
env: options.env,
|
|
74
|
+
cwd: options.cwd,
|
|
75
|
+
platform: options.platform,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeCliNames(names) {
|
|
80
|
+
return [...new Set((names || []).map(normalizeCliName).filter(Boolean))];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mapCliResults(names, results) {
|
|
84
|
+
return names.reduce(
|
|
85
|
+
(acc, name, index) => ({
|
|
86
|
+
...acc,
|
|
87
|
+
[name]: results[index],
|
|
88
|
+
}),
|
|
89
|
+
{},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveCliProbe(name, options = {}) {
|
|
94
|
+
const path = await (options.whichCommandAsyncFn || whichCommandAsync)(name, {
|
|
95
|
+
...buildCliProbeOptions(options),
|
|
96
|
+
execFileFn: options.execFileFn,
|
|
97
|
+
});
|
|
98
|
+
return toCliResult(path);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function checkCli(name, options = {}) {
|
|
102
|
+
const cliName = normalizeCliName(name);
|
|
103
|
+
if (!cliName) return { ok: false };
|
|
104
|
+
|
|
105
|
+
const cached = readCachedCliResult(cliName);
|
|
106
|
+
if (cached) return cached;
|
|
107
|
+
|
|
108
|
+
const pending = CLI_PROBE_PROMISES.get(cliName);
|
|
109
|
+
if (pending) return pending.then(cloneCliResult);
|
|
110
|
+
|
|
111
|
+
const nextProbe = resolveCliProbe(cliName, options)
|
|
112
|
+
.then((result) => storeCliResult(cliName, result))
|
|
113
|
+
.catch(() => storeCliResult(cliName, { ok: false }))
|
|
114
|
+
.finally(() => {
|
|
115
|
+
CLI_PROBE_PROMISES.delete(cliName);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
CLI_PROBE_PROMISES.set(cliName, nextProbe);
|
|
119
|
+
return nextProbe.then(cloneCliResult);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function checkCliSync(name, options = {}) {
|
|
123
|
+
const cliName = normalizeCliName(name);
|
|
124
|
+
if (!cliName) return { ok: false };
|
|
125
|
+
|
|
126
|
+
const cached = readCachedCliResult(cliName);
|
|
127
|
+
if (cached) return cached;
|
|
128
|
+
|
|
129
|
+
const path = (options.whichCommandFn || whichCommand)(
|
|
130
|
+
cliName,
|
|
131
|
+
buildCliProbeOptions(options),
|
|
132
|
+
);
|
|
133
|
+
return storeCliResult(cliName, toCliResult(path));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function probeClis(names, options = {}) {
|
|
137
|
+
const cliNames = normalizeCliNames(names);
|
|
138
|
+
const results = await Promise.all(
|
|
139
|
+
cliNames.map((name) => checkCli(name, options)),
|
|
140
|
+
);
|
|
141
|
+
return mapCliResults(cliNames, results);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resetCliProbeCache() {
|
|
145
|
+
CLI_PROBE_CACHE.clear();
|
|
146
|
+
CLI_PROBE_PROMISES.clear();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function detectCodexAuthState({
|
|
150
|
+
homeDir = homedir(),
|
|
151
|
+
existsSyncFn = existsSync,
|
|
152
|
+
readFileSyncFn = readFileSync,
|
|
153
|
+
} = {}) {
|
|
154
|
+
try {
|
|
155
|
+
const authPath = join(homeDir, ".codex", "auth.json");
|
|
156
|
+
if (!existsSyncFn(authPath))
|
|
157
|
+
return { plan: "unknown", source: "no_auth", fingerprint: "no_auth" };
|
|
158
|
+
|
|
159
|
+
const auth = JSON.parse(readFileSyncFn(authPath, "utf8"));
|
|
160
|
+
if (auth.auth_mode !== "chatgpt") {
|
|
161
|
+
const fingerprint = createHash("sha256")
|
|
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
|
+
)
|
|
168
|
+
.digest("hex");
|
|
169
|
+
return { plan: "api", source: "api_key", fingerprint };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const token = auth.tokens?.id_token || auth.tokens?.access_token;
|
|
173
|
+
if (!token) {
|
|
174
|
+
return {
|
|
175
|
+
plan: "unknown",
|
|
176
|
+
source: "no_token",
|
|
177
|
+
fingerprint: createHash("sha256")
|
|
178
|
+
.update(
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
auth_mode: auth.auth_mode || "chatgpt",
|
|
181
|
+
token: null,
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
.digest("hex"),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
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";
|
|
193
|
+
const fingerprint = createHash("sha256")
|
|
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
|
+
)
|
|
202
|
+
.digest("hex");
|
|
203
|
+
return { plan, source: "jwt", fingerprint };
|
|
204
|
+
} catch {
|
|
205
|
+
return { plan: "unknown", source: "error", fingerprint: "error" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function detectCodexPlan(options = {}) {
|
|
210
|
+
const { plan, source } = detectCodexAuthState(options);
|
|
211
|
+
return { plan, source };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function checkHub({
|
|
215
|
+
pkgRoot = DEFAULT_PKG_ROOT,
|
|
216
|
+
statusUrl = resolveDefaultStatusUrl(),
|
|
217
|
+
restart = true,
|
|
218
|
+
requestTimeoutMs = 3000,
|
|
219
|
+
pollAttempts = 8,
|
|
220
|
+
pollIntervalMs = 500,
|
|
221
|
+
execSyncFn = execSync,
|
|
222
|
+
spawnFn = spawn,
|
|
223
|
+
existsSyncFn = existsSync,
|
|
224
|
+
sleepSyncFn = sleepSync,
|
|
225
|
+
} = {}) {
|
|
226
|
+
try {
|
|
227
|
+
return fetchHubStatus({
|
|
228
|
+
execSyncFn,
|
|
229
|
+
statusUrl,
|
|
230
|
+
timeout: requestTimeoutMs,
|
|
231
|
+
});
|
|
232
|
+
} catch {}
|
|
233
|
+
|
|
234
|
+
if (!restart) return { ok: false, state: "unreachable", restart: "disabled" };
|
|
235
|
+
|
|
236
|
+
const serverPath = join(pkgRoot, "hub", "server.mjs");
|
|
237
|
+
if (!existsSyncFn(serverPath))
|
|
238
|
+
return { ok: false, state: "unreachable", restart: "no_server" };
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const child = spawnFn(process.execPath, [serverPath], {
|
|
242
|
+
env: { ...process.env, TFX_HUB_PORT: String(new URL(statusUrl).port) },
|
|
243
|
+
detached: true,
|
|
244
|
+
stdio: "ignore",
|
|
245
|
+
windowsHide: true,
|
|
246
|
+
});
|
|
247
|
+
child.unref();
|
|
248
|
+
} catch {
|
|
249
|
+
return { ok: false, state: "unreachable", restart: "spawn_failed" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < pollAttempts; i++) {
|
|
253
|
+
sleepSyncFn(pollIntervalMs);
|
|
254
|
+
try {
|
|
255
|
+
const status = fetchHubStatus({
|
|
256
|
+
execSyncFn,
|
|
257
|
+
statusUrl,
|
|
258
|
+
timeout: Math.min(requestTimeoutMs, 1000),
|
|
259
|
+
});
|
|
260
|
+
if (status.state === "healthy") {
|
|
261
|
+
return { ...status, restarted: true };
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
267
|
+
}
|
|
268
|
+
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_GEMINI_PROFILES = {
|
|
12
|
+
model: "gemini-3.1-pro-preview",
|
|
13
|
+
profiles: {
|
|
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
|
+
},
|
|
22
|
+
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
23
|
+
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
24
|
+
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PROFILE_COUNT = Object.keys(
|
|
29
|
+
DEFAULT_GEMINI_PROFILES.profiles,
|
|
30
|
+
).length;
|
|
31
|
+
|
|
32
|
+
function ensureGeminiProfiles({
|
|
33
|
+
geminiDir = join(homedir(), ".gemini"),
|
|
34
|
+
profilesPath = join(geminiDir, "triflux-profiles.json"),
|
|
35
|
+
} = {}) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
if (!existsSync(profilesPath)) {
|
|
40
|
+
writeFileSync(
|
|
41
|
+
profilesPath,
|
|
42
|
+
JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n",
|
|
43
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
44
|
+
);
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
created: true,
|
|
48
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
49
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
50
|
+
message: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let cfg;
|
|
55
|
+
try {
|
|
56
|
+
cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
|
|
57
|
+
} catch {
|
|
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
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
created: true,
|
|
69
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
70
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
71
|
+
message: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
76
|
+
if (
|
|
77
|
+
!cfg.profiles ||
|
|
78
|
+
typeof cfg.profiles !== "object" ||
|
|
79
|
+
Array.isArray(cfg.profiles)
|
|
80
|
+
)
|
|
81
|
+
cfg.profiles = {};
|
|
82
|
+
|
|
83
|
+
let added = 0;
|
|
84
|
+
for (const [name, value] of Object.entries(
|
|
85
|
+
DEFAULT_GEMINI_PROFILES.profiles,
|
|
86
|
+
)) {
|
|
87
|
+
if (!cfg.profiles[name]) {
|
|
88
|
+
cfg.profiles[name] = value;
|
|
89
|
+
added++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
93
|
+
|
|
94
|
+
if (added > 0) {
|
|
95
|
+
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
mode: 0o600,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
created: false,
|
|
104
|
+
added,
|
|
105
|
+
count: Object.keys(cfg.profiles).length,
|
|
106
|
+
message: null,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
created: false,
|
|
112
|
+
added: 0,
|
|
113
|
+
count: 0,
|
|
114
|
+
message: error.message,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { DEFAULT_GEMINI_PROFILES, ensureGeminiProfiles };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { findAllClaudeMdPaths } from "./claudemd-scanner.mjs";
|
|
5
|
+
|
|
6
|
+
function runCommand(command, cwd, executor = execSync) {
|
|
7
|
+
try {
|
|
8
|
+
const output = executor(command, {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
12
|
+
windowsHide: true,
|
|
13
|
+
});
|
|
14
|
+
return String(output || "").trimEnd();
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseStatusLines(statusText) {
|
|
21
|
+
const lines = String(statusText || "")
|
|
22
|
+
.split(/\r?\n/u)
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
const changedFiles = [];
|
|
25
|
+
const status = [];
|
|
26
|
+
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const normalized = line.trimEnd();
|
|
29
|
+
if (!normalized) continue;
|
|
30
|
+
|
|
31
|
+
const rawStatus = normalized.slice(0, 2).trim() || "??";
|
|
32
|
+
const rawPath = normalized.slice(3).trim();
|
|
33
|
+
const filePath = rawPath.includes(" -> ")
|
|
34
|
+
? rawPath.split(" -> ").at(-1)?.trim()
|
|
35
|
+
: rawPath;
|
|
36
|
+
if (!filePath) continue;
|
|
37
|
+
|
|
38
|
+
changedFiles.push(filePath);
|
|
39
|
+
status.push({ path: filePath, status: rawStatus });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
changedFiles: Array.from(new Set(changedFiles)),
|
|
44
|
+
status,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeDecisionLine(line) {
|
|
49
|
+
return String(line || "")
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/^[-*]\s+/u, "")
|
|
52
|
+
.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseDecisionFile(decisionFile) {
|
|
56
|
+
if (!decisionFile) return [];
|
|
57
|
+
const absolute = resolve(decisionFile);
|
|
58
|
+
if (!existsSync(absolute)) return [];
|
|
59
|
+
|
|
60
|
+
const raw = readFileSync(absolute, "utf8");
|
|
61
|
+
return raw
|
|
62
|
+
.split(/\r?\n/u)
|
|
63
|
+
.map(normalizeDecisionLine)
|
|
64
|
+
.filter((line) => line.length > 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeDecisions(decisions, decisionFile) {
|
|
68
|
+
const merged = [
|
|
69
|
+
...(Array.isArray(decisions) ? decisions : []),
|
|
70
|
+
...parseDecisionFile(decisionFile),
|
|
71
|
+
]
|
|
72
|
+
.map(normalizeDecisionLine)
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
|
|
75
|
+
return Array.from(new Set(merged));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatAheadBehind(value) {
|
|
79
|
+
if (!value) return null;
|
|
80
|
+
const [behindRaw, aheadRaw] = value.split(/\s+/u);
|
|
81
|
+
const behind = Number.parseInt(behindRaw, 10);
|
|
82
|
+
const ahead = Number.parseInt(aheadRaw, 10);
|
|
83
|
+
if (!Number.isFinite(behind) || !Number.isFinite(ahead)) return null;
|
|
84
|
+
return { ahead, behind };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function collectHandoffContext(options = {}) {
|
|
88
|
+
const cwd = resolve(options.cwd || process.cwd());
|
|
89
|
+
const executor =
|
|
90
|
+
typeof options.commandRunner === "function"
|
|
91
|
+
? options.commandRunner
|
|
92
|
+
: execSync;
|
|
93
|
+
const target = options.target === "local" ? "local" : "remote";
|
|
94
|
+
const decisions = normalizeDecisions(options.decisions, options.decisionFile);
|
|
95
|
+
const generatedAt = options.generatedAt || new Date().toISOString();
|
|
96
|
+
const claudeMdPaths = Array.isArray(options.claudeMdPaths)
|
|
97
|
+
? options.claudeMdPaths
|
|
98
|
+
: findAllClaudeMdPaths({
|
|
99
|
+
cwd,
|
|
100
|
+
homeDir: options.homeDir,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const gitRoot = runCommand("git rev-parse --show-toplevel", cwd, executor);
|
|
104
|
+
const branch =
|
|
105
|
+
runCommand("git rev-parse --abbrev-ref HEAD", cwd, executor) || null;
|
|
106
|
+
const shortStatus = runCommand("git status --short", cwd, executor);
|
|
107
|
+
const diffStat = runCommand("git diff --stat --no-color", cwd, executor);
|
|
108
|
+
const upstreamRaw = runCommand(
|
|
109
|
+
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
110
|
+
cwd,
|
|
111
|
+
executor,
|
|
112
|
+
);
|
|
113
|
+
const parsedStatus = parseStatusLines(shortStatus);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
generatedAt,
|
|
117
|
+
target,
|
|
118
|
+
cwd,
|
|
119
|
+
gitRoot: gitRoot || null,
|
|
120
|
+
repository: gitRoot ? basename(gitRoot) : basename(cwd),
|
|
121
|
+
branch,
|
|
122
|
+
upstream: formatAheadBehind(upstreamRaw),
|
|
123
|
+
changedFiles: parsedStatus.changedFiles,
|
|
124
|
+
fileStatus: parsedStatus.status,
|
|
125
|
+
diffStat,
|
|
126
|
+
decisions,
|
|
127
|
+
claudeMdPaths,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildHandoffPrompt(context) {
|
|
132
|
+
const safeContext = context || collectHandoffContext();
|
|
133
|
+
const branch = safeContext.branch || "unknown";
|
|
134
|
+
const upstream = safeContext.upstream
|
|
135
|
+
? `ahead ${safeContext.upstream.ahead}, behind ${safeContext.upstream.behind}`
|
|
136
|
+
: "unknown";
|
|
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 없음";
|
|
150
|
+
const diffStat = safeContext.diffStat || "(diff stat 없음)";
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
"## TFX Remote Handoff",
|
|
154
|
+
`- generated_at: ${safeContext.generatedAt}`,
|
|
155
|
+
`- target: ${safeContext.target}`,
|
|
156
|
+
`- repository: ${safeContext.repository}`,
|
|
157
|
+
`- branch: ${branch} (${upstream})`,
|
|
158
|
+
`- cwd: ${safeContext.cwd}`,
|
|
159
|
+
"",
|
|
160
|
+
"### 변경 파일",
|
|
161
|
+
changedFiles,
|
|
162
|
+
"",
|
|
163
|
+
"### 변경 요약 (git diff --stat)",
|
|
164
|
+
"```",
|
|
165
|
+
diffStat,
|
|
166
|
+
"```",
|
|
167
|
+
"",
|
|
168
|
+
"### 결정사항",
|
|
169
|
+
decisions,
|
|
170
|
+
"",
|
|
171
|
+
"### CLAUDE.md 참조",
|
|
172
|
+
claudeMdList,
|
|
173
|
+
"",
|
|
174
|
+
"### 다음 세션 지시",
|
|
175
|
+
"- 위 변경사항을 먼저 검토하고 누락된 테스트를 확인하세요.",
|
|
176
|
+
"- 필요 시 CLAUDE.md 지침을 재확인한 뒤 작업을 이어가세요.",
|
|
177
|
+
"- 작업 완료 후 변경 파일과 검증 결과를 요약하세요.",
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function serializeHandoff(options = {}) {
|
|
182
|
+
const context = collectHandoffContext(options);
|
|
183
|
+
return {
|
|
184
|
+
...context,
|
|
185
|
+
prompt: buildHandoffPrompt(context),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function nudge(message) {
|
|
2
|
+
process.stdout.write(
|
|
3
|
+
JSON.stringify({
|
|
4
|
+
hookSpecificOutput: {
|
|
5
|
+
hookEventName: "PreToolUse",
|
|
6
|
+
additionalContext: message,
|
|
7
|
+
},
|
|
8
|
+
}),
|
|
9
|
+
);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deny(reason) {
|
|
14
|
+
process.stderr.write(reason);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
@@ -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
|
+
}
|