@hua-labs/tap 0.2.6 → 0.3.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/LICENSE +21 -0
- package/README.md +280 -194
- package/dist/bridges/codex-app-server-bridge.d.mts +9 -1
- package/dist/bridges/codex-app-server-bridge.mjs +189 -9
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +8 -16
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/gemini-ide-companion-runner.d.mts +1 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs +22501 -0
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -0
- package/dist/cli.mjs +1938 -989
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +76 -22
- package/dist/index.mjs +26727 -3768
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +303 -196
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +65 -65
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
mkdirSync,
|
|
10
10
|
readdirSync,
|
|
11
11
|
readFileSync,
|
|
12
|
+
renameSync,
|
|
12
13
|
statSync,
|
|
14
|
+
unlinkSync,
|
|
13
15
|
writeFileSync
|
|
14
16
|
} from "fs";
|
|
15
17
|
import { isAbsolute, join, resolve } from "path";
|
|
@@ -41,7 +43,9 @@ function threadCwdMatches(expectedCwd, actualCwd) {
|
|
|
41
43
|
return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
|
|
42
44
|
}
|
|
43
45
|
function chooseLoadedThreadForCwd(cwd, threads) {
|
|
44
|
-
const matching = threads.filter(
|
|
46
|
+
const matching = threads.filter(
|
|
47
|
+
(thread) => threadCwdMatches(cwd, thread.cwd)
|
|
48
|
+
);
|
|
45
49
|
if (matching.length === 0) {
|
|
46
50
|
return null;
|
|
47
51
|
}
|
|
@@ -303,7 +307,7 @@ function normalizeAgentToken(value) {
|
|
|
303
307
|
if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
|
|
304
308
|
return null;
|
|
305
309
|
}
|
|
306
|
-
return normalized;
|
|
310
|
+
return normalized.replace(/-/g, "_");
|
|
307
311
|
}
|
|
308
312
|
function resolveAgentId(preferredAgentName) {
|
|
309
313
|
return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
|
|
@@ -340,6 +344,10 @@ function persistAgentName(stateDir, agentName) {
|
|
|
340
344
|
writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
|
|
341
345
|
`, "utf8");
|
|
342
346
|
}
|
|
347
|
+
function sanitizeErrorForPersistence(error) {
|
|
348
|
+
if (!error) return null;
|
|
349
|
+
return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/tap-auth-[A-Za-z0-9_-]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.-]+/gi, "Bearer ***");
|
|
350
|
+
}
|
|
343
351
|
function readGatewayTokenFile(tokenFile) {
|
|
344
352
|
const token = readFileSync(tokenFile, "utf8").trim();
|
|
345
353
|
if (!token) {
|
|
@@ -368,6 +376,69 @@ function readThreadState(stateDir) {
|
|
|
368
376
|
}
|
|
369
377
|
return null;
|
|
370
378
|
}
|
|
379
|
+
function readHeartbeatState(stateDir) {
|
|
380
|
+
const heartbeatPath = join(stateDir, "heartbeat.json");
|
|
381
|
+
if (!existsSync(heartbeatPath)) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
return JSON.parse(readFileSync(heartbeatPath, "utf8"));
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function parseUpdatedAt(value) {
|
|
391
|
+
if (!value) {
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
const parsed = Date.parse(value);
|
|
395
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
396
|
+
}
|
|
397
|
+
function appServerUrlMatches(expectedAppServerUrl, actualAppServerUrl) {
|
|
398
|
+
return actualAppServerUrl?.trim() === expectedAppServerUrl;
|
|
399
|
+
}
|
|
400
|
+
function hasValidHeartbeatThreadCwd(threadCwd) {
|
|
401
|
+
const normalized = threadCwd?.trim();
|
|
402
|
+
if (!normalized) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return isAbsolute(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
|
|
406
|
+
}
|
|
407
|
+
function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
|
|
408
|
+
const savedThread = readThreadState(stateDir);
|
|
409
|
+
const heartbeat = readHeartbeatState(stateDir);
|
|
410
|
+
const heartbeatThreadId = heartbeat?.threadId?.trim();
|
|
411
|
+
if (!heartbeatThreadId) {
|
|
412
|
+
return savedThread;
|
|
413
|
+
}
|
|
414
|
+
if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
|
|
415
|
+
return savedThread;
|
|
416
|
+
}
|
|
417
|
+
if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
|
|
418
|
+
return savedThread;
|
|
419
|
+
}
|
|
420
|
+
const heartbeatBackedThread = {
|
|
421
|
+
threadId: heartbeatThreadId,
|
|
422
|
+
updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
423
|
+
appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
|
|
424
|
+
ephemeral: savedThread?.ephemeral ?? false,
|
|
425
|
+
cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
|
|
426
|
+
};
|
|
427
|
+
let preferred = savedThread;
|
|
428
|
+
if (!savedThread?.threadId) {
|
|
429
|
+
preferred = heartbeatBackedThread;
|
|
430
|
+
} else if (savedThread.threadId === heartbeatThreadId) {
|
|
431
|
+
preferred = {
|
|
432
|
+
...savedThread,
|
|
433
|
+
updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
|
|
434
|
+
appServerUrl: heartbeatBackedThread.appServerUrl,
|
|
435
|
+
cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
|
|
436
|
+
};
|
|
437
|
+
} else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
|
|
438
|
+
preferred = heartbeatBackedThread;
|
|
439
|
+
}
|
|
440
|
+
return preferred;
|
|
441
|
+
}
|
|
371
442
|
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
|
|
372
443
|
const payload = {
|
|
373
444
|
threadId,
|
|
@@ -408,14 +479,18 @@ function recipientMatchesAgent(recipient, agentId, agentName) {
|
|
|
408
479
|
if (!normalizedRecipient) {
|
|
409
480
|
return false;
|
|
410
481
|
}
|
|
411
|
-
|
|
482
|
+
const canonicalRecipient = normalizedRecipient.replace(/-/g, "_");
|
|
483
|
+
const canonicalAgentId = agentId.trim().replace(/-/g, "_");
|
|
484
|
+
return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || canonicalRecipient === canonicalAgentId || normalizedRecipient === agentName;
|
|
412
485
|
}
|
|
413
486
|
function isOwnMessageSender(sender, agentId, agentName) {
|
|
414
487
|
const normalizedSender = sender.trim();
|
|
415
488
|
if (!normalizedSender) {
|
|
416
489
|
return false;
|
|
417
490
|
}
|
|
418
|
-
|
|
491
|
+
const canonicalSender = normalizedSender.replace(/-/g, "_");
|
|
492
|
+
const canonicalAgentId = agentId.trim().replace(/-/g, "_");
|
|
493
|
+
return canonicalSender === canonicalAgentId || normalizedSender === agentName;
|
|
419
494
|
}
|
|
420
495
|
function getInboxRoute(fileName) {
|
|
421
496
|
const stem = fileName.replace(/\.md$/i, "");
|
|
@@ -741,6 +816,7 @@ var AppServerClient = class {
|
|
|
741
816
|
threadId = null;
|
|
742
817
|
currentThreadCwd = null;
|
|
743
818
|
activeTurnId = null;
|
|
819
|
+
turnStartedAt = null;
|
|
744
820
|
lastTurnStatus = null;
|
|
745
821
|
lastNotificationMethod = null;
|
|
746
822
|
lastNotificationAt = null;
|
|
@@ -781,6 +857,7 @@ var AppServerClient = class {
|
|
|
781
857
|
"open",
|
|
782
858
|
() => {
|
|
783
859
|
this.connected = true;
|
|
860
|
+
this.logger(`connected to app-server at ${this.url}`);
|
|
784
861
|
resolveOnce();
|
|
785
862
|
},
|
|
786
863
|
{ once: true }
|
|
@@ -796,6 +873,8 @@ var AppServerClient = class {
|
|
|
796
873
|
this.connected = false;
|
|
797
874
|
this.initialized = false;
|
|
798
875
|
this.activeTurnId = null;
|
|
876
|
+
this.turnStartedAt = null;
|
|
877
|
+
this.logger("disconnected from app-server");
|
|
799
878
|
this.rejectPending(new Error("App Server connection closed"));
|
|
800
879
|
});
|
|
801
880
|
this.socket?.addEventListener("message", (event) => {
|
|
@@ -866,6 +945,7 @@ var AppServerClient = class {
|
|
|
866
945
|
this.threadId = null;
|
|
867
946
|
this.currentThreadCwd = null;
|
|
868
947
|
this.activeTurnId = null;
|
|
948
|
+
this.turnStartedAt = null;
|
|
869
949
|
this.lastTurnStatus = null;
|
|
870
950
|
} else {
|
|
871
951
|
this.logger(
|
|
@@ -958,6 +1038,7 @@ var AppServerClient = class {
|
|
|
958
1038
|
const turnId = response?.turn?.id ?? null;
|
|
959
1039
|
if (turnId) {
|
|
960
1040
|
this.activeTurnId = turnId;
|
|
1041
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
961
1042
|
}
|
|
962
1043
|
return turnId;
|
|
963
1044
|
}
|
|
@@ -1021,6 +1102,11 @@ var AppServerClient = class {
|
|
|
1021
1102
|
activeTurnId = turn.id;
|
|
1022
1103
|
}
|
|
1023
1104
|
}
|
|
1105
|
+
if (activeTurnId && activeTurnId !== this.activeTurnId) {
|
|
1106
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1107
|
+
} else if (!activeTurnId) {
|
|
1108
|
+
this.turnStartedAt = null;
|
|
1109
|
+
}
|
|
1024
1110
|
this.activeTurnId = activeTurnId;
|
|
1025
1111
|
this.lastTurnStatus = lastTurnStatus;
|
|
1026
1112
|
}
|
|
@@ -1071,14 +1157,22 @@ var AppServerClient = class {
|
|
|
1071
1157
|
case "turn/started":
|
|
1072
1158
|
if (params?.turn?.id) {
|
|
1073
1159
|
this.activeTurnId = params.turn.id;
|
|
1160
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1074
1161
|
this.logger(`turn started ${params.turn.id}`);
|
|
1075
1162
|
}
|
|
1076
1163
|
break;
|
|
1077
|
-
case "turn/completed":
|
|
1164
|
+
case "turn/completed": {
|
|
1078
1165
|
this.lastTurnStatus = params?.turn?.status ?? null;
|
|
1166
|
+
const prevTurnStartedAt = this.turnStartedAt;
|
|
1079
1167
|
this.activeTurnId = null;
|
|
1080
|
-
this.
|
|
1168
|
+
this.turnStartedAt = null;
|
|
1169
|
+
const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
|
|
1170
|
+
const elapsedSuffix = elapsedMs !== null ? ` \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed` : "";
|
|
1171
|
+
this.logger(
|
|
1172
|
+
`turn completed (${this.lastTurnStatus ?? "unknown"})${elapsedSuffix}`
|
|
1173
|
+
);
|
|
1081
1174
|
break;
|
|
1175
|
+
}
|
|
1082
1176
|
case "error":
|
|
1083
1177
|
this.lastError = JSON.stringify(params ?? {}, null, 2);
|
|
1084
1178
|
this.logger(`app-server error notification: ${this.lastError}`);
|
|
@@ -1115,6 +1209,7 @@ var AppServerClient = class {
|
|
|
1115
1209
|
this.pending.clear();
|
|
1116
1210
|
}
|
|
1117
1211
|
};
|
|
1212
|
+
var heartbeatCount = 0;
|
|
1118
1213
|
function writeHeartbeat(options, client, health) {
|
|
1119
1214
|
if (client?.threadId) {
|
|
1120
1215
|
const savedThread = readThreadState(options.stateDir);
|
|
@@ -1137,10 +1232,11 @@ function writeHeartbeat(options, client, health) {
|
|
|
1137
1232
|
threadId: client?.threadId ?? null,
|
|
1138
1233
|
threadCwd: client?.currentThreadCwd ?? null,
|
|
1139
1234
|
activeTurnId: client?.activeTurnId ?? null,
|
|
1235
|
+
turnStartedAt: client?.turnStartedAt ?? null,
|
|
1140
1236
|
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1141
1237
|
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
1142
1238
|
lastNotificationAt: client?.lastNotificationAt ?? null,
|
|
1143
|
-
lastError: client?.lastError ?? null,
|
|
1239
|
+
lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
|
|
1144
1240
|
lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
|
|
1145
1241
|
lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
|
|
1146
1242
|
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
@@ -1152,9 +1248,84 @@ function writeHeartbeat(options, client, health) {
|
|
|
1152
1248
|
`,
|
|
1153
1249
|
"utf8"
|
|
1154
1250
|
);
|
|
1251
|
+
heartbeatCount += 1;
|
|
1252
|
+
if (heartbeatCount % 5 === 0) {
|
|
1253
|
+
logStatus(
|
|
1254
|
+
`heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const status = client?.connected ? "active" : "idle";
|
|
1258
|
+
updateCommsHeartbeat(options, status);
|
|
1259
|
+
}
|
|
1260
|
+
var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
|
|
1261
|
+
var COMMS_LOCK_STALE_AGE_MS = 1e4;
|
|
1262
|
+
function acquireCommsLock(lockPath) {
|
|
1263
|
+
const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
|
|
1264
|
+
while (Date.now() < deadline) {
|
|
1265
|
+
try {
|
|
1266
|
+
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1267
|
+
return true;
|
|
1268
|
+
} catch {
|
|
1269
|
+
try {
|
|
1270
|
+
const lockAge = Date.now() - statSync(lockPath).mtimeMs;
|
|
1271
|
+
if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
|
|
1272
|
+
unlinkSync(lockPath);
|
|
1273
|
+
try {
|
|
1274
|
+
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1275
|
+
return true;
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
const start = Date.now();
|
|
1282
|
+
while (Date.now() - start < 50) {
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
function releaseCommsLock(lockPath) {
|
|
1289
|
+
try {
|
|
1290
|
+
unlinkSync(lockPath);
|
|
1291
|
+
} catch {
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function updateCommsHeartbeat(options, status) {
|
|
1295
|
+
const heartbeatsPath = join(options.commsDir, "heartbeats.json");
|
|
1296
|
+
const lockPath = join(options.commsDir, ".heartbeats.lock");
|
|
1297
|
+
if (!acquireCommsLock(lockPath)) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
let store = {};
|
|
1302
|
+
try {
|
|
1303
|
+
store = JSON.parse(readFileSync(heartbeatsPath, "utf-8"));
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
const key = options.agentId;
|
|
1307
|
+
const existing = store[key];
|
|
1308
|
+
store[key] = {
|
|
1309
|
+
id: options.agentId,
|
|
1310
|
+
agent: options.agentName,
|
|
1311
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1312
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1313
|
+
joinedAt: existing?.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1314
|
+
status
|
|
1315
|
+
};
|
|
1316
|
+
const tmpPath = heartbeatsPath + ".tmp." + process.pid;
|
|
1317
|
+
writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
1318
|
+
renameSync(tmpPath, heartbeatsPath);
|
|
1319
|
+
} catch {
|
|
1320
|
+
} finally {
|
|
1321
|
+
releaseCommsLock(lockPath);
|
|
1322
|
+
}
|
|
1155
1323
|
}
|
|
1156
1324
|
async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
1157
1325
|
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
1326
|
+
logStatus(
|
|
1327
|
+
`dispatching from ${candidate.sender || "unknown"}: ${candidate.subject || "(none)"}`
|
|
1328
|
+
);
|
|
1158
1329
|
if (client.isBusy()) {
|
|
1159
1330
|
if (options.busyMode !== "steer") {
|
|
1160
1331
|
return false;
|
|
@@ -1184,6 +1355,7 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1184
1355
|
}
|
|
1185
1356
|
if (shouldRetrySteerAsStart(error)) {
|
|
1186
1357
|
client.activeTurnId = null;
|
|
1358
|
+
client.turnStartedAt = null;
|
|
1187
1359
|
logStatus(
|
|
1188
1360
|
`steer fallback -> start for ${candidate.fileName} (${String(error)})`
|
|
1189
1361
|
);
|
|
@@ -1287,7 +1459,10 @@ async function main() {
|
|
|
1287
1459
|
options.messageLookbackMinutes,
|
|
1288
1460
|
options.processExistingMessages
|
|
1289
1461
|
);
|
|
1290
|
-
const initialSavedThread =
|
|
1462
|
+
const initialSavedThread = loadResumableThreadState(
|
|
1463
|
+
options.stateDir,
|
|
1464
|
+
options.appServerUrl
|
|
1465
|
+
);
|
|
1291
1466
|
logStatus("codex app-server bridge ready");
|
|
1292
1467
|
console.log(` repo: ${options.repoRoot}`);
|
|
1293
1468
|
console.log(` comms: ${options.commsDir}`);
|
|
@@ -1325,7 +1500,10 @@ async function main() {
|
|
|
1325
1500
|
options.gatewayToken
|
|
1326
1501
|
);
|
|
1327
1502
|
await client.connect();
|
|
1328
|
-
const savedThread =
|
|
1503
|
+
const savedThread = loadResumableThreadState(
|
|
1504
|
+
options.stateDir,
|
|
1505
|
+
options.appServerUrl
|
|
1506
|
+
);
|
|
1329
1507
|
const threadId = await client.ensureThread(
|
|
1330
1508
|
options.threadId,
|
|
1331
1509
|
savedThread,
|
|
@@ -1373,6 +1551,7 @@ async function main() {
|
|
|
1373
1551
|
}
|
|
1374
1552
|
client?.disconnect().catch(() => void 0);
|
|
1375
1553
|
client = null;
|
|
1554
|
+
logStatus(`reconnecting in ${options.reconnectSeconds}s...`);
|
|
1376
1555
|
await delay(options.reconnectSeconds * 1e3);
|
|
1377
1556
|
}
|
|
1378
1557
|
}
|
|
@@ -1412,6 +1591,7 @@ export {
|
|
|
1412
1591
|
buildUserInput,
|
|
1413
1592
|
chooseLoadedThreadForCwd,
|
|
1414
1593
|
isOwnMessageSender,
|
|
1594
|
+
loadResumableThreadState,
|
|
1415
1595
|
main,
|
|
1416
1596
|
maybeBootstrapHeadlessTurn,
|
|
1417
1597
|
recipientMatchesAgent,
|