@triflux/core 10.34.0 → 10.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/agy-session-hook.mjs +140 -0
- package/hub/bridge.mjs +130 -70
- package/hub/team/claude-daemon-control.mjs +348 -4
- package/hud/constants.mjs +2 -0
- package/hud/providers/gemini.mjs +135 -3
- package/hud/renderers.mjs +37 -35
- package/package.json +1 -1
- package/scripts/lib/mcp-manifest.mjs +2 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { argv, exit, stdin, stdout } from "node:process";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
|
|
6
|
+
import {
|
|
7
|
+
heartbeatInteractiveSession as defaultHeartbeatInteractiveSession,
|
|
8
|
+
registerInteractiveSession as defaultRegisterInteractiveSession,
|
|
9
|
+
} from "./session-start-fast.mjs";
|
|
10
|
+
|
|
11
|
+
// hub-ensure is loaded lazily so the byte-identical packages/core mirror of this
|
|
12
|
+
// file loads cleanly. packages/core mirrors scripts/lib only (not scripts/*), so a
|
|
13
|
+
// static `../scripts/hub-ensure.mjs` import would make the core copy throw
|
|
14
|
+
// ERR_MODULE_NOT_FOUND at load time. Mirrors codex-session-hook.mjs's pattern.
|
|
15
|
+
async function defaultHubEnsureRun(stdinData) {
|
|
16
|
+
const { run } = await import(
|
|
17
|
+
new URL("../scripts/hub-ensure.mjs", import.meta.url).href
|
|
18
|
+
);
|
|
19
|
+
return run(stdinData);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parsePayload(stdinData) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = typeof stdinData === "string" ? stdinData : "";
|
|
25
|
+
return raw.trim()
|
|
26
|
+
? { ok: true, payload: JSON.parse(raw) }
|
|
27
|
+
: { ok: false, payload: {} };
|
|
28
|
+
} catch {
|
|
29
|
+
return { ok: false, payload: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Antigravity hook payloads use camelCase system metadata (conversationId,
|
|
34
|
+
// workspacePaths) rather than Codex's session_id/cwd. registerInteractiveSession
|
|
35
|
+
// and heartbeatInteractiveSession parse the Codex shape, so we adapt the agy
|
|
36
|
+
// payload into that shape before delegating. conversationId is the stable
|
|
37
|
+
// per-conversation UUID (== session identity); the first mounted workspace path
|
|
38
|
+
// is the effective cwd.
|
|
39
|
+
export function toSessionPayload(payload) {
|
|
40
|
+
const sessionId = String(payload?.conversationId || "").trim();
|
|
41
|
+
const workspacePaths = Array.isArray(payload?.workspacePaths)
|
|
42
|
+
? payload.workspacePaths
|
|
43
|
+
: [];
|
|
44
|
+
const cwd =
|
|
45
|
+
typeof workspacePaths[0] === "string" && workspacePaths[0]
|
|
46
|
+
? workspacePaths[0]
|
|
47
|
+
: process.cwd();
|
|
48
|
+
return JSON.stringify({ session_id: sessionId, cwd });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// agy has no distinct SessionStart / UserPromptSubmit events. PreInvocation
|
|
52
|
+
// fires before every model call, so the per-conversation invocationNum gates
|
|
53
|
+
// register (first call == session start) vs heartbeat (subsequent calls).
|
|
54
|
+
// An explicit argv mode (register|heartbeat) overrides, mirroring the codex hook.
|
|
55
|
+
export function normalizeMode(argvMode, payload) {
|
|
56
|
+
const direct = String(argvMode || "")
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
if (direct === "register" || direct === "heartbeat") return direct;
|
|
60
|
+
|
|
61
|
+
const invocationNum = Number(payload?.invocationNum);
|
|
62
|
+
if (Number.isFinite(invocationNum)) {
|
|
63
|
+
return invocationNum <= 1 ? "register" : "heartbeat";
|
|
64
|
+
}
|
|
65
|
+
// Unknown invocation index: register is idempotent and also ensures the hub,
|
|
66
|
+
// so it is the safe default for a first-contact payload.
|
|
67
|
+
return "register";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runAgySessionHook(stdinData, opts = {}) {
|
|
71
|
+
const output = "{}\n";
|
|
72
|
+
const parsed = parsePayload(stdinData);
|
|
73
|
+
if (!parsed.ok) {
|
|
74
|
+
if (opts.writeStdout !== false) stdout.write(output);
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sessionPayload = toSessionPayload(parsed.payload);
|
|
79
|
+
const sessionId = JSON.parse(sessionPayload).session_id;
|
|
80
|
+
// No conversation id means nothing to register; stay a silent no-op.
|
|
81
|
+
if (!sessionId) {
|
|
82
|
+
if (opts.writeStdout !== false) stdout.write(output);
|
|
83
|
+
return output;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const mode = normalizeMode(opts.argvMode ?? argv[2], parsed.payload);
|
|
87
|
+
const hubEnsureRun = opts.hubEnsureRun || defaultHubEnsureRun;
|
|
88
|
+
const registerInteractiveSession =
|
|
89
|
+
opts.registerInteractiveSession || defaultRegisterInteractiveSession;
|
|
90
|
+
const heartbeatInteractiveSession =
|
|
91
|
+
opts.heartbeatInteractiveSession || defaultHeartbeatInteractiveSession;
|
|
92
|
+
const drainPendingSynapse =
|
|
93
|
+
opts.drainPendingSynapse || defaultDrainPendingSynapse;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (mode === "register") {
|
|
97
|
+
try {
|
|
98
|
+
await hubEnsureRun(sessionPayload);
|
|
99
|
+
} catch {}
|
|
100
|
+
try {
|
|
101
|
+
registerInteractiveSession(sessionPayload);
|
|
102
|
+
} catch {}
|
|
103
|
+
try {
|
|
104
|
+
await drainPendingSynapse(1000);
|
|
105
|
+
} catch {}
|
|
106
|
+
} else if (mode === "heartbeat") {
|
|
107
|
+
try {
|
|
108
|
+
heartbeatInteractiveSession(sessionPayload);
|
|
109
|
+
} catch {}
|
|
110
|
+
try {
|
|
111
|
+
await drainPendingSynapse(500);
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// agy session hooks are observational and must never block the session.
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (opts.writeStdout !== false) {
|
|
119
|
+
stdout.write(output);
|
|
120
|
+
}
|
|
121
|
+
return output;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readStdin() {
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let data = "";
|
|
127
|
+
stdin.setEncoding("utf8");
|
|
128
|
+
stdin.on("data", (chunk) => {
|
|
129
|
+
data += chunk;
|
|
130
|
+
});
|
|
131
|
+
stdin.on("end", () => resolve(data));
|
|
132
|
+
stdin.on("error", () => resolve(data));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (argv[1] && import.meta.url === pathToFileURL(argv[1]).href) {
|
|
137
|
+
const stdinData = await readStdin();
|
|
138
|
+
await runAgySessionHook(stdinData);
|
|
139
|
+
exit(0);
|
|
140
|
+
}
|
package/hub/bridge.mjs
CHANGED
|
@@ -1186,6 +1186,36 @@ function daemonAttachErrorResult(error) {
|
|
|
1186
1186
|
};
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
|
+
function daemonAttachProbeFailureResult(probe) {
|
|
1190
|
+
return {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
text: "",
|
|
1193
|
+
raw: "",
|
|
1194
|
+
responseRaw: "",
|
|
1195
|
+
matchedCompletion: false,
|
|
1196
|
+
timedOut: false,
|
|
1197
|
+
closed: false,
|
|
1198
|
+
inputSent: false,
|
|
1199
|
+
error: probe?.error || probe?.reason || "Claude daemon unavailable",
|
|
1200
|
+
reason: probe?.reason || "daemon-unavailable",
|
|
1201
|
+
daemon: probe?.daemon ?? null,
|
|
1202
|
+
daemons: probe?.daemons ?? [],
|
|
1203
|
+
matches: probe?.matches ?? [],
|
|
1204
|
+
candidateResults: probe?.candidateResults ?? [],
|
|
1205
|
+
callerProvenance: probe?.callerProvenance ?? null,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function daemonProbeMetadata(probe) {
|
|
1210
|
+
return {
|
|
1211
|
+
daemon: probe?.daemon ?? null,
|
|
1212
|
+
daemons: probe?.daemons ?? [],
|
|
1213
|
+
matches: probe?.matches ?? [],
|
|
1214
|
+
candidateResults: probe?.candidateResults ?? [],
|
|
1215
|
+
callerProvenance: probe?.callerProvenance ?? null,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1189
1219
|
function daemonInterruptErrorResult(error) {
|
|
1190
1220
|
return {
|
|
1191
1221
|
ok: false,
|
|
@@ -1197,6 +1227,22 @@ function daemonInterruptErrorResult(error) {
|
|
|
1197
1227
|
};
|
|
1198
1228
|
}
|
|
1199
1229
|
|
|
1230
|
+
function daemonInterruptProbeFailureResult(probe) {
|
|
1231
|
+
return {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
done: false,
|
|
1234
|
+
aborted: false,
|
|
1235
|
+
reason: probe?.reason || "interrupt_failed",
|
|
1236
|
+
inputSent: false,
|
|
1237
|
+
error: probe?.error || probe?.reason || "Claude daemon unavailable",
|
|
1238
|
+
daemon: probe?.daemon ?? null,
|
|
1239
|
+
daemons: probe?.daemons ?? [],
|
|
1240
|
+
matches: probe?.matches ?? [],
|
|
1241
|
+
candidateResults: probe?.candidateResults ?? [],
|
|
1242
|
+
callerProvenance: probe?.callerProvenance ?? null,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1200
1246
|
function numericOption(value, fallback) {
|
|
1201
1247
|
const parsed = Number(value);
|
|
1202
1248
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -1205,35 +1251,15 @@ function numericOption(value, fallback) {
|
|
|
1205
1251
|
async function cmdDaemonProbe(args) {
|
|
1206
1252
|
try {
|
|
1207
1253
|
const payload = readBridgePayload(args);
|
|
1208
|
-
const {
|
|
1209
|
-
|
|
1210
|
-
extractClaudeAgentSessions,
|
|
1211
|
-
findDaemonJobBySessionId,
|
|
1212
|
-
findDaemonJobByShort,
|
|
1213
|
-
sendClaudeControlRequest,
|
|
1214
|
-
} = await loadDaemonControl();
|
|
1215
|
-
const daemonPaths = deriveClaudeDaemonPaths({
|
|
1254
|
+
const { probeClaudeDaemonCandidates } = await loadDaemonControl();
|
|
1255
|
+
const probe = await probeClaudeDaemonCandidates({
|
|
1216
1256
|
configDir: payload.configDir,
|
|
1257
|
+
env: process.env,
|
|
1258
|
+
short: payload.short,
|
|
1259
|
+
sessionId: payload.sessionId,
|
|
1260
|
+
timeoutMs: numericOption(payload.timeoutMs, 6000),
|
|
1217
1261
|
});
|
|
1218
|
-
|
|
1219
|
-
daemonPaths.controlSock,
|
|
1220
|
-
{ proto: 1, op: "list" },
|
|
1221
|
-
{ timeoutMs: numericOption(payload.timeoutMs, 6000) },
|
|
1222
|
-
);
|
|
1223
|
-
const target = payload.sessionId
|
|
1224
|
-
? findDaemonJobBySessionId(list, payload.sessionId)
|
|
1225
|
-
: payload.short
|
|
1226
|
-
? findDaemonJobByShort(list, payload.short)
|
|
1227
|
-
: null;
|
|
1228
|
-
const sessions = extractClaudeAgentSessions(list);
|
|
1229
|
-
|
|
1230
|
-
return emitJson({
|
|
1231
|
-
ok: list?.ok !== false,
|
|
1232
|
-
controlSock: daemonPaths.controlSock,
|
|
1233
|
-
sessions,
|
|
1234
|
-
target: target || undefined,
|
|
1235
|
-
error: list?.ok === false ? list?.error : undefined,
|
|
1236
|
-
});
|
|
1262
|
+
return emitJson(probe);
|
|
1237
1263
|
} catch (error) {
|
|
1238
1264
|
return emitJson(daemonErrorResult(error));
|
|
1239
1265
|
}
|
|
@@ -1246,33 +1272,49 @@ async function cmdDaemonAttach(args) {
|
|
|
1246
1272
|
|
|
1247
1273
|
const {
|
|
1248
1274
|
attachClaudeDaemonSession,
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
sendClaudeControlRequest,
|
|
1275
|
+
buildDaemonControlAuth,
|
|
1276
|
+
probeClaudeDaemonCandidates,
|
|
1252
1277
|
} = await loadDaemonControl();
|
|
1253
|
-
const
|
|
1278
|
+
const probe = await probeClaudeDaemonCandidates({
|
|
1254
1279
|
configDir: payload.configDir,
|
|
1280
|
+
env: process.env,
|
|
1281
|
+
short: payload.short,
|
|
1282
|
+
sessionId: payload.sessionId,
|
|
1283
|
+
timeoutMs: numericOption(payload.timeoutMs, 6000),
|
|
1255
1284
|
});
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
{
|
|
1261
|
-
|
|
1285
|
+
if (!probe.ok) return emitJson(daemonAttachProbeFailureResult(probe));
|
|
1286
|
+
const short = payload.short ?? probe.target?.short;
|
|
1287
|
+
if (!short) {
|
|
1288
|
+
return emitJson(
|
|
1289
|
+
daemonAttachProbeFailureResult({
|
|
1290
|
+
...probe,
|
|
1291
|
+
ok: false,
|
|
1292
|
+
reason: "target-not-found",
|
|
1293
|
+
error: "short or resolvable sessionId is required",
|
|
1294
|
+
}),
|
|
1262
1295
|
);
|
|
1263
|
-
const target = findDaemonJobBySessionId(list, payload.sessionId);
|
|
1264
|
-
short = target?.short;
|
|
1265
1296
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1297
|
+
const controlAuth = await buildDaemonControlAuth(
|
|
1298
|
+
probe.daemon?.configDir ?? payload.configDir,
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
let result;
|
|
1302
|
+
try {
|
|
1303
|
+
result = await attachClaudeDaemonSession({
|
|
1304
|
+
controlSock: probe.controlSock,
|
|
1305
|
+
short,
|
|
1306
|
+
input: payload.prompt,
|
|
1307
|
+
...controlAuth,
|
|
1308
|
+
cols: numericOption(payload.cols, undefined),
|
|
1309
|
+
rows: numericOption(payload.rows, undefined),
|
|
1310
|
+
timeoutMs: numericOption(payload.timeoutMs, 30_000),
|
|
1311
|
+
});
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return emitJson({
|
|
1314
|
+
...daemonAttachErrorResult(error),
|
|
1315
|
+
...daemonProbeMetadata(probe),
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1276
1318
|
|
|
1277
1319
|
return emitJson({
|
|
1278
1320
|
ok: result.matchedCompletion === true,
|
|
@@ -1285,6 +1327,7 @@ async function cmdDaemonAttach(args) {
|
|
|
1285
1327
|
inputSent: result.inputSent === true,
|
|
1286
1328
|
error:
|
|
1287
1329
|
result.handshake?.ok === false ? result.handshake?.error : undefined,
|
|
1330
|
+
...daemonProbeMetadata(probe),
|
|
1288
1331
|
});
|
|
1289
1332
|
} catch (error) {
|
|
1290
1333
|
return emitJson(daemonAttachErrorResult(error));
|
|
@@ -1296,33 +1339,49 @@ async function cmdDaemonInterrupt(args) {
|
|
|
1296
1339
|
const payload = readBridgePayload(args);
|
|
1297
1340
|
|
|
1298
1341
|
const {
|
|
1299
|
-
|
|
1300
|
-
findDaemonJobBySessionId,
|
|
1342
|
+
buildDaemonControlAuth,
|
|
1301
1343
|
interruptClaudeDaemonSession,
|
|
1302
|
-
|
|
1344
|
+
probeClaudeDaemonCandidates,
|
|
1303
1345
|
} = await loadDaemonControl();
|
|
1304
|
-
const
|
|
1346
|
+
const probe = await probeClaudeDaemonCandidates({
|
|
1305
1347
|
configDir: payload.configDir,
|
|
1348
|
+
env: process.env,
|
|
1349
|
+
short: payload.short,
|
|
1350
|
+
sessionId: payload.sessionId,
|
|
1351
|
+
timeoutMs: numericOption(payload.timeoutMs, 6000),
|
|
1306
1352
|
});
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
{
|
|
1312
|
-
|
|
1353
|
+
if (!probe.ok) return emitJson(daemonInterruptProbeFailureResult(probe));
|
|
1354
|
+
const short = payload.short ?? probe.target?.short;
|
|
1355
|
+
if (!short) {
|
|
1356
|
+
return emitJson(
|
|
1357
|
+
daemonInterruptProbeFailureResult({
|
|
1358
|
+
...probe,
|
|
1359
|
+
ok: false,
|
|
1360
|
+
reason: "target-not-found",
|
|
1361
|
+
error: "short or resolvable sessionId is required",
|
|
1362
|
+
}),
|
|
1313
1363
|
);
|
|
1314
|
-
const target = findDaemonJobBySessionId(list, payload.sessionId);
|
|
1315
|
-
short = target?.short;
|
|
1316
1364
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1365
|
+
const controlAuth = await buildDaemonControlAuth(
|
|
1366
|
+
probe.daemon?.configDir ?? payload.configDir,
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
let result;
|
|
1370
|
+
try {
|
|
1371
|
+
result = await interruptClaudeDaemonSession({
|
|
1372
|
+
controlSock: probe.controlSock,
|
|
1373
|
+
short,
|
|
1374
|
+
...controlAuth,
|
|
1375
|
+
cols: numericOption(payload.cols, undefined),
|
|
1376
|
+
rows: numericOption(payload.rows, undefined),
|
|
1377
|
+
timeoutMs: numericOption(payload.timeoutMs, 5000),
|
|
1378
|
+
});
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
return emitJson({
|
|
1381
|
+
...daemonInterruptErrorResult(error),
|
|
1382
|
+
...daemonProbeMetadata(probe),
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1326
1385
|
const aborted = result.inputSent === true;
|
|
1327
1386
|
|
|
1328
1387
|
return emitJson({
|
|
@@ -1336,6 +1395,7 @@ async function cmdDaemonInterrupt(args) {
|
|
|
1336
1395
|
inputSent: result.inputSent === true,
|
|
1337
1396
|
error:
|
|
1338
1397
|
result.handshake?.ok === false ? result.handshake?.error : undefined,
|
|
1398
|
+
...daemonProbeMetadata(probe),
|
|
1339
1399
|
});
|
|
1340
1400
|
} catch (error) {
|
|
1341
1401
|
return emitJson(daemonInterruptErrorResult(error));
|
|
@@ -16,7 +16,24 @@ import {
|
|
|
16
16
|
|
|
17
17
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
18
18
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
19
|
-
return path.join(
|
|
19
|
+
return path.join(resolveClaudeHomeDir(env), ".claude");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveClaudeHomeDir(
|
|
23
|
+
env = process.env,
|
|
24
|
+
platform = process.platform,
|
|
25
|
+
) {
|
|
26
|
+
// win32: claude daemon launcher 의 os.homedir() 는 USERPROFILE 기반이고
|
|
27
|
+
// HOME 을 보지 않는다. git-bash 가 HOME 을 따로 잡아도 launcher 폴백을
|
|
28
|
+
// 그대로 미러해야 socket hash 가 daemon 과 일치한다 (HOME 수용 금지).
|
|
29
|
+
if (platform === "win32") {
|
|
30
|
+
const userProfile = nullableEnv(env.USERPROFILE);
|
|
31
|
+
return userProfile ? path.resolve(userProfile) : os.homedir();
|
|
32
|
+
}
|
|
33
|
+
// POSIX 는 HOME 우선 — sandbox HOME 오버라이드를 추적하는 provenance
|
|
34
|
+
// 해석(#382)과 일치 (os.homedir() 도 POSIX 에서는 HOME 을 우선한다).
|
|
35
|
+
const home = nullableEnv(env.HOME);
|
|
36
|
+
return home ? path.resolve(home) : os.homedir();
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export function deriveClaudeDaemonPaths({
|
|
@@ -44,6 +61,316 @@ export function deriveClaudeDaemonPaths({
|
|
|
44
61
|
};
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
function nullableEnv(value) {
|
|
65
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
66
|
+
return text.length > 0 ? text : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultClaudeConfigDir(env = process.env) {
|
|
70
|
+
return path.join(resolveClaudeHomeDir(env), ".claude");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function detectCallerProvenance(env = process.env) {
|
|
74
|
+
const claudeConfigDir = nullableEnv(env.CLAUDE_CONFIG_DIR);
|
|
75
|
+
const omxSessionId = nullableEnv(env.OMX_SESSION_ID);
|
|
76
|
+
const omxEntryPath = nullableEnv(env.OMX_ENTRY_PATH);
|
|
77
|
+
const codexThreadId = nullableEnv(env.CODEX_THREAD_ID);
|
|
78
|
+
const codexLauncher =
|
|
79
|
+
omxSessionId || omxEntryPath ? "omx" : codexThreadId ? "codex" : "unknown";
|
|
80
|
+
return {
|
|
81
|
+
claudeLauncher: "unknown",
|
|
82
|
+
codexLauncher,
|
|
83
|
+
signals: {
|
|
84
|
+
claudeConfigDir,
|
|
85
|
+
omxSessionId,
|
|
86
|
+
omxEntryPath,
|
|
87
|
+
codexThreadId,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readOmcLaunchProfile(configDir) {
|
|
93
|
+
if (!configDir) return null;
|
|
94
|
+
try {
|
|
95
|
+
// Provenance hint only: OMC writes this profile in its runtime config dir.
|
|
96
|
+
// It is not an auth/security boundary and must not override explicit/env
|
|
97
|
+
// config-dir authority.
|
|
98
|
+
const parsed = JSON.parse(
|
|
99
|
+
await fs.readFile(
|
|
100
|
+
path.join(path.resolve(configDir), ".omc-launch-profile.json"),
|
|
101
|
+
"utf8",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
const sourceConfigDir = nullableEnv(parsed?.sourceConfigDir);
|
|
105
|
+
if (!sourceConfigDir) return null;
|
|
106
|
+
const sourceClaudeMd = nullableEnv(parsed?.sourceClaudeMd);
|
|
107
|
+
return {
|
|
108
|
+
sourceConfigDir: path.resolve(sourceConfigDir),
|
|
109
|
+
sourceClaudeMd: sourceClaudeMd ? path.resolve(sourceClaudeMd) : null,
|
|
110
|
+
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function buildClaudeDaemonCandidate({
|
|
118
|
+
configDir,
|
|
119
|
+
configDirSource,
|
|
120
|
+
selectionMode,
|
|
121
|
+
env,
|
|
122
|
+
uid,
|
|
123
|
+
tmpRoot,
|
|
124
|
+
}) {
|
|
125
|
+
const paths = deriveClaudeDaemonPaths({ configDir, uid, tmpRoot });
|
|
126
|
+
const defaultConfig = path.resolve(defaultClaudeConfigDir(env));
|
|
127
|
+
const omcProfile = await readOmcLaunchProfile(paths.configDir);
|
|
128
|
+
const claudeLauncher = omcProfile
|
|
129
|
+
? "omc"
|
|
130
|
+
: paths.configDir === defaultConfig
|
|
131
|
+
? "claude"
|
|
132
|
+
: "unknown";
|
|
133
|
+
return {
|
|
134
|
+
...paths,
|
|
135
|
+
configDirSource,
|
|
136
|
+
selectionMode,
|
|
137
|
+
claudeLauncher,
|
|
138
|
+
sourceConfigDir: omcProfile?.sourceConfigDir,
|
|
139
|
+
sourceClaudeMd: omcProfile?.sourceClaudeMd ?? null,
|
|
140
|
+
callerProvenance: detectCallerProvenance(env),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function buildClaudeDaemonDiscoveryCandidates({
|
|
145
|
+
configDir,
|
|
146
|
+
env = process.env,
|
|
147
|
+
uid = typeof process.getuid === "function" ? process.getuid() : 0,
|
|
148
|
+
tmpRoot = "/tmp",
|
|
149
|
+
} = {}) {
|
|
150
|
+
const explicitConfigDir = nullableEnv(configDir);
|
|
151
|
+
if (explicitConfigDir) {
|
|
152
|
+
return [
|
|
153
|
+
await buildClaudeDaemonCandidate({
|
|
154
|
+
configDir: explicitConfigDir,
|
|
155
|
+
configDirSource: "explicit",
|
|
156
|
+
selectionMode: "exact",
|
|
157
|
+
env,
|
|
158
|
+
uid,
|
|
159
|
+
tmpRoot,
|
|
160
|
+
}),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const envConfigDir = nullableEnv(env.CLAUDE_CONFIG_DIR);
|
|
165
|
+
if (envConfigDir) {
|
|
166
|
+
return [
|
|
167
|
+
await buildClaudeDaemonCandidate({
|
|
168
|
+
configDir: envConfigDir,
|
|
169
|
+
configDirSource: "env",
|
|
170
|
+
selectionMode: "env-strict",
|
|
171
|
+
env,
|
|
172
|
+
uid,
|
|
173
|
+
tmpRoot,
|
|
174
|
+
}),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const candidates = [];
|
|
179
|
+
const defaultConfig = defaultClaudeConfigDir(env);
|
|
180
|
+
const omcRuntime = path.join(defaultConfig, ".omc-launch");
|
|
181
|
+
if (await readOmcLaunchProfile(omcRuntime)) {
|
|
182
|
+
candidates.push(
|
|
183
|
+
await buildClaudeDaemonCandidate({
|
|
184
|
+
configDir: omcRuntime,
|
|
185
|
+
configDirSource: "omc-runtime",
|
|
186
|
+
selectionMode: "ambient",
|
|
187
|
+
env,
|
|
188
|
+
uid,
|
|
189
|
+
tmpRoot,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
candidates.push(
|
|
194
|
+
await buildClaudeDaemonCandidate({
|
|
195
|
+
configDir: defaultConfig,
|
|
196
|
+
configDirSource: "default",
|
|
197
|
+
selectionMode: "ambient",
|
|
198
|
+
env,
|
|
199
|
+
uid,
|
|
200
|
+
tmpRoot,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
return candidates;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function publicClaudeDaemonCandidate(candidate) {
|
|
207
|
+
if (!candidate) return null;
|
|
208
|
+
return {
|
|
209
|
+
configDirSource: candidate.configDirSource,
|
|
210
|
+
selectionMode: candidate.selectionMode,
|
|
211
|
+
claudeLauncher: candidate.claudeLauncher,
|
|
212
|
+
configDir: candidate.configDir,
|
|
213
|
+
sourceConfigDir: candidate.sourceConfigDir ?? null,
|
|
214
|
+
sourceClaudeMd: candidate.sourceClaudeMd ?? null,
|
|
215
|
+
hash: candidate.hash,
|
|
216
|
+
controlSock: candidate.controlSock,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sessionsFromDaemonList(listResponse) {
|
|
221
|
+
// probe 경로도 list/find 경로와 같은 정규화 row 를 내보내야 한다
|
|
222
|
+
// (jobs/sessions, snake_case, dispatch 중첩 변형 흡수 — 8016b724).
|
|
223
|
+
return extractClaudeAgentSessions(listResponse);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function errorMessage(error) {
|
|
227
|
+
return error?.message || String(error);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findDaemonTargetInSessions(sessions, { short, sessionId } = {}) {
|
|
231
|
+
if (sessionId) {
|
|
232
|
+
return findDaemonJobBySessionId({ jobs: sessions }, sessionId);
|
|
233
|
+
}
|
|
234
|
+
if (short) {
|
|
235
|
+
return findDaemonJobByShort({ jobs: sessions }, short);
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function candidateResultSummary(result) {
|
|
241
|
+
const summary = publicClaudeDaemonCandidate(result.candidate);
|
|
242
|
+
return {
|
|
243
|
+
...summary,
|
|
244
|
+
ok: result.ok === true,
|
|
245
|
+
error: result.error ?? null,
|
|
246
|
+
sessionCount: Array.isArray(result.sessions) ? result.sessions.length : 0,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildProbeResponse({
|
|
251
|
+
candidates,
|
|
252
|
+
results,
|
|
253
|
+
selected,
|
|
254
|
+
matches,
|
|
255
|
+
targetRequested,
|
|
256
|
+
callerProvenance,
|
|
257
|
+
}) {
|
|
258
|
+
const reachable = results.filter((result) => result.ok === true);
|
|
259
|
+
let ok = false;
|
|
260
|
+
let reason = "daemon-unavailable";
|
|
261
|
+
let error = null;
|
|
262
|
+
|
|
263
|
+
if (targetRequested && matches.length > 1) {
|
|
264
|
+
reason = "ambiguous-target";
|
|
265
|
+
error = `Claude daemon target ambiguous across ${matches.length} candidates`;
|
|
266
|
+
} else if (selected) {
|
|
267
|
+
ok = true;
|
|
268
|
+
reason = selected.target ? "target-found" : "daemon-available";
|
|
269
|
+
} else if (targetRequested && reachable.length > 0) {
|
|
270
|
+
reason = "target-not-found";
|
|
271
|
+
error = "Claude daemon target not found in reachable candidates";
|
|
272
|
+
} else if (results.length > 0) {
|
|
273
|
+
error =
|
|
274
|
+
results
|
|
275
|
+
.map((result) => result.error)
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join("; ") || "Claude daemon unavailable";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const selectedSessions = selected
|
|
281
|
+
? selected.sessions
|
|
282
|
+
: targetRequested
|
|
283
|
+
? []
|
|
284
|
+
: reachable.flatMap((result) => result.sessions);
|
|
285
|
+
const selectedDaemon = publicClaudeDaemonCandidate(selected?.candidate);
|
|
286
|
+
const reachableDaemons = reachable.map((result) =>
|
|
287
|
+
publicClaudeDaemonCandidate(result.candidate),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
ok,
|
|
292
|
+
reason,
|
|
293
|
+
controlSock: selected?.candidate?.controlSock,
|
|
294
|
+
daemon: selectedDaemon,
|
|
295
|
+
daemons: reachableDaemons,
|
|
296
|
+
sessions: selectedSessions,
|
|
297
|
+
target: selected?.target || undefined,
|
|
298
|
+
matches: matches.map((match) => ({
|
|
299
|
+
daemon: publicClaudeDaemonCandidate(match.candidate),
|
|
300
|
+
target: match.target,
|
|
301
|
+
})),
|
|
302
|
+
candidateResults: results.map(candidateResultSummary),
|
|
303
|
+
callerProvenance,
|
|
304
|
+
candidates: candidates.map(publicClaudeDaemonCandidate),
|
|
305
|
+
error: ok ? undefined : error,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function probeClaudeDaemonCandidates({
|
|
310
|
+
configDir,
|
|
311
|
+
env = process.env,
|
|
312
|
+
short,
|
|
313
|
+
sessionId,
|
|
314
|
+
timeoutMs = 6000,
|
|
315
|
+
} = {}) {
|
|
316
|
+
const candidates = await buildClaudeDaemonDiscoveryCandidates({
|
|
317
|
+
configDir,
|
|
318
|
+
env,
|
|
319
|
+
});
|
|
320
|
+
const callerProvenance = detectCallerProvenance(env);
|
|
321
|
+
const targetRequested = Boolean(short || sessionId);
|
|
322
|
+
const results = [];
|
|
323
|
+
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
try {
|
|
326
|
+
const list = await sendClaudeControlRequest(
|
|
327
|
+
candidate.controlSock,
|
|
328
|
+
{ proto: 1, op: "list" },
|
|
329
|
+
{ timeoutMs },
|
|
330
|
+
);
|
|
331
|
+
const sessions = sessionsFromDaemonList(list);
|
|
332
|
+
const ok = list?.ok !== false;
|
|
333
|
+
results.push({
|
|
334
|
+
ok,
|
|
335
|
+
candidate,
|
|
336
|
+
list,
|
|
337
|
+
sessions: ok ? sessions : [],
|
|
338
|
+
target: ok
|
|
339
|
+
? findDaemonTargetInSessions(sessions, { short, sessionId })
|
|
340
|
+
: null,
|
|
341
|
+
error: ok ? null : list?.error || "Claude daemon list failed",
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
results.push({
|
|
345
|
+
ok: false,
|
|
346
|
+
candidate,
|
|
347
|
+
list: null,
|
|
348
|
+
sessions: [],
|
|
349
|
+
target: null,
|
|
350
|
+
error: errorMessage(error),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const matches = targetRequested
|
|
356
|
+
? results.filter((result) => result.ok === true && result.target)
|
|
357
|
+
: [];
|
|
358
|
+
const selected = targetRequested
|
|
359
|
+
? matches.length === 1
|
|
360
|
+
? matches[0]
|
|
361
|
+
: null
|
|
362
|
+
: results.find((result) => result.ok === true) || null;
|
|
363
|
+
|
|
364
|
+
return buildProbeResponse({
|
|
365
|
+
candidates,
|
|
366
|
+
results,
|
|
367
|
+
selected,
|
|
368
|
+
matches,
|
|
369
|
+
targetRequested,
|
|
370
|
+
callerProvenance,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
47
374
|
export function getProcStart(pid = process.pid) {
|
|
48
375
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
49
376
|
throw new Error(`invalid pid: ${pid}`);
|
|
@@ -141,6 +468,7 @@ export function buildDaemonAttachRequest({
|
|
|
141
468
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
142
469
|
rows = 40,
|
|
143
470
|
caps = { terminal: null, mux: null, ssh: false },
|
|
471
|
+
auth,
|
|
144
472
|
} = {}) {
|
|
145
473
|
if (!short) throw new Error("short is required");
|
|
146
474
|
return {
|
|
@@ -150,6 +478,7 @@ export function buildDaemonAttachRequest({
|
|
|
150
478
|
cols,
|
|
151
479
|
rows,
|
|
152
480
|
caps,
|
|
481
|
+
...(auth ? { auth } : {}),
|
|
153
482
|
};
|
|
154
483
|
}
|
|
155
484
|
|
|
@@ -683,6 +1012,7 @@ export function attachClaudeDaemonSession({
|
|
|
683
1012
|
controlSock,
|
|
684
1013
|
short,
|
|
685
1014
|
input,
|
|
1015
|
+
auth,
|
|
686
1016
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
687
1017
|
rows = 40,
|
|
688
1018
|
caps,
|
|
@@ -790,7 +1120,9 @@ export function attachClaudeDaemonSession({
|
|
|
790
1120
|
});
|
|
791
1121
|
socket.on("connect", () => {
|
|
792
1122
|
socket.write(
|
|
793
|
-
`${JSON.stringify(
|
|
1123
|
+
`${JSON.stringify(
|
|
1124
|
+
buildDaemonAttachRequest({ short, cols, rows, caps, auth }),
|
|
1125
|
+
)}\n`,
|
|
794
1126
|
);
|
|
795
1127
|
});
|
|
796
1128
|
socket.on("data", (chunk) => {
|
|
@@ -906,6 +1238,7 @@ export function attachClaudeDaemonSession({
|
|
|
906
1238
|
export function interruptClaudeDaemonSession({
|
|
907
1239
|
controlSock,
|
|
908
1240
|
short,
|
|
1241
|
+
auth,
|
|
909
1242
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
910
1243
|
rows = 40,
|
|
911
1244
|
caps,
|
|
@@ -956,7 +1289,9 @@ export function interruptClaudeDaemonSession({
|
|
|
956
1289
|
socket.on("error", fail);
|
|
957
1290
|
socket.on("connect", () => {
|
|
958
1291
|
socket.write(
|
|
959
|
-
`${JSON.stringify(
|
|
1292
|
+
`${JSON.stringify(
|
|
1293
|
+
buildDaemonAttachRequest({ short, cols, rows, caps, auth }),
|
|
1294
|
+
)}\n`,
|
|
960
1295
|
);
|
|
961
1296
|
});
|
|
962
1297
|
socket.on("data", (chunk) => {
|
|
@@ -1182,6 +1517,7 @@ export async function sendKillBySessionId({
|
|
|
1182
1517
|
daemonPaths,
|
|
1183
1518
|
sessionId,
|
|
1184
1519
|
timeoutMs = 6000,
|
|
1520
|
+
auth,
|
|
1185
1521
|
} = {}) {
|
|
1186
1522
|
const controlSock = daemonPaths?.controlSock;
|
|
1187
1523
|
if (!controlSock || !sessionId) {
|
|
@@ -1201,12 +1537,16 @@ export async function sendKillBySessionId({
|
|
|
1201
1537
|
if (!job?.short) {
|
|
1202
1538
|
return { ok: true, killed: false, reason: "not_found" };
|
|
1203
1539
|
}
|
|
1540
|
+
const controlAuth = auth
|
|
1541
|
+
? { auth }
|
|
1542
|
+
: await buildDaemonControlAuth(daemonPaths?.configDir);
|
|
1204
1543
|
return await sendClaudeControlRequest(
|
|
1205
1544
|
controlSock,
|
|
1206
1545
|
{
|
|
1207
1546
|
proto: 1,
|
|
1208
1547
|
op: "kill",
|
|
1209
1548
|
short: job.short,
|
|
1549
|
+
...controlAuth,
|
|
1210
1550
|
},
|
|
1211
1551
|
{ timeoutMs },
|
|
1212
1552
|
);
|
|
@@ -1356,6 +1696,7 @@ export async function teardownClaudeDaemonJob({
|
|
|
1356
1696
|
_deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
|
|
1357
1697
|
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
1358
1698
|
const removeJobStateImpl = _deps.removeClaudeJobState;
|
|
1699
|
+
const buildAuth = _deps.buildDaemonControlAuth || buildDaemonControlAuth;
|
|
1359
1700
|
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1360
1701
|
const resolvedJobsDir =
|
|
1361
1702
|
jobsDir ||
|
|
@@ -1371,7 +1712,10 @@ export async function teardownClaudeDaemonJob({
|
|
|
1371
1712
|
steps.push(sendKillBySessionId({ daemonPaths, sessionId }).catch(() => {}));
|
|
1372
1713
|
}
|
|
1373
1714
|
if (resolvedControlSock && short) {
|
|
1374
|
-
|
|
1715
|
+
const controlAuth = await buildAuth(paths?.configDir);
|
|
1716
|
+
steps.push(
|
|
1717
|
+
killJob(resolvedControlSock, short, controlAuth).catch(() => {}),
|
|
1718
|
+
);
|
|
1375
1719
|
}
|
|
1376
1720
|
if (removeJobState && removeJobStateImpl && resolvedJobsDir && short) {
|
|
1377
1721
|
steps.push(removeJobStateImpl(resolvedJobsDir, short).catch(() => {}));
|
package/hud/constants.mjs
CHANGED
|
@@ -165,6 +165,8 @@ export const ANTIGRAVITY_OAUTH_PATHS = [
|
|
|
165
165
|
join(homedir(), ".gemini", "antigravity-cli", "credentials.json"),
|
|
166
166
|
GEMINI_OAUTH_PATH,
|
|
167
167
|
];
|
|
168
|
+
export const ANTIGRAVITY_KEYCHAIN_SERVICE = "gemini";
|
|
169
|
+
export const ANTIGRAVITY_KEYCHAIN_ACCOUNT = "antigravity";
|
|
168
170
|
export const ANTIGRAVITY_SETTINGS_PATH = join(
|
|
169
171
|
homedir(),
|
|
170
172
|
".gemini",
|
package/hud/providers/gemini.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// Gemini 쿼터 API / 세션 토큰 / RPM 트래커
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import https from "node:https";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import {
|
|
11
|
+
ANTIGRAVITY_KEYCHAIN_ACCOUNT,
|
|
12
|
+
ANTIGRAVITY_KEYCHAIN_SERVICE,
|
|
11
13
|
ANTIGRAVITY_MODEL_ABBREV,
|
|
12
14
|
ANTIGRAVITY_OAUTH_PATHS,
|
|
13
15
|
ANTIGRAVITY_SETTINGS_PATH,
|
|
@@ -61,7 +63,19 @@ export function getGeminiModelLabel(model) {
|
|
|
61
63
|
|
|
62
64
|
// remainingFraction → 사용 퍼센트 변환 (remainingAmount가 있으면 절대값도 제공)
|
|
63
65
|
export function deriveGeminiLimits(bucket) {
|
|
64
|
-
if (!bucket
|
|
66
|
+
if (!bucket) return null;
|
|
67
|
+
// TODO(verify): Workspace quota 응답 모양 라이브 확인 필요
|
|
68
|
+
if (isGeminiUnlimitedBucket(bucket, bucket.remainingFraction)) {
|
|
69
|
+
return {
|
|
70
|
+
unlimited: true,
|
|
71
|
+
usedPct: null,
|
|
72
|
+
remaining: null,
|
|
73
|
+
limit: null,
|
|
74
|
+
resetTime: bucket.resetTime,
|
|
75
|
+
modelId: bucket.modelId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (bucket.remainingFraction == null) return null;
|
|
65
79
|
const fraction = bucket.remainingFraction;
|
|
66
80
|
const usedPct = clampPercent(Math.round((1 - fraction) * 100));
|
|
67
81
|
// remainingAmount가 API에서 오면 절대값 역산 (Gemini CLI 방식)
|
|
@@ -85,6 +99,23 @@ export function deriveGeminiLimits(bucket) {
|
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
function isUnlimitedValue(value) {
|
|
103
|
+
if (value === Infinity) return true;
|
|
104
|
+
if (typeof value !== "string") return false;
|
|
105
|
+
const normalized = value.trim().toLowerCase();
|
|
106
|
+
return normalized === "infinity" || normalized === "unlimited";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isGeminiUnlimitedBucket(bucket, fraction) {
|
|
110
|
+
return (
|
|
111
|
+
bucket?.unlimited === true ||
|
|
112
|
+
isUnlimitedValue(fraction) ||
|
|
113
|
+
isUnlimitedValue(bucket?.limit) ||
|
|
114
|
+
isUnlimitedValue(bucket?.remaining) ||
|
|
115
|
+
isUnlimitedValue(bucket?.remainingAmount)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
function getCredentialEmail(credential) {
|
|
89
120
|
return (
|
|
90
121
|
decodeJwtEmail(credential?.id_token) ||
|
|
@@ -165,7 +196,21 @@ export function deriveGeminiFamilyBucket(buckets) {
|
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
export function buildGeminiAuthContext(accountId) {
|
|
168
|
-
|
|
199
|
+
let oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
200
|
+
const fileExpired =
|
|
201
|
+
oauth?.expiry_date != null && oauth.expiry_date < Date.now();
|
|
202
|
+
// Preserve a valid Gemini file token; agy Keychain is only a missing/expired fallback.
|
|
203
|
+
if (!oauth?.access_token || fileExpired) {
|
|
204
|
+
const keychainOAuth = getAntigravityTokenFromKeychain();
|
|
205
|
+
if (keychainOAuth?.access_token) {
|
|
206
|
+
const {
|
|
207
|
+
expiry_date: _dropExpiry,
|
|
208
|
+
access_token: _dropToken,
|
|
209
|
+
...fileRest
|
|
210
|
+
} = oauth || {};
|
|
211
|
+
oauth = { ...fileRest, ...keychainOAuth };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
169
214
|
const tokenSource =
|
|
170
215
|
oauth?.refresh_token || oauth?.id_token || oauth?.access_token || "";
|
|
171
216
|
const tokenFingerprint = tokenSource ? makeHash(tokenSource) : "none";
|
|
@@ -173,6 +218,93 @@ export function buildGeminiAuthContext(accountId) {
|
|
|
173
218
|
return { oauth, tokenFingerprint, cacheKey };
|
|
174
219
|
}
|
|
175
220
|
|
|
221
|
+
function firstPresent(...values) {
|
|
222
|
+
for (const value of values) {
|
|
223
|
+
if (value != null && value !== "") return value;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeExpiryDate(value) {
|
|
229
|
+
if (value == null || value === "") return null;
|
|
230
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
231
|
+
return value < 1_000_000_000_000 ? value * 1000 : value;
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === "string") {
|
|
234
|
+
const numeric = Number(value);
|
|
235
|
+
if (Number.isFinite(numeric)) return normalizeExpiryDate(numeric);
|
|
236
|
+
const parsed = Date.parse(value);
|
|
237
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeAntigravityCredential(parsed) {
|
|
243
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const accessToken = firstPresent(
|
|
247
|
+
parsed.access_token,
|
|
248
|
+
parsed.accessToken,
|
|
249
|
+
parsed.access?.token,
|
|
250
|
+
parsed.token,
|
|
251
|
+
);
|
|
252
|
+
if (!accessToken) return null;
|
|
253
|
+
const normalized = { ...parsed, access_token: accessToken };
|
|
254
|
+
const idToken = firstPresent(parsed.id_token, parsed.idToken);
|
|
255
|
+
if (idToken) normalized.id_token = idToken;
|
|
256
|
+
const refreshToken = firstPresent(parsed.refresh_token, parsed.refreshToken);
|
|
257
|
+
if (refreshToken) normalized.refresh_token = refreshToken;
|
|
258
|
+
const expiryDate = normalizeExpiryDate(
|
|
259
|
+
firstPresent(
|
|
260
|
+
parsed.expiry_date,
|
|
261
|
+
parsed.expiryDate,
|
|
262
|
+
parsed.expiry,
|
|
263
|
+
parsed.expires_at,
|
|
264
|
+
parsed.expiresAt,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
if (expiryDate != null) normalized.expiry_date = expiryDate;
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getAntigravityTokenFromKeychain() {
|
|
272
|
+
// macOS Keychain only; elsewhere `security` is absent (wasted spawn) or a
|
|
273
|
+
// different PATH binary entirely, so skip on Linux/Windows and let the
|
|
274
|
+
// file-token fallback apply. TFX_HUD_KEYCHAIN_FORCE=1 re-enables the spawn
|
|
275
|
+
// for cross-platform tests that inject a fake `security` via PATH.
|
|
276
|
+
if (
|
|
277
|
+
process.platform !== "darwin" &&
|
|
278
|
+
process.env.TFX_HUD_KEYCHAIN_FORCE !== "1"
|
|
279
|
+
) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = spawnSync(
|
|
284
|
+
"security",
|
|
285
|
+
[
|
|
286
|
+
"find-generic-password",
|
|
287
|
+
"-s",
|
|
288
|
+
ANTIGRAVITY_KEYCHAIN_SERVICE,
|
|
289
|
+
"-a",
|
|
290
|
+
ANTIGRAVITY_KEYCHAIN_ACCOUNT,
|
|
291
|
+
"-w",
|
|
292
|
+
],
|
|
293
|
+
{ encoding: "utf8", timeout: 3000 },
|
|
294
|
+
);
|
|
295
|
+
if (result.status !== 0) return null;
|
|
296
|
+
const stdout = result.stdout?.trim();
|
|
297
|
+
if (!stdout) return null;
|
|
298
|
+
try {
|
|
299
|
+
return normalizeAntigravityCredential(JSON.parse(stdout));
|
|
300
|
+
} catch {
|
|
301
|
+
return { access_token: stdout };
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
176
308
|
// ============================================================================
|
|
177
309
|
// Gemini 쿼터 API 호출 (5분 캐시)
|
|
178
310
|
// ============================================================================
|
package/hud/renderers.mjs
CHANGED
|
@@ -51,6 +51,25 @@ import {
|
|
|
51
51
|
truncateAnsi,
|
|
52
52
|
} from "./utils.mjs";
|
|
53
53
|
|
|
54
|
+
function formatGeminiLimitValue(limit, provFn) {
|
|
55
|
+
if (limit?.unlimited) return provFn("\u221E");
|
|
56
|
+
const usedP = limit?.usedPct;
|
|
57
|
+
if (usedP == null) return dim(formatPlaceholderPercentCell());
|
|
58
|
+
return colorByProvider(usedP, formatPercentCell(usedP), provFn);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getGeminiLimitUsedPercent(bucket, limit) {
|
|
62
|
+
if (limit?.unlimited) return null;
|
|
63
|
+
if (limit?.usedPct != null) return limit.usedPct;
|
|
64
|
+
return clampPercent(Math.round((1 - (bucket?.remainingFraction ?? 1)) * 100));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatGeminiCompactValue(bucket, limit, provFn) {
|
|
68
|
+
if (limit?.unlimited) return provFn("\u221E");
|
|
69
|
+
const usedP = getGeminiLimitUsedPercent(bucket, limit);
|
|
70
|
+
return usedP != null ? colorByProvider(usedP, `${usedP}`, provFn) : dim("--");
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
// ============================================================================
|
|
55
74
|
// 최근 벤치마크 diff 파일 읽기
|
|
56
75
|
// ============================================================================
|
|
@@ -299,10 +318,7 @@ export function getMicroLine(
|
|
|
299
318
|
let gVal;
|
|
300
319
|
if (geminiBucket) {
|
|
301
320
|
const gl = deriveGeminiLimits(geminiBucket);
|
|
302
|
-
|
|
303
|
-
? gl.usedPct
|
|
304
|
-
: clampPercent((1 - (geminiBucket.remainingFraction ?? 1)) * 100);
|
|
305
|
-
gVal = colorByProvider(gU, `${gU}`, geminiBlue);
|
|
321
|
+
gVal = formatGeminiCompactValue(geminiBucket, gl, geminiBlue);
|
|
306
322
|
} else if ((geminiSession?.total || 0) > 0) {
|
|
307
323
|
gVal = geminiBlue("\u221E");
|
|
308
324
|
} else {
|
|
@@ -468,11 +484,10 @@ function formatAntigravityQuotaSection(
|
|
|
468
484
|
return withTime ? `${base} ${dim(formatTimeCell("n/a"))}` : base;
|
|
469
485
|
}
|
|
470
486
|
const gl = deriveGeminiLimits(bucket);
|
|
471
|
-
const usedP = gl
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
const base = `${dim(`${label}:`)}${bar}${colorByProvider(usedP, formatPercentCell(usedP), provFn)}`;
|
|
487
|
+
const usedP = getGeminiLimitUsedPercent(bucket, gl);
|
|
488
|
+
const bar =
|
|
489
|
+
withBar && usedP != null ? tierBar(currentTier, usedP, provAnsi) : "";
|
|
490
|
+
const base = `${dim(`${label}:`)}${bar}${formatGeminiLimitValue(gl, provFn)}`;
|
|
476
491
|
if (!withTime) return base;
|
|
477
492
|
const rstRemaining =
|
|
478
493
|
formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
@@ -558,20 +573,14 @@ export function getProviderRow(
|
|
|
558
573
|
if (provider === "gemini" && realQuota?.type === "gemini") {
|
|
559
574
|
const pools = realQuota.pools || {};
|
|
560
575
|
if (pools.pro || pools.flash) {
|
|
561
|
-
const pP = pools.pro
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
:
|
|
566
|
-
const
|
|
567
|
-
?
|
|
568
|
-
|
|
569
|
-
)
|
|
570
|
-
: null;
|
|
571
|
-
const pStr =
|
|
572
|
-
pP != null ? colorByProvider(pP, `${pP}`, provFn) : dim("--");
|
|
573
|
-
const fStr =
|
|
574
|
-
pF != null ? colorByProvider(pF, `${pF}`, provFn) : dim("--");
|
|
576
|
+
const pP = pools.pro ? deriveGeminiLimits(pools.pro) : null;
|
|
577
|
+
const pF = pools.flash ? deriveGeminiLimits(pools.flash) : null;
|
|
578
|
+
const pStr = pools.pro
|
|
579
|
+
? formatGeminiCompactValue(pools.pro, pP, provFn)
|
|
580
|
+
: dim("--");
|
|
581
|
+
const fStr = pools.flash
|
|
582
|
+
? formatGeminiCompactValue(pools.flash, pF, provFn)
|
|
583
|
+
: dim("--");
|
|
575
584
|
return {
|
|
576
585
|
prefix: minPrefix,
|
|
577
586
|
left: `${pStr}${dim("/")}${fStr}`,
|
|
@@ -622,10 +631,7 @@ export function getProviderRow(
|
|
|
622
631
|
if (!bucket)
|
|
623
632
|
return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())}`;
|
|
624
633
|
const gl = deriveGeminiLimits(bucket);
|
|
625
|
-
|
|
626
|
-
? gl.usedPct
|
|
627
|
-
: clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
628
|
-
return `${dim(label + ":")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)}`;
|
|
634
|
+
return `${dim(label + ":")}${formatGeminiLimitValue(gl, provFn)}`;
|
|
629
635
|
};
|
|
630
636
|
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
|
631
637
|
} else {
|
|
@@ -691,12 +697,9 @@ export function getProviderRow(
|
|
|
691
697
|
if (!bucket)
|
|
692
698
|
return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
693
699
|
const gl = deriveGeminiLimits(bucket);
|
|
694
|
-
const usedP = gl
|
|
695
|
-
? gl.usedPct
|
|
696
|
-
: clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
697
700
|
const rstRemaining =
|
|
698
701
|
formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
699
|
-
return `${dim(label + ":")}${
|
|
702
|
+
return `${dim(label + ":")}${formatGeminiLimitValue(gl, provFn)} ${dim(formatTimeCell(rstRemaining))}`;
|
|
700
703
|
};
|
|
701
704
|
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
|
702
705
|
} else {
|
|
@@ -778,12 +781,11 @@ export function getProviderRow(
|
|
|
778
781
|
return `${dim(label + ":")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
779
782
|
}
|
|
780
783
|
const gl = deriveGeminiLimits(bucket);
|
|
781
|
-
const usedP = gl
|
|
782
|
-
? gl.usedPct
|
|
783
|
-
: clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
784
|
+
const usedP = getGeminiLimitUsedPercent(bucket, gl);
|
|
784
785
|
const rstRemaining =
|
|
785
786
|
formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
786
|
-
|
|
787
|
+
const bar = usedP != null ? tierBar(currentTier, usedP, provAnsi) : "";
|
|
788
|
+
return `${dim(label + ":")}${bar}${formatGeminiLimitValue(gl, provFn)} ${dim(formatTimeCell(rstRemaining))}`;
|
|
787
789
|
};
|
|
788
790
|
|
|
789
791
|
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@ export const MANIFEST_PATH = join(
|
|
|
13
13
|
"mcp-enabled.json",
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
/** API 키 불필요 — 항상 활성화 for gateway-managed MCP servers */
|
|
17
|
-
export const CORE_SERVERS = Object.freeze([
|
|
16
|
+
/** API 키 불필요 — 항상 활성화 for gateway-managed MCP servers (현재 없음 — serena는 2026-06-10 core에서 제거) */
|
|
17
|
+
export const CORE_SERVERS = Object.freeze([]);
|
|
18
18
|
|
|
19
19
|
/** 검색 MCP — API 키 필요 */
|
|
20
20
|
export const SEARCH_SERVERS = Object.freeze([
|