@hua-labs/tap 0.2.5 → 0.3.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/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 +182 -6
- 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 +331 -205
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +65 -65
- /package/bin/{tap-comms.mjs → tap.mjs} +0 -0
|
@@ -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
|
}
|
|
@@ -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,
|
|
@@ -741,6 +812,7 @@ var AppServerClient = class {
|
|
|
741
812
|
threadId = null;
|
|
742
813
|
currentThreadCwd = null;
|
|
743
814
|
activeTurnId = null;
|
|
815
|
+
turnStartedAt = null;
|
|
744
816
|
lastTurnStatus = null;
|
|
745
817
|
lastNotificationMethod = null;
|
|
746
818
|
lastNotificationAt = null;
|
|
@@ -781,6 +853,7 @@ var AppServerClient = class {
|
|
|
781
853
|
"open",
|
|
782
854
|
() => {
|
|
783
855
|
this.connected = true;
|
|
856
|
+
this.logger(`connected to app-server at ${this.url}`);
|
|
784
857
|
resolveOnce();
|
|
785
858
|
},
|
|
786
859
|
{ once: true }
|
|
@@ -796,6 +869,8 @@ var AppServerClient = class {
|
|
|
796
869
|
this.connected = false;
|
|
797
870
|
this.initialized = false;
|
|
798
871
|
this.activeTurnId = null;
|
|
872
|
+
this.turnStartedAt = null;
|
|
873
|
+
this.logger("disconnected from app-server");
|
|
799
874
|
this.rejectPending(new Error("App Server connection closed"));
|
|
800
875
|
});
|
|
801
876
|
this.socket?.addEventListener("message", (event) => {
|
|
@@ -866,6 +941,7 @@ var AppServerClient = class {
|
|
|
866
941
|
this.threadId = null;
|
|
867
942
|
this.currentThreadCwd = null;
|
|
868
943
|
this.activeTurnId = null;
|
|
944
|
+
this.turnStartedAt = null;
|
|
869
945
|
this.lastTurnStatus = null;
|
|
870
946
|
} else {
|
|
871
947
|
this.logger(
|
|
@@ -958,6 +1034,7 @@ var AppServerClient = class {
|
|
|
958
1034
|
const turnId = response?.turn?.id ?? null;
|
|
959
1035
|
if (turnId) {
|
|
960
1036
|
this.activeTurnId = turnId;
|
|
1037
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
961
1038
|
}
|
|
962
1039
|
return turnId;
|
|
963
1040
|
}
|
|
@@ -1021,6 +1098,11 @@ var AppServerClient = class {
|
|
|
1021
1098
|
activeTurnId = turn.id;
|
|
1022
1099
|
}
|
|
1023
1100
|
}
|
|
1101
|
+
if (activeTurnId && activeTurnId !== this.activeTurnId) {
|
|
1102
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1103
|
+
} else if (!activeTurnId) {
|
|
1104
|
+
this.turnStartedAt = null;
|
|
1105
|
+
}
|
|
1024
1106
|
this.activeTurnId = activeTurnId;
|
|
1025
1107
|
this.lastTurnStatus = lastTurnStatus;
|
|
1026
1108
|
}
|
|
@@ -1071,14 +1153,22 @@ var AppServerClient = class {
|
|
|
1071
1153
|
case "turn/started":
|
|
1072
1154
|
if (params?.turn?.id) {
|
|
1073
1155
|
this.activeTurnId = params.turn.id;
|
|
1156
|
+
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1074
1157
|
this.logger(`turn started ${params.turn.id}`);
|
|
1075
1158
|
}
|
|
1076
1159
|
break;
|
|
1077
|
-
case "turn/completed":
|
|
1160
|
+
case "turn/completed": {
|
|
1078
1161
|
this.lastTurnStatus = params?.turn?.status ?? null;
|
|
1162
|
+
const prevTurnStartedAt = this.turnStartedAt;
|
|
1079
1163
|
this.activeTurnId = null;
|
|
1080
|
-
this.
|
|
1164
|
+
this.turnStartedAt = null;
|
|
1165
|
+
const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
|
|
1166
|
+
const elapsedSuffix = elapsedMs !== null ? ` \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed` : "";
|
|
1167
|
+
this.logger(
|
|
1168
|
+
`turn completed (${this.lastTurnStatus ?? "unknown"})${elapsedSuffix}`
|
|
1169
|
+
);
|
|
1081
1170
|
break;
|
|
1171
|
+
}
|
|
1082
1172
|
case "error":
|
|
1083
1173
|
this.lastError = JSON.stringify(params ?? {}, null, 2);
|
|
1084
1174
|
this.logger(`app-server error notification: ${this.lastError}`);
|
|
@@ -1115,6 +1205,7 @@ var AppServerClient = class {
|
|
|
1115
1205
|
this.pending.clear();
|
|
1116
1206
|
}
|
|
1117
1207
|
};
|
|
1208
|
+
var heartbeatCount = 0;
|
|
1118
1209
|
function writeHeartbeat(options, client, health) {
|
|
1119
1210
|
if (client?.threadId) {
|
|
1120
1211
|
const savedThread = readThreadState(options.stateDir);
|
|
@@ -1137,10 +1228,11 @@ function writeHeartbeat(options, client, health) {
|
|
|
1137
1228
|
threadId: client?.threadId ?? null,
|
|
1138
1229
|
threadCwd: client?.currentThreadCwd ?? null,
|
|
1139
1230
|
activeTurnId: client?.activeTurnId ?? null,
|
|
1231
|
+
turnStartedAt: client?.turnStartedAt ?? null,
|
|
1140
1232
|
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1141
1233
|
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
1142
1234
|
lastNotificationAt: client?.lastNotificationAt ?? null,
|
|
1143
|
-
lastError: client?.lastError ?? null,
|
|
1235
|
+
lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
|
|
1144
1236
|
lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
|
|
1145
1237
|
lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
|
|
1146
1238
|
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
@@ -1152,9 +1244,84 @@ function writeHeartbeat(options, client, health) {
|
|
|
1152
1244
|
`,
|
|
1153
1245
|
"utf8"
|
|
1154
1246
|
);
|
|
1247
|
+
heartbeatCount += 1;
|
|
1248
|
+
if (heartbeatCount % 5 === 0) {
|
|
1249
|
+
logStatus(
|
|
1250
|
+
`heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const status = client?.connected ? "active" : "idle";
|
|
1254
|
+
updateCommsHeartbeat(options, status);
|
|
1255
|
+
}
|
|
1256
|
+
var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
|
|
1257
|
+
var COMMS_LOCK_STALE_AGE_MS = 1e4;
|
|
1258
|
+
function acquireCommsLock(lockPath) {
|
|
1259
|
+
const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
|
|
1260
|
+
while (Date.now() < deadline) {
|
|
1261
|
+
try {
|
|
1262
|
+
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch {
|
|
1265
|
+
try {
|
|
1266
|
+
const lockAge = Date.now() - statSync(lockPath).mtimeMs;
|
|
1267
|
+
if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
|
|
1268
|
+
unlinkSync(lockPath);
|
|
1269
|
+
try {
|
|
1270
|
+
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1271
|
+
return true;
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
const start = Date.now();
|
|
1278
|
+
while (Date.now() - start < 50) {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
function releaseCommsLock(lockPath) {
|
|
1285
|
+
try {
|
|
1286
|
+
unlinkSync(lockPath);
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function updateCommsHeartbeat(options, status) {
|
|
1291
|
+
const heartbeatsPath = join(options.commsDir, "heartbeats.json");
|
|
1292
|
+
const lockPath = join(options.commsDir, ".heartbeats.lock");
|
|
1293
|
+
if (!acquireCommsLock(lockPath)) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
let store = {};
|
|
1298
|
+
try {
|
|
1299
|
+
store = JSON.parse(readFileSync(heartbeatsPath, "utf-8"));
|
|
1300
|
+
} catch {
|
|
1301
|
+
}
|
|
1302
|
+
const key = options.agentId;
|
|
1303
|
+
const existing = store[key];
|
|
1304
|
+
store[key] = {
|
|
1305
|
+
id: options.agentId,
|
|
1306
|
+
agent: options.agentName,
|
|
1307
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1308
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1309
|
+
joinedAt: existing?.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1310
|
+
status
|
|
1311
|
+
};
|
|
1312
|
+
const tmpPath = heartbeatsPath + ".tmp." + process.pid;
|
|
1313
|
+
writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
1314
|
+
renameSync(tmpPath, heartbeatsPath);
|
|
1315
|
+
} catch {
|
|
1316
|
+
} finally {
|
|
1317
|
+
releaseCommsLock(lockPath);
|
|
1318
|
+
}
|
|
1155
1319
|
}
|
|
1156
1320
|
async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
1157
1321
|
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
1322
|
+
logStatus(
|
|
1323
|
+
`dispatching from ${candidate.sender || "unknown"}: ${candidate.subject || "(none)"}`
|
|
1324
|
+
);
|
|
1158
1325
|
if (client.isBusy()) {
|
|
1159
1326
|
if (options.busyMode !== "steer") {
|
|
1160
1327
|
return false;
|
|
@@ -1184,6 +1351,7 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
|
1184
1351
|
}
|
|
1185
1352
|
if (shouldRetrySteerAsStart(error)) {
|
|
1186
1353
|
client.activeTurnId = null;
|
|
1354
|
+
client.turnStartedAt = null;
|
|
1187
1355
|
logStatus(
|
|
1188
1356
|
`steer fallback -> start for ${candidate.fileName} (${String(error)})`
|
|
1189
1357
|
);
|
|
@@ -1287,7 +1455,10 @@ async function main() {
|
|
|
1287
1455
|
options.messageLookbackMinutes,
|
|
1288
1456
|
options.processExistingMessages
|
|
1289
1457
|
);
|
|
1290
|
-
const initialSavedThread =
|
|
1458
|
+
const initialSavedThread = loadResumableThreadState(
|
|
1459
|
+
options.stateDir,
|
|
1460
|
+
options.appServerUrl
|
|
1461
|
+
);
|
|
1291
1462
|
logStatus("codex app-server bridge ready");
|
|
1292
1463
|
console.log(` repo: ${options.repoRoot}`);
|
|
1293
1464
|
console.log(` comms: ${options.commsDir}`);
|
|
@@ -1325,7 +1496,10 @@ async function main() {
|
|
|
1325
1496
|
options.gatewayToken
|
|
1326
1497
|
);
|
|
1327
1498
|
await client.connect();
|
|
1328
|
-
const savedThread =
|
|
1499
|
+
const savedThread = loadResumableThreadState(
|
|
1500
|
+
options.stateDir,
|
|
1501
|
+
options.appServerUrl
|
|
1502
|
+
);
|
|
1329
1503
|
const threadId = await client.ensureThread(
|
|
1330
1504
|
options.threadId,
|
|
1331
1505
|
savedThread,
|
|
@@ -1373,6 +1547,7 @@ async function main() {
|
|
|
1373
1547
|
}
|
|
1374
1548
|
client?.disconnect().catch(() => void 0);
|
|
1375
1549
|
client = null;
|
|
1550
|
+
logStatus(`reconnecting in ${options.reconnectSeconds}s...`);
|
|
1376
1551
|
await delay(options.reconnectSeconds * 1e3);
|
|
1377
1552
|
}
|
|
1378
1553
|
}
|
|
@@ -1412,6 +1587,7 @@ export {
|
|
|
1412
1587
|
buildUserInput,
|
|
1413
1588
|
chooseLoadedThreadForCwd,
|
|
1414
1589
|
isOwnMessageSender,
|
|
1590
|
+
loadResumableThreadState,
|
|
1415
1591
|
main,
|
|
1416
1592
|
maybeBootstrapHeadlessTurn,
|
|
1417
1593
|
recipientMatchesAgent,
|