@hua-labs/tap 0.2.3 → 0.2.5
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/README.md +194 -194
- package/dist/bridges/codex-app-server-auth-gateway.mjs +11 -3
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +11 -1
- package/dist/bridges/codex-app-server-bridge.mjs +109 -50
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +23 -17
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +1063 -237
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +446 -140
- package/dist/index.mjs.map +1 -1
- package/package.json +65 -65
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/commands/init.ts
|
|
2
2
|
import * as fs6 from "fs";
|
|
3
3
|
import * as path6 from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
5
|
|
|
6
6
|
// src/state.ts
|
|
7
7
|
import * as fs3 from "fs";
|
|
@@ -23,9 +23,16 @@ function detectPlatform() {
|
|
|
23
23
|
return process.platform;
|
|
24
24
|
}
|
|
25
25
|
var _noGitWarned = false;
|
|
26
|
+
var _loggedWarnings = /* @__PURE__ */ new Set();
|
|
26
27
|
function _setNoGitWarned() {
|
|
27
28
|
_noGitWarned = true;
|
|
28
29
|
}
|
|
30
|
+
function resetLoggedWarnings() {
|
|
31
|
+
_loggedWarnings.clear();
|
|
32
|
+
}
|
|
33
|
+
function wasWarningLogged(message) {
|
|
34
|
+
return _loggedWarnings.has(message);
|
|
35
|
+
}
|
|
29
36
|
function findRepoRoot(startDir = process.cwd()) {
|
|
30
37
|
let dir = path.resolve(startDir);
|
|
31
38
|
while (true) {
|
|
@@ -33,8 +40,8 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
33
40
|
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
34
41
|
if (!_noGitWarned) {
|
|
35
42
|
_setNoGitWarned();
|
|
36
|
-
|
|
37
|
-
"No .git directory found. Resolved
|
|
43
|
+
log(
|
|
44
|
+
"No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
38
45
|
);
|
|
39
46
|
}
|
|
40
47
|
return dir;
|
|
@@ -45,8 +52,8 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
45
52
|
}
|
|
46
53
|
if (!_noGitWarned) {
|
|
47
54
|
_setNoGitWarned();
|
|
48
|
-
|
|
49
|
-
"No git repository or package.json found. Using current directory as root.
|
|
55
|
+
log(
|
|
56
|
+
"No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
50
57
|
);
|
|
51
58
|
}
|
|
52
59
|
return process.cwd();
|
|
@@ -101,7 +108,9 @@ function logSuccess(message) {
|
|
|
101
108
|
if (!_jsonMode) console.log(` + ${message}`);
|
|
102
109
|
}
|
|
103
110
|
function logWarn(message) {
|
|
104
|
-
if (
|
|
111
|
+
if (_jsonMode) return;
|
|
112
|
+
_loggedWarnings.add(message);
|
|
113
|
+
console.log(` ! ${message}`);
|
|
105
114
|
}
|
|
106
115
|
function logError(message) {
|
|
107
116
|
if (!_jsonMode) console.error(` x ${message}`);
|
|
@@ -111,6 +120,16 @@ function logHeader(message) {
|
|
|
111
120
|
${message}
|
|
112
121
|
`);
|
|
113
122
|
}
|
|
123
|
+
function parseIntFlag(value, name, min, max) {
|
|
124
|
+
if (value === void 0) return void 0;
|
|
125
|
+
const parsed = Number(value);
|
|
126
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
127
|
+
throw new RangeError(
|
|
128
|
+
`Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
114
133
|
function resolveInstanceId(identifier, state) {
|
|
115
134
|
if (state.instances[identifier]) {
|
|
116
135
|
return { ok: true, instanceId: identifier };
|
|
@@ -160,8 +179,8 @@ function findRepoRoot2(startDir = process.cwd()) {
|
|
|
160
179
|
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
161
180
|
if (!_noGitWarned) {
|
|
162
181
|
_setNoGitWarned();
|
|
163
|
-
|
|
164
|
-
"
|
|
182
|
+
log(
|
|
183
|
+
"No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
165
184
|
);
|
|
166
185
|
}
|
|
167
186
|
return dir;
|
|
@@ -172,8 +191,8 @@ function findRepoRoot2(startDir = process.cwd()) {
|
|
|
172
191
|
}
|
|
173
192
|
if (!_noGitWarned) {
|
|
174
193
|
_setNoGitWarned();
|
|
175
|
-
|
|
176
|
-
"
|
|
194
|
+
log(
|
|
195
|
+
"No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
|
|
177
196
|
);
|
|
178
197
|
}
|
|
179
198
|
return process.cwd();
|
|
@@ -187,7 +206,7 @@ function loadJsonFile(filePath) {
|
|
|
187
206
|
return null;
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
|
-
function
|
|
209
|
+
function loadSharedConfig(repoRoot) {
|
|
191
210
|
return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
|
|
192
211
|
}
|
|
193
212
|
function loadLocalConfig(repoRoot) {
|
|
@@ -211,7 +230,7 @@ function loadLegacyShellConfig(repoRoot) {
|
|
|
211
230
|
}
|
|
212
231
|
function resolveConfig(overrides = {}, startDir) {
|
|
213
232
|
const repoRoot = findRepoRoot2(startDir);
|
|
214
|
-
const shared =
|
|
233
|
+
const shared = loadSharedConfig(repoRoot) ?? {};
|
|
215
234
|
const local = loadLocalConfig(repoRoot) ?? {};
|
|
216
235
|
const legacy = loadLegacyShellConfig(repoRoot) ?? {};
|
|
217
236
|
const sources = {
|
|
@@ -754,7 +773,39 @@ function parsePermissionMode(args) {
|
|
|
754
773
|
}
|
|
755
774
|
return "safe";
|
|
756
775
|
}
|
|
776
|
+
var INIT_HELP = `
|
|
777
|
+
Usage:
|
|
778
|
+
tap init [options]
|
|
779
|
+
|
|
780
|
+
Description:
|
|
781
|
+
Initialize the tap directory structure, state file, and permissions.
|
|
782
|
+
Optionally clone a shared comms repository.
|
|
783
|
+
|
|
784
|
+
Options:
|
|
785
|
+
--comms-dir <path> Override comms directory (default: tap-comms/)
|
|
786
|
+
--comms-repo <url> Clone a shared comms git repo into comms directory
|
|
787
|
+
--permissions <mode> Permission mode: safe (default) or full
|
|
788
|
+
--force Re-initialize even if already set up
|
|
789
|
+
--help, -h Show help
|
|
790
|
+
|
|
791
|
+
Examples:
|
|
792
|
+
npx @hua-labs/tap init
|
|
793
|
+
npx @hua-labs/tap init --permissions full
|
|
794
|
+
npx @hua-labs/tap init --comms-repo https://github.com/org/comms.git
|
|
795
|
+
npx @hua-labs/tap init --comms-dir /shared/comms --force
|
|
796
|
+
`.trim();
|
|
757
797
|
async function initCommand(args) {
|
|
798
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
799
|
+
log(INIT_HELP);
|
|
800
|
+
return {
|
|
801
|
+
ok: true,
|
|
802
|
+
command: "init",
|
|
803
|
+
code: "TAP_NO_OP",
|
|
804
|
+
message: INIT_HELP,
|
|
805
|
+
warnings: [],
|
|
806
|
+
data: {}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
758
809
|
const repoRoot = findRepoRoot();
|
|
759
810
|
const commsDir = resolveCommsDir(args, repoRoot);
|
|
760
811
|
const permMode = parsePermissionMode(args);
|
|
@@ -791,10 +842,19 @@ async function initCommand(args) {
|
|
|
791
842
|
} else {
|
|
792
843
|
log(`Cloning comms repo: ${commsRepoUrl}`);
|
|
793
844
|
try {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
845
|
+
const cloneResult = spawnSync(
|
|
846
|
+
"git",
|
|
847
|
+
["clone", commsRepoUrl, commsDir],
|
|
848
|
+
{
|
|
849
|
+
stdio: "pipe",
|
|
850
|
+
encoding: "utf-8"
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
if (cloneResult.status !== 0) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
cloneResult.stderr || `git clone exited with code ${cloneResult.status}`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
798
858
|
logSuccess(`Cloned comms repo to ${commsDir}`);
|
|
799
859
|
} catch (err) {
|
|
800
860
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -811,7 +871,7 @@ async function initCommand(args) {
|
|
|
811
871
|
}
|
|
812
872
|
}
|
|
813
873
|
{
|
|
814
|
-
const sharedConfig =
|
|
874
|
+
const sharedConfig = loadSharedConfig(repoRoot) ?? {};
|
|
815
875
|
let configChanged = false;
|
|
816
876
|
if (commsRepoUrl) {
|
|
817
877
|
sharedConfig.commsRepoUrl = commsRepoUrl;
|
|
@@ -903,17 +963,17 @@ ${entry}
|
|
|
903
963
|
// src/adapters/claude.ts
|
|
904
964
|
import * as fs8 from "fs";
|
|
905
965
|
import * as path8 from "path";
|
|
906
|
-
import { execSync
|
|
966
|
+
import { execSync } from "child_process";
|
|
907
967
|
|
|
908
968
|
// src/adapters/common.ts
|
|
909
969
|
import * as fs7 from "fs";
|
|
910
970
|
import * as os2 from "os";
|
|
911
971
|
import * as path7 from "path";
|
|
912
|
-
import { spawnSync } from "child_process";
|
|
972
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
913
973
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
914
974
|
function probeCommand(candidates) {
|
|
915
975
|
for (const candidate of candidates) {
|
|
916
|
-
const result =
|
|
976
|
+
const result = spawnSync2(candidate, ["--version"], {
|
|
917
977
|
encoding: "utf-8",
|
|
918
978
|
shell: process.platform === "win32"
|
|
919
979
|
});
|
|
@@ -991,7 +1051,7 @@ function findPreferredBunCommand() {
|
|
|
991
1051
|
const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
|
|
992
1052
|
for (const candidate of candidates) {
|
|
993
1053
|
if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
|
|
994
|
-
const result =
|
|
1054
|
+
const result = spawnSync2(candidate, ["--version"], {
|
|
995
1055
|
encoding: "utf-8",
|
|
996
1056
|
shell: process.platform === "win32"
|
|
997
1057
|
});
|
|
@@ -1017,7 +1077,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1017
1077
|
}
|
|
1018
1078
|
if (!sourcePath) {
|
|
1019
1079
|
issues.push(
|
|
1020
|
-
"tap
|
|
1080
|
+
"tap MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
1021
1081
|
);
|
|
1022
1082
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1023
1083
|
}
|
|
@@ -1047,7 +1107,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1047
1107
|
}
|
|
1048
1108
|
if (!command) {
|
|
1049
1109
|
issues.push(
|
|
1050
|
-
"bun is required to run the repo-local tap
|
|
1110
|
+
"bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
|
|
1051
1111
|
);
|
|
1052
1112
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1053
1113
|
}
|
|
@@ -1062,13 +1122,14 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1062
1122
|
}
|
|
1063
1123
|
|
|
1064
1124
|
// src/adapters/claude.ts
|
|
1065
|
-
var MCP_SERVER_KEY = "tap
|
|
1125
|
+
var MCP_SERVER_KEY = "tap";
|
|
1126
|
+
var OLD_MCP_SERVER_KEY = "tap-comms";
|
|
1066
1127
|
function findMcpJsonPath(ctx) {
|
|
1067
1128
|
return path8.join(ctx.repoRoot, ".mcp.json");
|
|
1068
1129
|
}
|
|
1069
1130
|
function findClaudeCommand() {
|
|
1070
1131
|
try {
|
|
1071
|
-
|
|
1132
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
1072
1133
|
return "claude";
|
|
1073
1134
|
} catch {
|
|
1074
1135
|
return null;
|
|
@@ -1140,6 +1201,11 @@ var claudeAdapter = {
|
|
|
1140
1201
|
`Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
|
|
1141
1202
|
);
|
|
1142
1203
|
}
|
|
1204
|
+
if (config.mcpServers?.[OLD_MCP_SERVER_KEY]) {
|
|
1205
|
+
conflicts.push(
|
|
1206
|
+
`Legacy "${OLD_MCP_SERVER_KEY}" entry will be migrated to "${MCP_SERVER_KEY}".`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1143
1209
|
} catch {
|
|
1144
1210
|
warnings.push(
|
|
1145
1211
|
".mcp.json exists but is not valid JSON. Will be overwritten."
|
|
@@ -1149,7 +1215,7 @@ var claudeAdapter = {
|
|
|
1149
1215
|
const serverEntry = buildMcpServerEntry(ctx);
|
|
1150
1216
|
if (!serverEntry) {
|
|
1151
1217
|
warnings.push(
|
|
1152
|
-
"tap
|
|
1218
|
+
"tap MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
|
|
1153
1219
|
);
|
|
1154
1220
|
return {
|
|
1155
1221
|
runtime: "claude",
|
|
@@ -1202,6 +1268,10 @@ var claudeAdapter = {
|
|
|
1202
1268
|
);
|
|
1203
1269
|
}
|
|
1204
1270
|
}
|
|
1271
|
+
const servers = config.mcpServers;
|
|
1272
|
+
if (servers?.[OLD_MCP_SERVER_KEY]) {
|
|
1273
|
+
delete servers[OLD_MCP_SERVER_KEY];
|
|
1274
|
+
}
|
|
1205
1275
|
if (op.key) {
|
|
1206
1276
|
setNestedKey(config, op.key, op.value);
|
|
1207
1277
|
}
|
|
@@ -1250,7 +1320,7 @@ var claudeAdapter = {
|
|
|
1250
1320
|
checks.push({ name: "Config is valid JSON", passed: true });
|
|
1251
1321
|
const entry = config.mcpServers?.[MCP_SERVER_KEY];
|
|
1252
1322
|
checks.push({
|
|
1253
|
-
name: "tap
|
|
1323
|
+
name: "tap entry present",
|
|
1254
1324
|
passed: !!entry,
|
|
1255
1325
|
message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
|
|
1256
1326
|
});
|
|
@@ -1343,8 +1413,10 @@ function readArtifactBackup(backupPath) {
|
|
|
1343
1413
|
}
|
|
1344
1414
|
|
|
1345
1415
|
// src/adapters/codex.ts
|
|
1346
|
-
var MCP_SELECTOR = "mcp_servers.tap
|
|
1347
|
-
var ENV_SELECTOR = "mcp_servers.tap
|
|
1416
|
+
var MCP_SELECTOR = "mcp_servers.tap";
|
|
1417
|
+
var ENV_SELECTOR = "mcp_servers.tap.env";
|
|
1418
|
+
var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1419
|
+
var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1348
1420
|
function findCodexConfigPath2() {
|
|
1349
1421
|
return path10.join(getHomeDir(), ".codex", "config.toml");
|
|
1350
1422
|
}
|
|
@@ -1398,12 +1470,12 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1398
1470
|
message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1399
1471
|
});
|
|
1400
1472
|
checks.push({
|
|
1401
|
-
name: "tap
|
|
1473
|
+
name: "tap MCP table present",
|
|
1402
1474
|
passed: !!mainTable,
|
|
1403
1475
|
message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
|
|
1404
1476
|
});
|
|
1405
1477
|
checks.push({
|
|
1406
|
-
name: "tap
|
|
1478
|
+
name: "tap env table present",
|
|
1407
1479
|
passed: !!envTable,
|
|
1408
1480
|
message: envTable ? void 0 : `${ENV_SELECTOR} not found`
|
|
1409
1481
|
});
|
|
@@ -1423,7 +1495,7 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1423
1495
|
passed: mainTable.includes(
|
|
1424
1496
|
`command = "${managed.command.replace(/\\/g, "\\\\")}"`
|
|
1425
1497
|
) && mainTable.includes(`args = [${expectedArgs}]`),
|
|
1426
|
-
message: "Managed tap
|
|
1498
|
+
message: "Managed tap command/args do not match expected values"
|
|
1427
1499
|
});
|
|
1428
1500
|
}
|
|
1429
1501
|
return checks;
|
|
@@ -1473,6 +1545,11 @@ var codexAdapter = {
|
|
|
1473
1545
|
if (extractTomlTable(content, MCP_SELECTOR)) {
|
|
1474
1546
|
conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
|
|
1475
1547
|
}
|
|
1548
|
+
if (extractTomlTable(content, OLD_MCP_SELECTOR)) {
|
|
1549
|
+
conflicts.push(
|
|
1550
|
+
`Legacy ${OLD_MCP_SELECTOR} table will be migrated to ${MCP_SELECTOR}.`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1476
1553
|
if (extractTomlTable(content, ENV_SELECTOR)) {
|
|
1477
1554
|
conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
|
|
1478
1555
|
}
|
|
@@ -1538,6 +1615,12 @@ var codexAdapter = {
|
|
|
1538
1615
|
return { ...artifact, backupPath };
|
|
1539
1616
|
});
|
|
1540
1617
|
let nextContent = existingContent;
|
|
1618
|
+
if (extractTomlTable(nextContent, OLD_ENV_SELECTOR)) {
|
|
1619
|
+
nextContent = removeTomlTable(nextContent, OLD_ENV_SELECTOR);
|
|
1620
|
+
}
|
|
1621
|
+
if (extractTomlTable(nextContent, OLD_MCP_SELECTOR)) {
|
|
1622
|
+
nextContent = removeTomlTable(nextContent, OLD_MCP_SELECTOR);
|
|
1623
|
+
}
|
|
1541
1624
|
nextContent = replaceTomlTable(
|
|
1542
1625
|
nextContent,
|
|
1543
1626
|
MCP_SELECTOR,
|
|
@@ -1651,7 +1734,8 @@ var codexAdapter = {
|
|
|
1651
1734
|
// src/adapters/gemini.ts
|
|
1652
1735
|
import * as fs11 from "fs";
|
|
1653
1736
|
import * as path11 from "path";
|
|
1654
|
-
var GEMINI_SELECTOR = "mcpServers.tap
|
|
1737
|
+
var GEMINI_SELECTOR = "mcpServers.tap";
|
|
1738
|
+
var OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
|
|
1655
1739
|
function candidateConfigPaths(ctx) {
|
|
1656
1740
|
const home = getHomeDir();
|
|
1657
1741
|
return [
|
|
@@ -1713,7 +1797,7 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1713
1797
|
message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1714
1798
|
});
|
|
1715
1799
|
checks.push({
|
|
1716
|
-
name: "tap
|
|
1800
|
+
name: "tap entry present",
|
|
1717
1801
|
passed: !!entry,
|
|
1718
1802
|
message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
|
|
1719
1803
|
});
|
|
@@ -1779,6 +1863,11 @@ var geminiAdapter = {
|
|
|
1779
1863
|
if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
|
|
1780
1864
|
conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
|
|
1781
1865
|
}
|
|
1866
|
+
if (readNestedKey(config, OLD_GEMINI_SELECTOR) !== void 0) {
|
|
1867
|
+
conflicts.push(
|
|
1868
|
+
`Legacy ${OLD_GEMINI_SELECTOR} entry will be migrated to ${GEMINI_SELECTOR}.`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1782
1871
|
} catch {
|
|
1783
1872
|
warnings.push(
|
|
1784
1873
|
`${configPath} exists but is not valid JSON. It will be replaced.`
|
|
@@ -1846,6 +1935,13 @@ var geminiAdapter = {
|
|
|
1846
1935
|
existed: previousValue !== void 0,
|
|
1847
1936
|
value: previousValue
|
|
1848
1937
|
});
|
|
1938
|
+
const oldValue = readNestedKey(config, OLD_GEMINI_SELECTOR);
|
|
1939
|
+
if (oldValue !== void 0) {
|
|
1940
|
+
const servers = config.mcpServers;
|
|
1941
|
+
if (servers) {
|
|
1942
|
+
delete servers["tap-comms"];
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1849
1945
|
setNestedKey2(config, GEMINI_SELECTOR, {
|
|
1850
1946
|
command: managed.command,
|
|
1851
1947
|
args: managed.args,
|
|
@@ -1927,15 +2023,16 @@ function getAdapter(runtime) {
|
|
|
1927
2023
|
// src/engine/bridge.ts
|
|
1928
2024
|
import * as fs13 from "fs";
|
|
1929
2025
|
import * as net from "net";
|
|
2026
|
+
import * as os3 from "os";
|
|
1930
2027
|
import * as path13 from "path";
|
|
1931
2028
|
import { randomBytes } from "crypto";
|
|
1932
|
-
import { spawn, spawnSync as
|
|
2029
|
+
import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
|
|
1933
2030
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1934
2031
|
|
|
1935
2032
|
// src/runtime/resolve-node.ts
|
|
1936
2033
|
import * as fs12 from "fs";
|
|
1937
2034
|
import * as path12 from "path";
|
|
1938
|
-
import { execSync as
|
|
2035
|
+
import { execSync as execSync2 } from "child_process";
|
|
1939
2036
|
function readNodeVersion(repoRoot) {
|
|
1940
2037
|
const nvFile = path12.join(repoRoot, ".node-version");
|
|
1941
2038
|
if (!fs12.existsSync(nvFile)) return null;
|
|
@@ -1978,7 +2075,7 @@ function probeFnmNode(desiredVersion) {
|
|
|
1978
2075
|
);
|
|
1979
2076
|
if (!fs12.existsSync(candidate)) continue;
|
|
1980
2077
|
try {
|
|
1981
|
-
const v =
|
|
2078
|
+
const v = execSync2(`"${candidate}" --version`, {
|
|
1982
2079
|
encoding: "utf-8",
|
|
1983
2080
|
timeout: 5e3
|
|
1984
2081
|
}).trim();
|
|
@@ -1992,7 +2089,7 @@ function probeFnmNode(desiredVersion) {
|
|
|
1992
2089
|
}
|
|
1993
2090
|
function detectNodeMajorVersion(command) {
|
|
1994
2091
|
try {
|
|
1995
|
-
const version2 =
|
|
2092
|
+
const version2 = execSync2(`"${command}" --version`, {
|
|
1996
2093
|
encoding: "utf-8",
|
|
1997
2094
|
timeout: 5e3
|
|
1998
2095
|
}).trim();
|
|
@@ -2006,7 +2103,7 @@ function checkStripTypesSupport(command) {
|
|
|
2006
2103
|
const major = detectNodeMajorVersion(command);
|
|
2007
2104
|
if (major !== null && major >= 22) return true;
|
|
2008
2105
|
try {
|
|
2009
|
-
|
|
2106
|
+
execSync2(`"${command}" --experimental-strip-types -e ""`, {
|
|
2010
2107
|
timeout: 5e3,
|
|
2011
2108
|
stdio: "pipe"
|
|
2012
2109
|
});
|
|
@@ -2097,8 +2194,10 @@ var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
|
2097
2194
|
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
2098
2195
|
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
2099
2196
|
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2100
|
-
var
|
|
2197
|
+
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
2101
2198
|
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2199
|
+
var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
|
|
2200
|
+
var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
|
|
2102
2201
|
function appServerLogFilePath(stateDir, instanceId) {
|
|
2103
2202
|
return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
|
|
2104
2203
|
}
|
|
@@ -2135,12 +2234,66 @@ function removeFileIfExists(filePath) {
|
|
|
2135
2234
|
} catch {
|
|
2136
2235
|
}
|
|
2137
2236
|
}
|
|
2237
|
+
function toPowerShellSingleQuotedString(value) {
|
|
2238
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
2239
|
+
}
|
|
2240
|
+
function toPowerShellStringArrayLiteral(values) {
|
|
2241
|
+
return `@(${values.map(toPowerShellSingleQuotedString).join(", ")})`;
|
|
2242
|
+
}
|
|
2243
|
+
function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
|
|
2244
|
+
let entries;
|
|
2245
|
+
try {
|
|
2246
|
+
entries = fs13.readdirSync(os3.tmpdir());
|
|
2247
|
+
} catch {
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
for (const entry of entries) {
|
|
2251
|
+
if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
const wrapperPath = path13.join(os3.tmpdir(), entry);
|
|
2255
|
+
try {
|
|
2256
|
+
const stats = fs13.statSync(wrapperPath);
|
|
2257
|
+
if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
fs13.unlinkSync(wrapperPath);
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
|
|
2266
|
+
const lines = ["$ErrorActionPreference = 'Stop'"];
|
|
2267
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2268
|
+
if (value !== void 0 && value !== process.env[key]) {
|
|
2269
|
+
lines.push(
|
|
2270
|
+
`[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
lines.push(
|
|
2275
|
+
`$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
|
|
2276
|
+
`$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
|
|
2277
|
+
`$commandPath = ${toPowerShellSingleQuotedString(command)}`,
|
|
2278
|
+
`$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
|
|
2279
|
+
"$exitCode = 1",
|
|
2280
|
+
"try {",
|
|
2281
|
+
" & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
|
|
2282
|
+
" $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
|
|
2283
|
+
"} finally {",
|
|
2284
|
+
" Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
|
|
2285
|
+
"}",
|
|
2286
|
+
"exit $exitCode"
|
|
2287
|
+
);
|
|
2288
|
+
return `${lines.join("\r\n")}\r
|
|
2289
|
+
`;
|
|
2290
|
+
}
|
|
2138
2291
|
function getWebSocketCtor() {
|
|
2139
2292
|
const candidate = globalThis.WebSocket;
|
|
2140
2293
|
return typeof candidate === "function" ? candidate : null;
|
|
2141
2294
|
}
|
|
2142
2295
|
function delay(ms) {
|
|
2143
|
-
return new Promise((
|
|
2296
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
2144
2297
|
}
|
|
2145
2298
|
function isLoopbackHost(hostname) {
|
|
2146
2299
|
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
@@ -2158,8 +2311,11 @@ function resolvePowerShellCommand() {
|
|
|
2158
2311
|
function resolveAuthGatewayScript(repoRoot) {
|
|
2159
2312
|
const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
|
|
2160
2313
|
const candidates = [
|
|
2161
|
-
|
|
2162
|
-
path13.join(moduleDir, "
|
|
2314
|
+
// Bundled: dist/bridges/ sibling (npm install / built package)
|
|
2315
|
+
path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
2316
|
+
// Source: src/bridges/ sibling (monorepo dev with ts runner)
|
|
2317
|
+
path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
|
|
2318
|
+
// Monorepo dist fallback
|
|
2163
2319
|
path13.join(
|
|
2164
2320
|
repoRoot,
|
|
2165
2321
|
"packages",
|
|
@@ -2189,7 +2345,7 @@ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
|
2189
2345
|
}
|
|
2190
2346
|
async function allocateLoopbackPort(hostname) {
|
|
2191
2347
|
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2192
|
-
return await new Promise((
|
|
2348
|
+
return await new Promise((resolve13, reject) => {
|
|
2193
2349
|
const server = net.createServer();
|
|
2194
2350
|
server.unref();
|
|
2195
2351
|
server.once("error", reject);
|
|
@@ -2207,15 +2363,13 @@ async function allocateLoopbackPort(hostname) {
|
|
|
2207
2363
|
reject(error);
|
|
2208
2364
|
return;
|
|
2209
2365
|
}
|
|
2210
|
-
|
|
2366
|
+
resolve13(port);
|
|
2211
2367
|
});
|
|
2212
2368
|
});
|
|
2213
2369
|
});
|
|
2214
2370
|
}
|
|
2215
|
-
function buildProtectedAppServerUrl(publicUrl,
|
|
2216
|
-
|
|
2217
|
-
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
2218
|
-
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
2371
|
+
function buildProtectedAppServerUrl(publicUrl, _token) {
|
|
2372
|
+
return publicUrl;
|
|
2219
2373
|
}
|
|
2220
2374
|
function readGatewayTokenFromPath(tokenPath) {
|
|
2221
2375
|
return fs13.readFileSync(tokenPath, "utf8").trim();
|
|
@@ -2330,7 +2484,7 @@ async function createManagedAppServerAuth(options) {
|
|
|
2330
2484
|
throw new Error("Failed to spawn app-server auth gateway");
|
|
2331
2485
|
}
|
|
2332
2486
|
return {
|
|
2333
|
-
mode: "
|
|
2487
|
+
mode: "subprotocol",
|
|
2334
2488
|
protectedUrl,
|
|
2335
2489
|
upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
|
|
2336
2490
|
tokenPath,
|
|
@@ -2392,35 +2546,50 @@ function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
|
2392
2546
|
return null;
|
|
2393
2547
|
}
|
|
2394
2548
|
function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
|
|
2395
|
-
const ext = path13.extname(command).toLowerCase();
|
|
2396
2549
|
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2550
|
+
const powerShellCommand = resolvePowerShellCommand();
|
|
2551
|
+
cleanupStaleWindowsSpawnWrappers();
|
|
2552
|
+
const wrapperPath = path13.join(
|
|
2553
|
+
os3.tmpdir(),
|
|
2554
|
+
`${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
|
|
2555
|
+
);
|
|
2556
|
+
fs13.writeFileSync(
|
|
2557
|
+
wrapperPath,
|
|
2558
|
+
buildWindowsDetachedWrapperScript(
|
|
2559
|
+
command,
|
|
2560
|
+
args,
|
|
2561
|
+
logPath,
|
|
2562
|
+
stderrLogPath,
|
|
2563
|
+
env
|
|
2564
|
+
)
|
|
2565
|
+
);
|
|
2566
|
+
const psCommand = [
|
|
2567
|
+
"$p = Start-Process",
|
|
2568
|
+
`-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
|
|
2569
|
+
`-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
|
|
2570
|
+
`-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
|
|
2571
|
+
"-WindowStyle Hidden",
|
|
2572
|
+
"-PassThru",
|
|
2573
|
+
"; Write-Output $p.Id"
|
|
2574
|
+
].join(" ");
|
|
2575
|
+
const result = spawnSync3(
|
|
2576
|
+
powerShellCommand,
|
|
2577
|
+
["-NoLogo", "-NoProfile", "-Command", psCommand],
|
|
2578
|
+
{
|
|
2579
|
+
encoding: "utf-8",
|
|
2580
|
+
windowsHide: true
|
|
2581
|
+
}
|
|
2582
|
+
);
|
|
2583
|
+
if (result.status !== 0) {
|
|
2584
|
+
removeFileIfExists(wrapperPath);
|
|
2585
|
+
return null;
|
|
2423
2586
|
}
|
|
2587
|
+
const pid = parseInt(result.stdout.trim(), 10);
|
|
2588
|
+
if (!Number.isFinite(pid)) {
|
|
2589
|
+
removeFileIfExists(wrapperPath);
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
return pid;
|
|
2424
2593
|
}
|
|
2425
2594
|
function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
|
|
2426
2595
|
return startWindowsDetachedProcess(
|
|
@@ -2444,7 +2613,7 @@ function findListeningProcessId(url, platform) {
|
|
|
2444
2613
|
if (port == null || !Number.isFinite(port)) {
|
|
2445
2614
|
return null;
|
|
2446
2615
|
}
|
|
2447
|
-
const result =
|
|
2616
|
+
const result = spawnSync3(
|
|
2448
2617
|
resolvePowerShellCommand(),
|
|
2449
2618
|
[
|
|
2450
2619
|
"-NoLogo",
|
|
@@ -2482,12 +2651,12 @@ function resolveAppServerUrl(baseUrl, port) {
|
|
|
2482
2651
|
}
|
|
2483
2652
|
async function isTcpPortAvailable(hostname, port) {
|
|
2484
2653
|
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2485
|
-
return await new Promise((
|
|
2654
|
+
return await new Promise((resolve13) => {
|
|
2486
2655
|
const server = net.createServer();
|
|
2487
2656
|
server.unref();
|
|
2488
|
-
server.once("error", () =>
|
|
2657
|
+
server.once("error", () => resolve13(false));
|
|
2489
2658
|
server.listen(port, bindHost, () => {
|
|
2490
|
-
server.close((error) =>
|
|
2659
|
+
server.close((error) => resolve13(!error));
|
|
2491
2660
|
});
|
|
2492
2661
|
});
|
|
2493
2662
|
}
|
|
@@ -2517,12 +2686,12 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
|
|
|
2517
2686
|
`Failed to find a free app-server port starting at ${basePort}`
|
|
2518
2687
|
);
|
|
2519
2688
|
}
|
|
2520
|
-
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2689
|
+
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
|
|
2521
2690
|
const WebSocket = getWebSocketCtor();
|
|
2522
2691
|
if (!WebSocket) {
|
|
2523
2692
|
return false;
|
|
2524
2693
|
}
|
|
2525
|
-
return new Promise((
|
|
2694
|
+
return new Promise((resolve13) => {
|
|
2526
2695
|
let settled = false;
|
|
2527
2696
|
let socket = null;
|
|
2528
2697
|
const finish = (healthy) => {
|
|
@@ -2535,11 +2704,12 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
2535
2704
|
socket?.close();
|
|
2536
2705
|
} catch {
|
|
2537
2706
|
}
|
|
2538
|
-
|
|
2707
|
+
resolve13(healthy);
|
|
2539
2708
|
};
|
|
2540
2709
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2541
2710
|
try {
|
|
2542
|
-
|
|
2711
|
+
const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
|
|
2712
|
+
socket = new WebSocket(url, protocols);
|
|
2543
2713
|
socket.addEventListener("open", () => finish(true), { once: true });
|
|
2544
2714
|
socket.addEventListener("error", () => finish(false), { once: true });
|
|
2545
2715
|
socket.addEventListener("close", () => finish(false), { once: true });
|
|
@@ -2548,10 +2718,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
2548
2718
|
}
|
|
2549
2719
|
});
|
|
2550
2720
|
}
|
|
2551
|
-
async function waitForAppServerHealth(url, timeoutMs) {
|
|
2721
|
+
async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
|
|
2552
2722
|
const deadline = Date.now() + timeoutMs;
|
|
2553
2723
|
while (Date.now() < deadline) {
|
|
2554
|
-
if (await checkAppServerHealth(
|
|
2724
|
+
if (await checkAppServerHealth(
|
|
2725
|
+
url,
|
|
2726
|
+
APP_SERVER_HEALTH_TIMEOUT_MS,
|
|
2727
|
+
gatewayToken
|
|
2728
|
+
)) {
|
|
2555
2729
|
return true;
|
|
2556
2730
|
}
|
|
2557
2731
|
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
@@ -2564,7 +2738,7 @@ async function terminateProcess(pid, platform) {
|
|
|
2564
2738
|
}
|
|
2565
2739
|
try {
|
|
2566
2740
|
if (platform === "win32") {
|
|
2567
|
-
|
|
2741
|
+
execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2568
2742
|
} else {
|
|
2569
2743
|
process.kill(pid, "SIGTERM");
|
|
2570
2744
|
await delay(2e3);
|
|
@@ -2816,8 +2990,9 @@ Or start it manually:
|
|
|
2816
2990
|
throw new Error("Tap auth gateway token is missing after startup.");
|
|
2817
2991
|
}
|
|
2818
2992
|
const gatewayHealthy = await waitForAppServerHealth(
|
|
2819
|
-
|
|
2820
|
-
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
2993
|
+
effectiveUrl,
|
|
2994
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS,
|
|
2995
|
+
gatewayToken
|
|
2821
2996
|
);
|
|
2822
2997
|
if (!gatewayHealthy) {
|
|
2823
2998
|
await terminateProcess(pid, options.platform);
|
|
@@ -2853,7 +3028,11 @@ function logFilePath(stateDir, instanceId) {
|
|
|
2853
3028
|
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
2854
3029
|
return path13.join(runtimeStateDir, "heartbeat.json");
|
|
2855
3030
|
}
|
|
2856
|
-
function
|
|
3031
|
+
function runtimeThreadStateFilePath(runtimeStateDir) {
|
|
3032
|
+
return path13.join(runtimeStateDir, "thread.json");
|
|
3033
|
+
}
|
|
3034
|
+
function loadRuntimeBridgeHeartbeat(bridgeState) {
|
|
3035
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
2857
3036
|
if (!runtimeStateDir) {
|
|
2858
3037
|
return null;
|
|
2859
3038
|
}
|
|
@@ -2862,13 +3041,35 @@ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
|
2862
3041
|
return null;
|
|
2863
3042
|
}
|
|
2864
3043
|
try {
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3044
|
+
return JSON.parse(
|
|
3045
|
+
fs13.readFileSync(heartbeatPath, "utf-8")
|
|
3046
|
+
);
|
|
2868
3047
|
} catch {
|
|
2869
3048
|
return null;
|
|
2870
3049
|
}
|
|
2871
3050
|
}
|
|
3051
|
+
function loadRuntimeBridgeThreadState(bridgeState) {
|
|
3052
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
3053
|
+
if (!runtimeStateDir) {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
3056
|
+
const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
|
|
3057
|
+
if (!fs13.existsSync(threadPath)) {
|
|
3058
|
+
return null;
|
|
3059
|
+
}
|
|
3060
|
+
try {
|
|
3061
|
+
const parsed = JSON.parse(
|
|
3062
|
+
fs13.readFileSync(threadPath, "utf-8")
|
|
3063
|
+
);
|
|
3064
|
+
return parsed.threadId ? parsed : null;
|
|
3065
|
+
} catch {
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
3070
|
+
const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
|
|
3071
|
+
return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
|
|
3072
|
+
}
|
|
2872
3073
|
function resolveHeartbeatTimestamp(state) {
|
|
2873
3074
|
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
2874
3075
|
}
|
|
@@ -3038,6 +3239,7 @@ async function startBridge(options) {
|
|
|
3038
3239
|
options.messageLookbackMinutes
|
|
3039
3240
|
)
|
|
3040
3241
|
} : {},
|
|
3242
|
+
...process.env.TAP_COLD_START_WARMUP === "true" ? { TAP_COLD_START_WARMUP: "true" } : {},
|
|
3041
3243
|
...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
|
|
3042
3244
|
...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
|
|
3043
3245
|
...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
|
|
@@ -3169,8 +3371,49 @@ function getBridgeStatus(stateDir, instanceId) {
|
|
|
3169
3371
|
}
|
|
3170
3372
|
|
|
3171
3373
|
// src/commands/add.ts
|
|
3374
|
+
var ADD_HELP = `
|
|
3375
|
+
Usage:
|
|
3376
|
+
tap add <claude|codex|gemini> [options]
|
|
3377
|
+
|
|
3378
|
+
Description:
|
|
3379
|
+
Install a runtime instance and configure it to use tap.
|
|
3380
|
+
|
|
3381
|
+
Options:
|
|
3382
|
+
--name <name> Instance name (default: runtime name)
|
|
3383
|
+
--port <port> Port for app-server bridge
|
|
3384
|
+
--agent-name <name> Agent display name for bridge identification
|
|
3385
|
+
--force Re-install even if already configured
|
|
3386
|
+
--headless Enable headless reviewer mode (requires --name)
|
|
3387
|
+
--role <role> Headless role: reviewer, validator, long-running
|
|
3388
|
+
--help, -h Show help
|
|
3389
|
+
|
|
3390
|
+
Examples:
|
|
3391
|
+
npx @hua-labs/tap add claude
|
|
3392
|
+
npx @hua-labs/tap add codex --name reviewer --port 4501 --headless --role reviewer
|
|
3393
|
+
`.trim();
|
|
3394
|
+
function normalizeAgentName(value) {
|
|
3395
|
+
if (typeof value !== "string") {
|
|
3396
|
+
return null;
|
|
3397
|
+
}
|
|
3398
|
+
const trimmed = value.trim();
|
|
3399
|
+
return trimmed ? trimmed : null;
|
|
3400
|
+
}
|
|
3401
|
+
function resolveAgentName2(options) {
|
|
3402
|
+
return normalizeAgentName(options.explicit) ?? normalizeAgentName(options.stored) ?? normalizeAgentName(options.env) ?? normalizeAgentName(options.fallback) ?? null;
|
|
3403
|
+
}
|
|
3172
3404
|
async function addCommand(args) {
|
|
3173
3405
|
const { positional, flags } = parseArgs(args);
|
|
3406
|
+
if (flags["help"] === true || flags["h"] === true) {
|
|
3407
|
+
log(ADD_HELP);
|
|
3408
|
+
return {
|
|
3409
|
+
ok: true,
|
|
3410
|
+
command: "add",
|
|
3411
|
+
code: "TAP_NO_OP",
|
|
3412
|
+
message: ADD_HELP,
|
|
3413
|
+
warnings: [],
|
|
3414
|
+
data: {}
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3174
3417
|
const runtimeArg = positional[0];
|
|
3175
3418
|
if (!runtimeArg) {
|
|
3176
3419
|
return {
|
|
@@ -3196,8 +3439,10 @@ async function addCommand(args) {
|
|
|
3196
3439
|
const instanceName = typeof flags["name"] === "string" ? flags["name"] : void 0;
|
|
3197
3440
|
const instanceId = buildInstanceId(runtime, instanceName);
|
|
3198
3441
|
const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
|
|
3199
|
-
const port = portStr ?
|
|
3200
|
-
const agentNameFlag =
|
|
3442
|
+
const port = portStr ? Number(portStr) : null;
|
|
3443
|
+
const agentNameFlag = normalizeAgentName(
|
|
3444
|
+
typeof flags["agent-name"] === "string" ? flags["agent-name"] : null
|
|
3445
|
+
);
|
|
3201
3446
|
const force = flags["force"] === true;
|
|
3202
3447
|
const headlessFlag = flags["headless"] === true;
|
|
3203
3448
|
const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
|
|
@@ -3232,20 +3477,21 @@ async function addCommand(args) {
|
|
|
3232
3477
|
maxRounds: 5,
|
|
3233
3478
|
qualitySeverityFloor: "high"
|
|
3234
3479
|
} : null;
|
|
3235
|
-
if (portStr && (port === null || isNaN(port))) {
|
|
3480
|
+
if (portStr && (port === null || isNaN(port) || port < 1 || port > 65535)) {
|
|
3236
3481
|
return {
|
|
3237
3482
|
ok: false,
|
|
3238
3483
|
command: "add",
|
|
3239
3484
|
runtime,
|
|
3240
3485
|
instanceId,
|
|
3241
3486
|
code: "TAP_INVALID_ARGUMENT",
|
|
3242
|
-
message: `Invalid port: ${portStr}
|
|
3487
|
+
message: `Invalid port: ${portStr}. Must be between 1 and 65535.`,
|
|
3243
3488
|
warnings: [],
|
|
3244
3489
|
data: {}
|
|
3245
3490
|
};
|
|
3246
3491
|
}
|
|
3247
3492
|
const repoRoot = findRepoRoot();
|
|
3248
3493
|
const state = loadState(repoRoot);
|
|
3494
|
+
const adapter = getAdapter(runtime);
|
|
3249
3495
|
if (!state) {
|
|
3250
3496
|
return {
|
|
3251
3497
|
ok: false,
|
|
@@ -3258,7 +3504,39 @@ async function addCommand(args) {
|
|
|
3258
3504
|
data: {}
|
|
3259
3505
|
};
|
|
3260
3506
|
}
|
|
3261
|
-
|
|
3507
|
+
const existingInstance = state.instances[instanceId];
|
|
3508
|
+
const mode = adapter.bridgeMode();
|
|
3509
|
+
const envAgentName = normalizeAgentName(
|
|
3510
|
+
process.env.TAP_AGENT_NAME ?? process.env.CODEX_TAP_AGENT_NAME
|
|
3511
|
+
);
|
|
3512
|
+
const defaultAgentName = mode === "app-server" ? instanceId : null;
|
|
3513
|
+
const resolvedAgentName = resolveAgentName2({
|
|
3514
|
+
explicit: agentNameFlag,
|
|
3515
|
+
env: envAgentName,
|
|
3516
|
+
stored: existingInstance?.agentName ?? null,
|
|
3517
|
+
fallback: defaultAgentName
|
|
3518
|
+
});
|
|
3519
|
+
if (existingInstance?.installed && !force) {
|
|
3520
|
+
if (resolvedAgentName !== existingInstance.agentName) {
|
|
3521
|
+
const updatedState = updateInstanceState(state, instanceId, {
|
|
3522
|
+
...existingInstance,
|
|
3523
|
+
agentName: resolvedAgentName
|
|
3524
|
+
});
|
|
3525
|
+
saveState(repoRoot, updatedState);
|
|
3526
|
+
return {
|
|
3527
|
+
ok: true,
|
|
3528
|
+
command: "add",
|
|
3529
|
+
runtime,
|
|
3530
|
+
instanceId,
|
|
3531
|
+
code: "TAP_ADD_OK",
|
|
3532
|
+
message: resolvedAgentName === null ? `${instanceId} updated` : `${instanceId} agent name updated to "${resolvedAgentName}".`,
|
|
3533
|
+
warnings: [],
|
|
3534
|
+
data: {
|
|
3535
|
+
updatedFields: ["agentName"],
|
|
3536
|
+
agentName: resolvedAgentName
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3262
3540
|
return {
|
|
3263
3541
|
ok: true,
|
|
3264
3542
|
command: "add",
|
|
@@ -3288,14 +3566,12 @@ async function addCommand(args) {
|
|
|
3288
3566
|
logHeader(`@hua-labs/tap add ${instanceId}`);
|
|
3289
3567
|
if (instanceName) log(`Instance name: ${instanceName}`);
|
|
3290
3568
|
if (port !== null) log(`Port: ${port}`);
|
|
3291
|
-
|
|
3292
|
-
const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
|
|
3569
|
+
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
3293
3570
|
const ctx = {
|
|
3294
3571
|
...createAdapterContext(state.commsDir, repoRoot),
|
|
3295
3572
|
instanceId,
|
|
3296
|
-
agentName:
|
|
3573
|
+
agentName: resolvedAgentName ?? void 0
|
|
3297
3574
|
};
|
|
3298
|
-
const adapter = getAdapter(runtime);
|
|
3299
3575
|
const warnings = [];
|
|
3300
3576
|
log("Probing runtime...");
|
|
3301
3577
|
const probe = await adapter.probe(ctx);
|
|
@@ -3375,7 +3651,6 @@ async function addCommand(args) {
|
|
|
3375
3651
|
);
|
|
3376
3652
|
}
|
|
3377
3653
|
let bridge = null;
|
|
3378
|
-
const mode = adapter.bridgeMode();
|
|
3379
3654
|
if (mode === "app-server") {
|
|
3380
3655
|
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
3381
3656
|
if (!bridgeScript) {
|
|
@@ -3383,36 +3658,34 @@ async function addCommand(args) {
|
|
|
3383
3658
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
3384
3659
|
} else {
|
|
3385
3660
|
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3386
|
-
{
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
warnings.push(`Bridge not started: ${msg}`);
|
|
3408
|
-
}
|
|
3661
|
+
log(`Starting bridge: ${bridgeScript}`);
|
|
3662
|
+
try {
|
|
3663
|
+
bridge = await startBridge({
|
|
3664
|
+
instanceId,
|
|
3665
|
+
runtime,
|
|
3666
|
+
stateDir: ctx.stateDir,
|
|
3667
|
+
commsDir: ctx.commsDir,
|
|
3668
|
+
bridgeScript,
|
|
3669
|
+
platform: ctx.platform,
|
|
3670
|
+
agentName: resolvedAgentName ?? void 0,
|
|
3671
|
+
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
3672
|
+
appServerUrl: resolvedCfg.appServerUrl,
|
|
3673
|
+
repoRoot,
|
|
3674
|
+
port: port ?? void 0,
|
|
3675
|
+
headless
|
|
3676
|
+
});
|
|
3677
|
+
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
3678
|
+
} catch (err) {
|
|
3679
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3680
|
+
logWarn(`Bridge not started: ${msg}`);
|
|
3681
|
+
warnings.push(`Bridge not started: ${msg}`);
|
|
3409
3682
|
}
|
|
3410
3683
|
}
|
|
3411
3684
|
}
|
|
3412
3685
|
const instanceState = {
|
|
3413
3686
|
instanceId,
|
|
3414
3687
|
runtime,
|
|
3415
|
-
agentName:
|
|
3688
|
+
agentName: resolvedAgentName,
|
|
3416
3689
|
port,
|
|
3417
3690
|
installed: true,
|
|
3418
3691
|
configPath: probe.configPath ?? "",
|
|
@@ -3424,7 +3697,7 @@ async function addCommand(args) {
|
|
|
3424
3697
|
lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
3425
3698
|
bridge,
|
|
3426
3699
|
headless,
|
|
3427
|
-
warnings: [...result.warnings, ...verify.warnings]
|
|
3700
|
+
warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
|
|
3428
3701
|
};
|
|
3429
3702
|
const newState = updateInstanceState(state, instanceId, instanceState);
|
|
3430
3703
|
saveState(repoRoot, newState);
|
|
@@ -3432,6 +3705,13 @@ async function addCommand(args) {
|
|
|
3432
3705
|
if (result.restartRequired) {
|
|
3433
3706
|
logWarn(`Restart ${runtime} to pick up the new configuration.`);
|
|
3434
3707
|
}
|
|
3708
|
+
if (runtime === "claude") {
|
|
3709
|
+
log("");
|
|
3710
|
+
log("For real-time notifications:");
|
|
3711
|
+
log(" claude --dangerously-load-development-channels server:tap-comms");
|
|
3712
|
+
log("Or polling mode (tools still work):");
|
|
3713
|
+
log(" claude");
|
|
3714
|
+
}
|
|
3435
3715
|
logHeader("Done!");
|
|
3436
3716
|
return {
|
|
3437
3717
|
ok: true,
|
|
@@ -3451,6 +3731,16 @@ async function addCommand(args) {
|
|
|
3451
3731
|
}
|
|
3452
3732
|
|
|
3453
3733
|
// src/commands/status.ts
|
|
3734
|
+
var STATUS_HELP = `
|
|
3735
|
+
Usage:
|
|
3736
|
+
tap status
|
|
3737
|
+
|
|
3738
|
+
Description:
|
|
3739
|
+
Show all installed instances, their bridge status, and configuration info.
|
|
3740
|
+
|
|
3741
|
+
Examples:
|
|
3742
|
+
npx @hua-labs/tap status
|
|
3743
|
+
`.trim();
|
|
3454
3744
|
function resolveStatus(inst, stateDir) {
|
|
3455
3745
|
if (!inst.installed) return "not installed";
|
|
3456
3746
|
switch (inst.bridgeMode) {
|
|
@@ -3477,7 +3767,18 @@ function instanceStatusLine(inst, status) {
|
|
|
3477
3767
|
const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
|
|
3478
3768
|
return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
|
|
3479
3769
|
}
|
|
3480
|
-
async function statusCommand(
|
|
3770
|
+
async function statusCommand(args) {
|
|
3771
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3772
|
+
log(STATUS_HELP);
|
|
3773
|
+
return {
|
|
3774
|
+
ok: true,
|
|
3775
|
+
command: "status",
|
|
3776
|
+
code: "TAP_NO_OP",
|
|
3777
|
+
message: STATUS_HELP,
|
|
3778
|
+
warnings: [],
|
|
3779
|
+
data: {}
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3481
3782
|
const repoRoot = findRepoRoot();
|
|
3482
3783
|
const state = loadState(repoRoot);
|
|
3483
3784
|
if (!state) {
|
|
@@ -3706,7 +4007,32 @@ function cleanEmptyParents(obj, keyPath) {
|
|
|
3706
4007
|
}
|
|
3707
4008
|
|
|
3708
4009
|
// src/commands/remove.ts
|
|
4010
|
+
var REMOVE_HELP = `
|
|
4011
|
+
Usage:
|
|
4012
|
+
tap remove <instance>
|
|
4013
|
+
|
|
4014
|
+
Description:
|
|
4015
|
+
Remove a registered instance, stop its bridge, and rollback config changes.
|
|
4016
|
+
|
|
4017
|
+
Arguments:
|
|
4018
|
+
<instance> Instance ID or runtime name (e.g. claude, codex-reviewer)
|
|
4019
|
+
|
|
4020
|
+
Examples:
|
|
4021
|
+
npx @hua-labs/tap remove claude
|
|
4022
|
+
npx @hua-labs/tap remove codex-reviewer
|
|
4023
|
+
`.trim();
|
|
3709
4024
|
async function removeCommand(args) {
|
|
4025
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4026
|
+
log(REMOVE_HELP);
|
|
4027
|
+
return {
|
|
4028
|
+
ok: true,
|
|
4029
|
+
command: "remove",
|
|
4030
|
+
code: "TAP_NO_OP",
|
|
4031
|
+
message: REMOVE_HELP,
|
|
4032
|
+
warnings: [],
|
|
4033
|
+
data: {}
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
3710
4036
|
const identifier = args.find((a) => !a.startsWith("-"));
|
|
3711
4037
|
if (!identifier) {
|
|
3712
4038
|
return {
|
|
@@ -3742,8 +4068,8 @@ async function removeCommand(args) {
|
|
|
3742
4068
|
};
|
|
3743
4069
|
}
|
|
3744
4070
|
const instanceId = resolved.instanceId;
|
|
3745
|
-
const
|
|
3746
|
-
if (!
|
|
4071
|
+
const instance2 = state.instances[instanceId];
|
|
4072
|
+
if (!instance2?.installed) {
|
|
3747
4073
|
return {
|
|
3748
4074
|
ok: true,
|
|
3749
4075
|
command: "remove",
|
|
@@ -3755,7 +4081,7 @@ async function removeCommand(args) {
|
|
|
3755
4081
|
};
|
|
3756
4082
|
}
|
|
3757
4083
|
logHeader(`@hua-labs/tap remove ${instanceId}`);
|
|
3758
|
-
if (
|
|
4084
|
+
if (instance2.bridge) {
|
|
3759
4085
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
3760
4086
|
const stopped = await stopBridge({
|
|
3761
4087
|
instanceId,
|
|
@@ -3768,7 +4094,7 @@ async function removeCommand(args) {
|
|
|
3768
4094
|
log(`No running bridge for ${instanceId}`);
|
|
3769
4095
|
}
|
|
3770
4096
|
}
|
|
3771
|
-
const result = await rollbackRuntime(instanceId,
|
|
4097
|
+
const result = await rollbackRuntime(instanceId, instance2);
|
|
3772
4098
|
if (result.success) {
|
|
3773
4099
|
logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
|
|
3774
4100
|
for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
|
|
@@ -3780,7 +4106,7 @@ async function removeCommand(args) {
|
|
|
3780
4106
|
ok: true,
|
|
3781
4107
|
command: "remove",
|
|
3782
4108
|
instanceId,
|
|
3783
|
-
runtime:
|
|
4109
|
+
runtime: instance2.runtime,
|
|
3784
4110
|
code: "TAP_REMOVE_OK",
|
|
3785
4111
|
message: `${instanceId} removed successfully`,
|
|
3786
4112
|
warnings: [],
|
|
@@ -3795,7 +4121,7 @@ async function removeCommand(args) {
|
|
|
3795
4121
|
ok: false,
|
|
3796
4122
|
command: "remove",
|
|
3797
4123
|
instanceId,
|
|
3798
|
-
runtime:
|
|
4124
|
+
runtime: instance2.runtime,
|
|
3799
4125
|
code: "TAP_ROLLBACK_FAILED",
|
|
3800
4126
|
message: "Rollback had errors. State preserved for retry.",
|
|
3801
4127
|
warnings: result.errors,
|
|
@@ -3812,7 +4138,7 @@ function formatAge(seconds) {
|
|
|
3812
4138
|
}
|
|
3813
4139
|
var BRIDGE_HELP = `
|
|
3814
4140
|
Usage:
|
|
3815
|
-
tap
|
|
4141
|
+
tap bridge <subcommand> [instance] [options]
|
|
3816
4142
|
|
|
3817
4143
|
Subcommands:
|
|
3818
4144
|
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
@@ -3824,7 +4150,7 @@ Subcommands:
|
|
|
3824
4150
|
|
|
3825
4151
|
Options:
|
|
3826
4152
|
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
3827
|
-
|
|
4153
|
+
Overrides the stored name from 'tap add' when needed
|
|
3828
4154
|
--all Start all registered app-server instances
|
|
3829
4155
|
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
3830
4156
|
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
@@ -3860,16 +4186,31 @@ function redactProtectedUrl(url) {
|
|
|
3860
4186
|
try {
|
|
3861
4187
|
const parsed = new URL(url);
|
|
3862
4188
|
if (parsed.searchParams.has("tap_token")) {
|
|
3863
|
-
parsed.searchParams.
|
|
4189
|
+
parsed.searchParams.delete("tap_token");
|
|
3864
4190
|
}
|
|
3865
4191
|
return parsed.toString().replace(/\/$/, "");
|
|
3866
4192
|
} catch {
|
|
3867
|
-
return url.replace(/tap_token=[^&]+/g, "
|
|
4193
|
+
return url.replace(/[?&]tap_token=[^&]+/g, "");
|
|
3868
4194
|
}
|
|
3869
4195
|
}
|
|
3870
4196
|
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
3871
4197
|
return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
|
|
3872
4198
|
}
|
|
4199
|
+
function formatThreadSummary(threadId, cwd) {
|
|
4200
|
+
if (!threadId) {
|
|
4201
|
+
return "-";
|
|
4202
|
+
}
|
|
4203
|
+
return cwd ? `${threadId} (${cwd})` : threadId;
|
|
4204
|
+
}
|
|
4205
|
+
function normalizeComparablePath(value) {
|
|
4206
|
+
return path14.resolve(value).replace(/\\/g, "/").toLowerCase();
|
|
4207
|
+
}
|
|
4208
|
+
function sameOptionalPath(left, right) {
|
|
4209
|
+
if (!left || !right) {
|
|
4210
|
+
return left === right;
|
|
4211
|
+
}
|
|
4212
|
+
return normalizeComparablePath(left) === normalizeComparablePath(right);
|
|
4213
|
+
}
|
|
3873
4214
|
function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
|
|
3874
4215
|
const shared = [];
|
|
3875
4216
|
for (const [id, inst] of Object.entries(state.instances)) {
|
|
@@ -3947,37 +4288,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3947
4288
|
};
|
|
3948
4289
|
}
|
|
3949
4290
|
const instanceId = resolved.instanceId;
|
|
3950
|
-
let
|
|
3951
|
-
if (!
|
|
4291
|
+
let instance2 = state.instances[instanceId];
|
|
4292
|
+
if (!instance2?.installed) {
|
|
3952
4293
|
return {
|
|
3953
4294
|
ok: false,
|
|
3954
4295
|
command: "bridge",
|
|
3955
4296
|
instanceId,
|
|
3956
|
-
runtime:
|
|
4297
|
+
runtime: instance2?.runtime,
|
|
3957
4298
|
code: "TAP_INSTANCE_NOT_FOUND",
|
|
3958
|
-
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${
|
|
4299
|
+
message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
|
|
3959
4300
|
warnings: [],
|
|
3960
4301
|
data: {}
|
|
3961
4302
|
};
|
|
3962
4303
|
}
|
|
3963
|
-
const adapter = getAdapter(
|
|
4304
|
+
const adapter = getAdapter(instance2.runtime);
|
|
3964
4305
|
const mode = adapter.bridgeMode();
|
|
3965
4306
|
if (mode !== "app-server") {
|
|
3966
4307
|
return {
|
|
3967
4308
|
ok: true,
|
|
3968
4309
|
command: "bridge",
|
|
3969
4310
|
instanceId,
|
|
3970
|
-
runtime:
|
|
4311
|
+
runtime: instance2.runtime,
|
|
3971
4312
|
code: "TAP_NO_OP",
|
|
3972
4313
|
message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
|
|
3973
4314
|
warnings: [],
|
|
3974
4315
|
data: { bridgeMode: mode }
|
|
3975
4316
|
};
|
|
3976
4317
|
}
|
|
3977
|
-
const resolvedAgentName = agentName ??
|
|
3978
|
-
if (agentName && agentName !==
|
|
3979
|
-
|
|
3980
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
4318
|
+
const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
|
|
4319
|
+
if (agentName && agentName !== instance2.agentName) {
|
|
4320
|
+
instance2 = { ...instance2, agentName };
|
|
4321
|
+
const updatedState = updateInstanceState(state, instanceId, instance2);
|
|
3981
4322
|
saveState(repoRoot, updatedState);
|
|
3982
4323
|
state = updatedState;
|
|
3983
4324
|
}
|
|
@@ -3988,7 +4329,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3988
4329
|
ok: false,
|
|
3989
4330
|
command: "bridge",
|
|
3990
4331
|
instanceId,
|
|
3991
|
-
runtime:
|
|
4332
|
+
runtime: instance2.runtime,
|
|
3992
4333
|
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
3993
4334
|
message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
|
|
3994
4335
|
warnings: [],
|
|
@@ -3997,8 +4338,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3997
4338
|
}
|
|
3998
4339
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
3999
4340
|
const runtimeCommand = resolvedConfig.runtimeCommand;
|
|
4000
|
-
const manageAppServer =
|
|
4001
|
-
let effectivePort =
|
|
4341
|
+
const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
|
|
4342
|
+
let effectivePort = instance2.port;
|
|
4002
4343
|
if (effectivePort == null && manageAppServer) {
|
|
4003
4344
|
effectivePort = await findNextAvailableAppServerPort(
|
|
4004
4345
|
state,
|
|
@@ -4006,8 +4347,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4006
4347
|
4501,
|
|
4007
4348
|
instanceId
|
|
4008
4349
|
);
|
|
4009
|
-
|
|
4010
|
-
const updatedState = updateInstanceState(state, instanceId,
|
|
4350
|
+
instance2 = { ...instance2, port: effectivePort };
|
|
4351
|
+
const updatedState = updateInstanceState(state, instanceId, instance2);
|
|
4011
4352
|
saveState(repoRoot, updatedState);
|
|
4012
4353
|
state = updatedState;
|
|
4013
4354
|
}
|
|
@@ -4023,19 +4364,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4023
4364
|
if (effectivePort != null) log(`Port: ${effectivePort}`);
|
|
4024
4365
|
if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
|
|
4025
4366
|
const noAuth = flags["no-auth"] === true;
|
|
4026
|
-
if (!manageAppServer &&
|
|
4367
|
+
if (!manageAppServer && instance2.runtime === "codex") {
|
|
4027
4368
|
log("Auto server: disabled (--no-server)");
|
|
4028
4369
|
}
|
|
4029
4370
|
if (noAuth && manageAppServer) {
|
|
4030
4371
|
log("Auth gateway: disabled (--no-auth)");
|
|
4031
4372
|
}
|
|
4032
|
-
const willBeHeadless = flags["headless"] === true ||
|
|
4373
|
+
const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
|
|
4033
4374
|
if (willBeHeadless) {
|
|
4034
|
-
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ??
|
|
4375
|
+
const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
|
|
4035
4376
|
log(`Headless: ${role}`);
|
|
4036
4377
|
}
|
|
4037
4378
|
try {
|
|
4038
|
-
if (!manageAppServer &&
|
|
4379
|
+
if (!manageAppServer && instance2.runtime === "codex") {
|
|
4039
4380
|
log("Checking app-server health...");
|
|
4040
4381
|
const healthy = await checkAppServerHealth(appServerUrl);
|
|
4041
4382
|
if (healthy) {
|
|
@@ -4046,7 +4387,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4046
4387
|
ok: false,
|
|
4047
4388
|
command: "bridge",
|
|
4048
4389
|
instanceId,
|
|
4049
|
-
runtime:
|
|
4390
|
+
runtime: instance2.runtime,
|
|
4050
4391
|
code: "TAP_BRIDGE_START_FAILED",
|
|
4051
4392
|
message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
|
|
4052
4393
|
warnings: [],
|
|
@@ -4060,7 +4401,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4060
4401
|
ok: false,
|
|
4061
4402
|
command: "bridge",
|
|
4062
4403
|
instanceId,
|
|
4063
|
-
runtime:
|
|
4404
|
+
runtime: instance2.runtime,
|
|
4064
4405
|
code: "TAP_INVALID_ARGUMENT",
|
|
4065
4406
|
message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
|
|
4066
4407
|
warnings: [],
|
|
@@ -4068,9 +4409,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4068
4409
|
};
|
|
4069
4410
|
}
|
|
4070
4411
|
const busyMode = busyModeRaw;
|
|
4071
|
-
const
|
|
4072
|
-
const
|
|
4073
|
-
const
|
|
4412
|
+
const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
|
|
4413
|
+
const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
|
|
4414
|
+
const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
|
|
4415
|
+
let pollSeconds;
|
|
4416
|
+
let reconnectSeconds;
|
|
4417
|
+
let messageLookbackMinutes;
|
|
4418
|
+
try {
|
|
4419
|
+
pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
|
|
4420
|
+
reconnectSeconds = parseIntFlag(
|
|
4421
|
+
reconnectSecondsRaw,
|
|
4422
|
+
"--reconnect-seconds",
|
|
4423
|
+
1,
|
|
4424
|
+
3600
|
|
4425
|
+
);
|
|
4426
|
+
messageLookbackMinutes = parseIntFlag(
|
|
4427
|
+
lookbackRaw,
|
|
4428
|
+
"--message-lookback-minutes",
|
|
4429
|
+
1,
|
|
4430
|
+
10080
|
|
4431
|
+
);
|
|
4432
|
+
} catch (err) {
|
|
4433
|
+
return {
|
|
4434
|
+
ok: false,
|
|
4435
|
+
command: "bridge",
|
|
4436
|
+
instanceId,
|
|
4437
|
+
runtime: instance2.runtime,
|
|
4438
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4439
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4440
|
+
warnings: [],
|
|
4441
|
+
data: {}
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4074
4444
|
const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
|
|
4075
4445
|
const ephemeral = flags["ephemeral"] === true;
|
|
4076
4446
|
const processExistingMessages = flags["process-existing-messages"] === true;
|
|
@@ -4082,7 +4452,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4082
4452
|
ok: false,
|
|
4083
4453
|
command: "bridge",
|
|
4084
4454
|
instanceId,
|
|
4085
|
-
runtime:
|
|
4455
|
+
runtime: instance2.runtime,
|
|
4086
4456
|
code: "TAP_INVALID_ARGUMENT",
|
|
4087
4457
|
message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
|
|
4088
4458
|
warnings: [],
|
|
@@ -4094,10 +4464,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4094
4464
|
role: roleArg ?? "reviewer",
|
|
4095
4465
|
maxRounds: 5,
|
|
4096
4466
|
qualitySeverityFloor: "high"
|
|
4097
|
-
} :
|
|
4467
|
+
} : instance2.headless;
|
|
4098
4468
|
const bridge = await startBridge({
|
|
4099
4469
|
instanceId,
|
|
4100
|
-
runtime:
|
|
4470
|
+
runtime: instance2.runtime,
|
|
4101
4471
|
stateDir: ctx.stateDir,
|
|
4102
4472
|
commsDir: ctx.commsDir,
|
|
4103
4473
|
bridgeScript,
|
|
@@ -4138,14 +4508,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4138
4508
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
4139
4509
|
}
|
|
4140
4510
|
}
|
|
4141
|
-
const updated = { ...
|
|
4511
|
+
const updated = { ...instance2, bridge, manageAppServer, noAuth };
|
|
4142
4512
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4143
4513
|
saveState(repoRoot, newState);
|
|
4144
4514
|
return {
|
|
4145
4515
|
ok: true,
|
|
4146
4516
|
command: "bridge",
|
|
4147
4517
|
instanceId,
|
|
4148
|
-
runtime:
|
|
4518
|
+
runtime: instance2.runtime,
|
|
4149
4519
|
code: "TAP_BRIDGE_START_OK",
|
|
4150
4520
|
message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
|
|
4151
4521
|
warnings: [],
|
|
@@ -4158,7 +4528,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4158
4528
|
ok: false,
|
|
4159
4529
|
command: "bridge",
|
|
4160
4530
|
instanceId,
|
|
4161
|
-
runtime:
|
|
4531
|
+
runtime: instance2.runtime,
|
|
4162
4532
|
code: "TAP_BRIDGE_START_FAILED",
|
|
4163
4533
|
message: msg,
|
|
4164
4534
|
warnings: [],
|
|
@@ -4260,11 +4630,11 @@ async function bridgeStopOne(identifier) {
|
|
|
4260
4630
|
}
|
|
4261
4631
|
const instanceId = resolved.instanceId;
|
|
4262
4632
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4263
|
-
const
|
|
4633
|
+
const instance2 = state.instances[instanceId];
|
|
4264
4634
|
const bridgeState = loadCurrentBridgeState(
|
|
4265
4635
|
ctx.stateDir,
|
|
4266
4636
|
instanceId,
|
|
4267
|
-
|
|
4637
|
+
instance2?.bridge
|
|
4268
4638
|
);
|
|
4269
4639
|
const appServer = bridgeState?.appServer ?? null;
|
|
4270
4640
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
@@ -4312,8 +4682,8 @@ async function bridgeStopOne(identifier) {
|
|
|
4312
4682
|
}
|
|
4313
4683
|
}
|
|
4314
4684
|
}
|
|
4315
|
-
if (
|
|
4316
|
-
const updated = { ...
|
|
4685
|
+
if (instance2) {
|
|
4686
|
+
const updated = { ...instance2, bridge: null };
|
|
4317
4687
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4318
4688
|
saveState(repoRoot, newState);
|
|
4319
4689
|
}
|
|
@@ -4385,9 +4755,9 @@ async function bridgeStopAll() {
|
|
|
4385
4755
|
logSuccess(`Stopped bridge for ${instanceId}`);
|
|
4386
4756
|
stopped.push(instanceId);
|
|
4387
4757
|
}
|
|
4388
|
-
const
|
|
4389
|
-
if (
|
|
4390
|
-
state.instances[instanceId] = { ...
|
|
4758
|
+
const instance2 = state.instances[instanceId];
|
|
4759
|
+
if (instance2?.bridge) {
|
|
4760
|
+
state.instances[instanceId] = { ...instance2, bridge: null };
|
|
4391
4761
|
stateChanged = true;
|
|
4392
4762
|
}
|
|
4393
4763
|
}
|
|
@@ -4453,12 +4823,18 @@ function bridgeStatusAll() {
|
|
|
4453
4823
|
pid: null,
|
|
4454
4824
|
port: inst.port,
|
|
4455
4825
|
lastHeartbeat: null,
|
|
4826
|
+
threadId: null,
|
|
4827
|
+
threadCwd: null,
|
|
4828
|
+
savedThreadId: null,
|
|
4829
|
+
savedThreadCwd: null,
|
|
4456
4830
|
appServer: null
|
|
4457
4831
|
};
|
|
4458
4832
|
continue;
|
|
4459
4833
|
}
|
|
4460
4834
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
4461
4835
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
4836
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
4837
|
+
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
4462
4838
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4463
4839
|
const pid = bridgeState?.pid ?? null;
|
|
4464
4840
|
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
@@ -4480,12 +4856,26 @@ function bridgeStatusAll() {
|
|
|
4480
4856
|
);
|
|
4481
4857
|
}
|
|
4482
4858
|
}
|
|
4859
|
+
if (runtimeHeartbeat?.threadId) {
|
|
4860
|
+
log(
|
|
4861
|
+
` Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
|
|
4862
|
+
);
|
|
4863
|
+
}
|
|
4864
|
+
if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
|
|
4865
|
+
log(
|
|
4866
|
+
` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4483
4869
|
bridges[instanceId] = {
|
|
4484
4870
|
status,
|
|
4485
4871
|
runtime: inst.runtime,
|
|
4486
4872
|
pid,
|
|
4487
4873
|
port: inst.port,
|
|
4488
4874
|
lastHeartbeat: heartbeat,
|
|
4875
|
+
threadId: runtimeHeartbeat?.threadId ?? null,
|
|
4876
|
+
threadCwd: runtimeHeartbeat?.threadCwd ?? null,
|
|
4877
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
4878
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
4489
4879
|
appServer: bridgeState?.appServer ?? null
|
|
4490
4880
|
};
|
|
4491
4881
|
}
|
|
@@ -4561,6 +4951,10 @@ function bridgeStatusOne(identifier) {
|
|
|
4561
4951
|
pid: null,
|
|
4562
4952
|
port: inst.port,
|
|
4563
4953
|
lastHeartbeat: null,
|
|
4954
|
+
threadId: null,
|
|
4955
|
+
threadCwd: null,
|
|
4956
|
+
savedThreadId: null,
|
|
4957
|
+
savedThreadCwd: null,
|
|
4564
4958
|
appServer: null
|
|
4565
4959
|
}
|
|
4566
4960
|
};
|
|
@@ -4569,6 +4963,8 @@ function bridgeStatusOne(identifier) {
|
|
|
4569
4963
|
const stateDir = resolvedCfg2.stateDir;
|
|
4570
4964
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
4571
4965
|
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
4966
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
4967
|
+
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
4572
4968
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
4573
4969
|
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
4574
4970
|
log(`Status: ${status}`);
|
|
@@ -4577,6 +4973,16 @@ function bridgeStatusOne(identifier) {
|
|
|
4577
4973
|
log(
|
|
4578
4974
|
`Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
|
|
4579
4975
|
);
|
|
4976
|
+
if (runtimeHeartbeat?.threadId) {
|
|
4977
|
+
log(
|
|
4978
|
+
`Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
|
|
4982
|
+
log(
|
|
4983
|
+
`Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4580
4986
|
log(
|
|
4581
4987
|
`Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
4582
4988
|
);
|
|
@@ -4625,6 +5031,10 @@ function bridgeStatusOne(identifier) {
|
|
|
4625
5031
|
pid: bridgeState?.pid ?? null,
|
|
4626
5032
|
port: inst.port,
|
|
4627
5033
|
lastHeartbeat: heartbeat,
|
|
5034
|
+
threadId: runtimeHeartbeat?.threadId ?? null,
|
|
5035
|
+
threadCwd: runtimeHeartbeat?.threadCwd ?? null,
|
|
5036
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
5037
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
4628
5038
|
appServer: bridgeState?.appServer ?? null
|
|
4629
5039
|
}
|
|
4630
5040
|
};
|
|
@@ -4683,8 +5093,22 @@ async function bridgeRestart(identifier, flags) {
|
|
|
4683
5093
|
};
|
|
4684
5094
|
}
|
|
4685
5095
|
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
4686
|
-
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] :
|
|
4687
|
-
|
|
5096
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
|
|
5097
|
+
let drainTimeout;
|
|
5098
|
+
try {
|
|
5099
|
+
drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
|
|
5100
|
+
} catch (err) {
|
|
5101
|
+
return {
|
|
5102
|
+
ok: false,
|
|
5103
|
+
command: "bridge",
|
|
5104
|
+
instanceId,
|
|
5105
|
+
runtime: instance.runtime,
|
|
5106
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
5107
|
+
message: err instanceof Error ? err.message : String(err),
|
|
5108
|
+
warnings: [],
|
|
5109
|
+
data: {}
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
4688
5112
|
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
4689
5113
|
log(`Drain timeout: ${drainTimeout}s`);
|
|
4690
5114
|
try {
|
|
@@ -4831,7 +5255,7 @@ async function bridgeCommand(args) {
|
|
|
4831
5255
|
// src/engine/dashboard.ts
|
|
4832
5256
|
import * as fs15 from "fs";
|
|
4833
5257
|
import * as path15 from "path";
|
|
4834
|
-
import { execSync as
|
|
5258
|
+
import { execSync as execSync4 } from "child_process";
|
|
4835
5259
|
function collectAgents(commsDir) {
|
|
4836
5260
|
const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
|
|
4837
5261
|
if (!fs15.existsSync(heartbeatsPath)) return [];
|
|
@@ -4910,7 +5334,7 @@ function collectBridges(repoRoot) {
|
|
|
4910
5334
|
}
|
|
4911
5335
|
function collectPRs() {
|
|
4912
5336
|
try {
|
|
4913
|
-
const output =
|
|
5337
|
+
const output = execSync4(
|
|
4914
5338
|
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
4915
5339
|
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
4916
5340
|
);
|
|
@@ -4980,7 +5404,7 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
|
4980
5404
|
// src/commands/up.ts
|
|
4981
5405
|
var UP_HELP = `
|
|
4982
5406
|
Usage:
|
|
4983
|
-
tap
|
|
5407
|
+
tap up [bridge-start options]
|
|
4984
5408
|
|
|
4985
5409
|
Description:
|
|
4986
5410
|
Start all registered app-server bridge daemons with one command.
|
|
@@ -5004,7 +5428,18 @@ async function upCommand(args) {
|
|
|
5004
5428
|
};
|
|
5005
5429
|
}
|
|
5006
5430
|
const repoRoot = findRepoRoot();
|
|
5007
|
-
const
|
|
5431
|
+
const previousColdStartWarmup = process.env.TAP_COLD_START_WARMUP;
|
|
5432
|
+
process.env.TAP_COLD_START_WARMUP = "true";
|
|
5433
|
+
let result;
|
|
5434
|
+
try {
|
|
5435
|
+
result = await bridgeCommand(["start", "--all", ...args]);
|
|
5436
|
+
} finally {
|
|
5437
|
+
if (previousColdStartWarmup === void 0) {
|
|
5438
|
+
delete process.env.TAP_COLD_START_WARMUP;
|
|
5439
|
+
} else {
|
|
5440
|
+
process.env.TAP_COLD_START_WARMUP = previousColdStartWarmup;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5008
5443
|
const snapshot = collectDashboardSnapshot(repoRoot);
|
|
5009
5444
|
const activeBridges = snapshot.bridges.filter(
|
|
5010
5445
|
(bridge) => bridge.status === "running"
|
|
@@ -5035,7 +5470,7 @@ async function upCommand(args) {
|
|
|
5035
5470
|
// src/commands/down.ts
|
|
5036
5471
|
var DOWN_HELP = `
|
|
5037
5472
|
Usage:
|
|
5038
|
-
tap
|
|
5473
|
+
tap down
|
|
5039
5474
|
|
|
5040
5475
|
Description:
|
|
5041
5476
|
Stop all running bridge daemons and managed app-servers.
|
|
@@ -5084,7 +5519,34 @@ async function downCommand(args) {
|
|
|
5084
5519
|
// src/commands/serve.ts
|
|
5085
5520
|
import * as path16 from "path";
|
|
5086
5521
|
import { spawn as spawn2 } from "child_process";
|
|
5522
|
+
var SERVE_HELP = `
|
|
5523
|
+
Usage:
|
|
5524
|
+
tap serve [options]
|
|
5525
|
+
|
|
5526
|
+
Description:
|
|
5527
|
+
Start the tap MCP server over stdio. This command takes over the
|
|
5528
|
+
process \u2014 it is intended to be launched by an MCP host (e.g. Claude Code).
|
|
5529
|
+
|
|
5530
|
+
Options:
|
|
5531
|
+
--comms-dir <path> Override comms directory (also reads TAP_COMMS_DIR env)
|
|
5532
|
+
--help, -h Show help
|
|
5533
|
+
|
|
5534
|
+
Examples:
|
|
5535
|
+
npx @hua-labs/tap serve
|
|
5536
|
+
npx @hua-labs/tap serve --comms-dir /shared/comms
|
|
5537
|
+
`.trim();
|
|
5087
5538
|
async function serveCommand(args) {
|
|
5539
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
5540
|
+
log(SERVE_HELP);
|
|
5541
|
+
return {
|
|
5542
|
+
ok: true,
|
|
5543
|
+
command: "serve",
|
|
5544
|
+
code: "TAP_NO_OP",
|
|
5545
|
+
message: SERVE_HELP,
|
|
5546
|
+
warnings: [],
|
|
5547
|
+
data: {}
|
|
5548
|
+
};
|
|
5549
|
+
}
|
|
5088
5550
|
const repoRoot = findRepoRoot();
|
|
5089
5551
|
let commsDir;
|
|
5090
5552
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
@@ -5132,9 +5594,9 @@ async function serveCommand(args) {
|
|
|
5132
5594
|
TAP_COMMS_DIR: commsDir
|
|
5133
5595
|
}
|
|
5134
5596
|
});
|
|
5135
|
-
return new Promise((
|
|
5597
|
+
return new Promise((resolve13) => {
|
|
5136
5598
|
child.on("error", (err) => {
|
|
5137
|
-
|
|
5599
|
+
resolve13({
|
|
5138
5600
|
ok: false,
|
|
5139
5601
|
command: "serve",
|
|
5140
5602
|
code: "TAP_INTERNAL_ERROR",
|
|
@@ -5144,7 +5606,7 @@ async function serveCommand(args) {
|
|
|
5144
5606
|
});
|
|
5145
5607
|
});
|
|
5146
5608
|
child.on("exit", (code) => {
|
|
5147
|
-
|
|
5609
|
+
resolve13({
|
|
5148
5610
|
ok: code === 0,
|
|
5149
5611
|
command: "serve",
|
|
5150
5612
|
code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
|
|
@@ -5159,10 +5621,10 @@ async function serveCommand(args) {
|
|
|
5159
5621
|
// src/commands/init-worktree.ts
|
|
5160
5622
|
import * as fs16 from "fs";
|
|
5161
5623
|
import * as path17 from "path";
|
|
5162
|
-
import { execSync as
|
|
5624
|
+
import { execSync as execSync5 } from "child_process";
|
|
5163
5625
|
var INIT_WORKTREE_HELP = `
|
|
5164
5626
|
Usage:
|
|
5165
|
-
tap
|
|
5627
|
+
tap init-worktree [options]
|
|
5166
5628
|
|
|
5167
5629
|
Options:
|
|
5168
5630
|
--path <dir> Worktree directory (required, e.g. ../hua-wt-3)
|
|
@@ -5183,7 +5645,7 @@ function warn(warnings, message) {
|
|
|
5183
5645
|
}
|
|
5184
5646
|
function run(cmd, opts) {
|
|
5185
5647
|
try {
|
|
5186
|
-
return
|
|
5648
|
+
return execSync5(cmd, {
|
|
5187
5649
|
cwd: opts?.cwd,
|
|
5188
5650
|
encoding: "utf-8",
|
|
5189
5651
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5200,7 +5662,7 @@ function toAbsolute(p) {
|
|
|
5200
5662
|
}
|
|
5201
5663
|
function probeBun(candidate) {
|
|
5202
5664
|
try {
|
|
5203
|
-
const out =
|
|
5665
|
+
const out = execSync5(`"${candidate}" --version`, {
|
|
5204
5666
|
encoding: "utf-8",
|
|
5205
5667
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5206
5668
|
timeout: 5e3
|
|
@@ -5214,7 +5676,7 @@ function findBun() {
|
|
|
5214
5676
|
const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
|
|
5215
5677
|
for (const name of candidates) {
|
|
5216
5678
|
try {
|
|
5217
|
-
const out =
|
|
5679
|
+
const out = execSync5(
|
|
5218
5680
|
process.platform === "win32" ? `where ${name}` : `which ${name}`,
|
|
5219
5681
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
|
|
5220
5682
|
).trim();
|
|
@@ -5345,7 +5807,7 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
5345
5807
|
);
|
|
5346
5808
|
const mcpConfig = {
|
|
5347
5809
|
mcpServers: {
|
|
5348
|
-
|
|
5810
|
+
tap: {
|
|
5349
5811
|
command: bunAbs,
|
|
5350
5812
|
args: [channelEntry],
|
|
5351
5813
|
cwd: wtAbs,
|
|
@@ -5445,7 +5907,7 @@ async function initWorktreeCommand(args) {
|
|
|
5445
5907
|
ok: true,
|
|
5446
5908
|
command: "init-worktree",
|
|
5447
5909
|
code: "TAP_NO_OP",
|
|
5448
|
-
message:
|
|
5910
|
+
message: INIT_WORKTREE_HELP,
|
|
5449
5911
|
warnings: [],
|
|
5450
5912
|
data: {}
|
|
5451
5913
|
};
|
|
@@ -5607,8 +6069,38 @@ function renderSnapshot(snapshot) {
|
|
|
5607
6069
|
}
|
|
5608
6070
|
}
|
|
5609
6071
|
}
|
|
6072
|
+
var DASHBOARD_HELP = `
|
|
6073
|
+
Usage:
|
|
6074
|
+
tap dashboard [options]
|
|
6075
|
+
|
|
6076
|
+
Description:
|
|
6077
|
+
Display a unified ops dashboard: agents, bridges, PRs, and warnings.
|
|
6078
|
+
|
|
6079
|
+
Options:
|
|
6080
|
+
--json Output snapshot as JSON
|
|
6081
|
+
--watch Refresh dashboard on an interval
|
|
6082
|
+
--interval <seconds> Refresh interval in seconds (default: 5, min: 2)
|
|
6083
|
+
--comms-dir <path> Override comms directory
|
|
6084
|
+
--help, -h Show help
|
|
6085
|
+
|
|
6086
|
+
Examples:
|
|
6087
|
+
npx @hua-labs/tap dashboard
|
|
6088
|
+
npx @hua-labs/tap dashboard --watch --interval 10
|
|
6089
|
+
npx @hua-labs/tap dashboard --json
|
|
6090
|
+
`.trim();
|
|
5610
6091
|
async function dashboardCommand(args) {
|
|
5611
6092
|
const { flags } = parseArgs(args);
|
|
6093
|
+
if (flags["help"] === true || flags["h"] === true) {
|
|
6094
|
+
log(DASHBOARD_HELP);
|
|
6095
|
+
return {
|
|
6096
|
+
ok: true,
|
|
6097
|
+
command: "dashboard",
|
|
6098
|
+
code: "TAP_NO_OP",
|
|
6099
|
+
message: DASHBOARD_HELP,
|
|
6100
|
+
warnings: [],
|
|
6101
|
+
data: {}
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
5612
6104
|
const jsonMode = flags["json"] === true;
|
|
5613
6105
|
const watchMode = flags["watch"] === true;
|
|
5614
6106
|
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
|
|
@@ -5668,14 +6160,150 @@ import {
|
|
|
5668
6160
|
mkdirSync as mkdirSync10,
|
|
5669
6161
|
readdirSync as readdirSync4,
|
|
5670
6162
|
readFileSync as readFileSync14,
|
|
6163
|
+
renameSync as renameSync10,
|
|
5671
6164
|
statSync as statSync2,
|
|
5672
|
-
unlinkSync as unlinkSync3
|
|
6165
|
+
unlinkSync as unlinkSync3,
|
|
6166
|
+
writeFileSync as writeFileSync12
|
|
5673
6167
|
} from "fs";
|
|
5674
|
-
import {
|
|
5675
|
-
import {
|
|
6168
|
+
import { homedir as homedir3 } from "os";
|
|
6169
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
6170
|
+
import { dirname as dirname11, join as join17, resolve as resolve12 } from "path";
|
|
5676
6171
|
var PASS = "pass";
|
|
5677
6172
|
var WARN = "warn";
|
|
5678
6173
|
var FAIL = "fail";
|
|
6174
|
+
var CODEX_ENV_DRIFT_KEYS = [
|
|
6175
|
+
"TAP_COMMS_DIR",
|
|
6176
|
+
"TAP_STATE_DIR",
|
|
6177
|
+
"TAP_REPO_ROOT"
|
|
6178
|
+
];
|
|
6179
|
+
function normalizeComparablePath2(value) {
|
|
6180
|
+
return resolve12(value).replace(/\\/g, "/").toLowerCase();
|
|
6181
|
+
}
|
|
6182
|
+
function samePath(left, right) {
|
|
6183
|
+
return normalizeComparablePath2(left) === normalizeComparablePath2(right);
|
|
6184
|
+
}
|
|
6185
|
+
function looksLikePathToken(value) {
|
|
6186
|
+
return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("/") || value.startsWith("\\") || value.startsWith(".") || value.includes("/") || value.includes("\\");
|
|
6187
|
+
}
|
|
6188
|
+
function sameCommandToken(left, right) {
|
|
6189
|
+
return looksLikePathToken(left) || looksLikePathToken(right) ? samePath(left, right) : left === right;
|
|
6190
|
+
}
|
|
6191
|
+
function sameStringArray(left, right) {
|
|
6192
|
+
return left.length === right.length && left.every((value, index) => sameCommandToken(value, right[index] ?? ""));
|
|
6193
|
+
}
|
|
6194
|
+
function appendWarningMessage(message, extra) {
|
|
6195
|
+
return message.includes(extra) ? message : `${message}; ${extra}`;
|
|
6196
|
+
}
|
|
6197
|
+
function findCodexConfigPath3() {
|
|
6198
|
+
return join17(homedir3(), ".codex", "config.toml");
|
|
6199
|
+
}
|
|
6200
|
+
function canonicalizeTrustPath3(targetPath) {
|
|
6201
|
+
let resolved = resolve12(targetPath).replace(/\//g, "\\");
|
|
6202
|
+
const driveRoot = /^[A-Za-z]:\\$/;
|
|
6203
|
+
if (!driveRoot.test(resolved)) {
|
|
6204
|
+
resolved = resolved.replace(/\\+$/g, "");
|
|
6205
|
+
}
|
|
6206
|
+
return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
|
|
6207
|
+
}
|
|
6208
|
+
function trustSelector2(targetPath) {
|
|
6209
|
+
return `projects.'${canonicalizeTrustPath3(targetPath)}'`;
|
|
6210
|
+
}
|
|
6211
|
+
function writeTomlAtomically(filePath, content) {
|
|
6212
|
+
const dir = dirname11(filePath);
|
|
6213
|
+
mkdirSync10(dir, { recursive: true });
|
|
6214
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
6215
|
+
writeFileSync12(tmp, content, "utf-8");
|
|
6216
|
+
renameSync10(tmp, filePath);
|
|
6217
|
+
}
|
|
6218
|
+
function hasInstalledCodexInstance(state) {
|
|
6219
|
+
return !!state ? Object.values(state.instances).some(
|
|
6220
|
+
(instance2) => instance2.runtime === "codex" && instance2.installed
|
|
6221
|
+
) : false;
|
|
6222
|
+
}
|
|
6223
|
+
function getCodexTrustTargets(repoRoot) {
|
|
6224
|
+
return [...new Set([repoRoot, process.cwd()].map((value) => resolve12(value)))];
|
|
6225
|
+
}
|
|
6226
|
+
function buildCodexDoctorSpec(repoRoot, commsDir) {
|
|
6227
|
+
const state = loadState(repoRoot);
|
|
6228
|
+
if (!hasInstalledCodexInstance(state)) {
|
|
6229
|
+
return null;
|
|
6230
|
+
}
|
|
6231
|
+
const ctx = createAdapterContext(commsDir, repoRoot);
|
|
6232
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
6233
|
+
return {
|
|
6234
|
+
configPath: findCodexConfigPath3(),
|
|
6235
|
+
trustTargets: getCodexTrustTargets(repoRoot),
|
|
6236
|
+
managed
|
|
6237
|
+
};
|
|
6238
|
+
}
|
|
6239
|
+
function repairCodexConfig(repoRoot, commsDir) {
|
|
6240
|
+
const spec = buildCodexDoctorSpec(repoRoot, commsDir);
|
|
6241
|
+
if (!spec) {
|
|
6242
|
+
throw new Error("No installed Codex instance found in tap state.");
|
|
6243
|
+
}
|
|
6244
|
+
if (!spec.managed.command || spec.managed.issues.length > 0) {
|
|
6245
|
+
throw new Error(
|
|
6246
|
+
spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
|
|
6247
|
+
);
|
|
6248
|
+
}
|
|
6249
|
+
const existingContent = existsSync16(spec.configPath) ? readFileSync14(spec.configPath, "utf-8") : "";
|
|
6250
|
+
const existingTapEnvTable = extractTomlTable(existingContent, "mcp_servers.tap.env");
|
|
6251
|
+
const existingLegacyEnvTable = extractTomlTable(
|
|
6252
|
+
existingContent,
|
|
6253
|
+
"mcp_servers.tap-comms.env"
|
|
6254
|
+
);
|
|
6255
|
+
const preservedEnv = parseTomlAssignments(
|
|
6256
|
+
existingTapEnvTable ?? existingLegacyEnvTable ?? ""
|
|
6257
|
+
);
|
|
6258
|
+
const repairedEnv = {
|
|
6259
|
+
...preservedEnv,
|
|
6260
|
+
...Object.fromEntries(
|
|
6261
|
+
CODEX_ENV_DRIFT_KEYS.map((key) => [key, spec.managed.env[key]])
|
|
6262
|
+
)
|
|
6263
|
+
};
|
|
6264
|
+
let nextContent = existingContent;
|
|
6265
|
+
if (extractTomlTable(nextContent, "mcp_servers.tap-comms.env")) {
|
|
6266
|
+
nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms.env");
|
|
6267
|
+
}
|
|
6268
|
+
if (extractTomlTable(nextContent, "mcp_servers.tap-comms")) {
|
|
6269
|
+
nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms");
|
|
6270
|
+
}
|
|
6271
|
+
nextContent = replaceTomlTable(
|
|
6272
|
+
nextContent,
|
|
6273
|
+
"mcp_servers.tap",
|
|
6274
|
+
renderTomlTable(
|
|
6275
|
+
"mcp_servers.tap",
|
|
6276
|
+
{
|
|
6277
|
+
command: spec.managed.command,
|
|
6278
|
+
args: spec.managed.args
|
|
6279
|
+
},
|
|
6280
|
+
extractTomlTable(existingContent, "mcp_servers.tap")
|
|
6281
|
+
)
|
|
6282
|
+
);
|
|
6283
|
+
nextContent = replaceTomlTable(
|
|
6284
|
+
nextContent,
|
|
6285
|
+
"mcp_servers.tap.env",
|
|
6286
|
+
renderTomlTable(
|
|
6287
|
+
"mcp_servers.tap.env",
|
|
6288
|
+
repairedEnv,
|
|
6289
|
+
existingTapEnvTable ?? existingLegacyEnvTable
|
|
6290
|
+
)
|
|
6291
|
+
);
|
|
6292
|
+
for (const trustTarget of spec.trustTargets) {
|
|
6293
|
+
const selector = trustSelector2(trustTarget);
|
|
6294
|
+
nextContent = replaceTomlTable(
|
|
6295
|
+
nextContent,
|
|
6296
|
+
selector,
|
|
6297
|
+
renderTomlTable(
|
|
6298
|
+
selector,
|
|
6299
|
+
{ trust_level: "trusted" },
|
|
6300
|
+
extractTomlTable(existingContent, selector)
|
|
6301
|
+
)
|
|
6302
|
+
);
|
|
6303
|
+
}
|
|
6304
|
+
writeTomlAtomically(spec.configPath, nextContent);
|
|
6305
|
+
return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
|
|
6306
|
+
}
|
|
5679
6307
|
function countFiles(dir, ext = ".md") {
|
|
5680
6308
|
if (!existsSync16(dir)) return 0;
|
|
5681
6309
|
try {
|
|
@@ -5700,21 +6328,6 @@ function recentFileCount(dir, withinMs) {
|
|
|
5700
6328
|
}
|
|
5701
6329
|
return count;
|
|
5702
6330
|
}
|
|
5703
|
-
function loadBridgeRuntimeHeartbeat(bridgeState) {
|
|
5704
|
-
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
5705
|
-
if (!runtimeStateDir) {
|
|
5706
|
-
return null;
|
|
5707
|
-
}
|
|
5708
|
-
const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
|
|
5709
|
-
if (!existsSync16(heartbeatPath)) {
|
|
5710
|
-
return null;
|
|
5711
|
-
}
|
|
5712
|
-
try {
|
|
5713
|
-
return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
|
|
5714
|
-
} catch {
|
|
5715
|
-
return null;
|
|
5716
|
-
}
|
|
5717
|
-
}
|
|
5718
6331
|
function checkComms(commsDir) {
|
|
5719
6332
|
const checks = [];
|
|
5720
6333
|
checks.push({
|
|
@@ -5798,7 +6411,8 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
5798
6411
|
const running = isBridgeRunning(stateDir, id);
|
|
5799
6412
|
const bridgeState = loadBridgeState(stateDir, id);
|
|
5800
6413
|
const heartbeatAge = getHeartbeatAge(stateDir, id);
|
|
5801
|
-
const runtimeHeartbeat =
|
|
6414
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
6415
|
+
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
5802
6416
|
let status;
|
|
5803
6417
|
let message;
|
|
5804
6418
|
let fix;
|
|
@@ -5844,9 +6458,30 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
5844
6458
|
}
|
|
5845
6459
|
const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
|
|
5846
6460
|
if (lastRuntimeError) {
|
|
5847
|
-
status =
|
|
6461
|
+
status = WARN;
|
|
5848
6462
|
message = `${message}; bridge last error: ${lastRuntimeError}`;
|
|
5849
6463
|
}
|
|
6464
|
+
if (savedThread?.threadId && savedThread.cwd && !samePath(savedThread.cwd, repoRoot)) {
|
|
6465
|
+
status = WARN;
|
|
6466
|
+
message = appendWarningMessage(
|
|
6467
|
+
message,
|
|
6468
|
+
`saved thread cwd mismatch (${savedThread.cwd})`
|
|
6469
|
+
);
|
|
6470
|
+
}
|
|
6471
|
+
if (runtimeHeartbeat?.threadId && savedThread?.threadId && runtimeHeartbeat.threadId !== savedThread.threadId) {
|
|
6472
|
+
status = WARN;
|
|
6473
|
+
message = appendWarningMessage(
|
|
6474
|
+
message,
|
|
6475
|
+
`saved thread ${savedThread.threadId} differs from active thread ${runtimeHeartbeat.threadId}`
|
|
6476
|
+
);
|
|
6477
|
+
}
|
|
6478
|
+
if (runtimeHeartbeat?.threadCwd && !samePath(runtimeHeartbeat.threadCwd, repoRoot)) {
|
|
6479
|
+
status = WARN;
|
|
6480
|
+
message = appendWarningMessage(
|
|
6481
|
+
message,
|
|
6482
|
+
`active thread cwd mismatch (${runtimeHeartbeat.threadCwd})`
|
|
6483
|
+
);
|
|
6484
|
+
}
|
|
5850
6485
|
checks.push({ name: `bridge: ${id}`, status, message, fix });
|
|
5851
6486
|
} else {
|
|
5852
6487
|
checks.push({
|
|
@@ -5919,15 +6554,25 @@ function checkMcpServer(repoRoot) {
|
|
|
5919
6554
|
});
|
|
5920
6555
|
return checks;
|
|
5921
6556
|
}
|
|
5922
|
-
const
|
|
5923
|
-
|
|
6557
|
+
const mcpServers = config?.mcpServers;
|
|
6558
|
+
const hasTap = mcpServers?.["tap"];
|
|
6559
|
+
const hasOldKey = mcpServers?.["tap-comms"];
|
|
6560
|
+
if (hasOldKey) {
|
|
5924
6561
|
checks.push({
|
|
5925
6562
|
name: "MCP config (.mcp.json)",
|
|
5926
6563
|
status: WARN,
|
|
5927
|
-
message: "tap-comms
|
|
6564
|
+
message: 'Legacy "tap-comms" key found. Run "tap add claude" to migrate to the new "tap" key.'
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6567
|
+
if (!hasTap && !hasOldKey) {
|
|
6568
|
+
checks.push({
|
|
6569
|
+
name: "MCP config (.mcp.json)",
|
|
6570
|
+
status: WARN,
|
|
6571
|
+
message: "tap not configured"
|
|
5928
6572
|
});
|
|
5929
6573
|
return checks;
|
|
5930
6574
|
}
|
|
6575
|
+
const hasTapComms = hasTap ?? hasOldKey;
|
|
5931
6576
|
checks.push({
|
|
5932
6577
|
name: "MCP config (.mcp.json)",
|
|
5933
6578
|
status: PASS,
|
|
@@ -5938,11 +6583,12 @@ function checkMcpServer(repoRoot) {
|
|
|
5938
6583
|
let cmdAvailable = existsSync16(cmd);
|
|
5939
6584
|
if (!cmdAvailable) {
|
|
5940
6585
|
try {
|
|
5941
|
-
|
|
6586
|
+
const result = spawnSync4(cmd, ["--version"], {
|
|
5942
6587
|
stdio: "pipe",
|
|
5943
|
-
timeout: 5e3
|
|
6588
|
+
timeout: 5e3,
|
|
6589
|
+
shell: process.platform === "win32"
|
|
5944
6590
|
});
|
|
5945
|
-
cmdAvailable =
|
|
6591
|
+
cmdAvailable = result.status === 0;
|
|
5946
6592
|
} catch {
|
|
5947
6593
|
}
|
|
5948
6594
|
}
|
|
@@ -6001,6 +6647,88 @@ function checkMcpServer(repoRoot) {
|
|
|
6001
6647
|
});
|
|
6002
6648
|
return checks;
|
|
6003
6649
|
}
|
|
6650
|
+
function checkCodexConfig(repoRoot, commsDir) {
|
|
6651
|
+
const spec = buildCodexDoctorSpec(repoRoot, commsDir);
|
|
6652
|
+
if (!spec) {
|
|
6653
|
+
return [];
|
|
6654
|
+
}
|
|
6655
|
+
const checks = [];
|
|
6656
|
+
const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
|
|
6657
|
+
if (!existsSync16(spec.configPath)) {
|
|
6658
|
+
checks.push({
|
|
6659
|
+
name: "MCP config (~/.codex/config.toml)",
|
|
6660
|
+
status: WARN,
|
|
6661
|
+
message: `${spec.configPath} not found. ${fixHint}`,
|
|
6662
|
+
fix: () => repairCodexConfig(repoRoot, commsDir)
|
|
6663
|
+
});
|
|
6664
|
+
return checks;
|
|
6665
|
+
}
|
|
6666
|
+
const content = readFileSync14(spec.configPath, "utf-8");
|
|
6667
|
+
const tapTable = extractTomlTable(content, "mcp_servers.tap");
|
|
6668
|
+
const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
|
|
6669
|
+
const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
|
|
6670
|
+
const legacyEnvTable = extractTomlTable(content, "mcp_servers.tap-comms.env");
|
|
6671
|
+
const selectedMain = parseTomlAssignments(tapTable ?? "");
|
|
6672
|
+
const selectedEnv = parseTomlAssignments(
|
|
6673
|
+
tapEnvTable ?? legacyEnvTable ?? ""
|
|
6674
|
+
);
|
|
6675
|
+
const issues = [];
|
|
6676
|
+
if (legacyTable || legacyEnvTable) {
|
|
6677
|
+
issues.push('legacy "tap-comms" key present');
|
|
6678
|
+
}
|
|
6679
|
+
if (!tapTable && !legacyTable) {
|
|
6680
|
+
issues.push("tap MCP table missing");
|
|
6681
|
+
}
|
|
6682
|
+
if (!tapEnvTable && !legacyEnvTable) {
|
|
6683
|
+
issues.push("tap MCP env table missing");
|
|
6684
|
+
}
|
|
6685
|
+
if (tapTable && spec.managed.command) {
|
|
6686
|
+
const actualCommand = selectedMain.command;
|
|
6687
|
+
if (typeof actualCommand !== "string") {
|
|
6688
|
+
issues.push("tap MCP command missing");
|
|
6689
|
+
} else if (!sameCommandToken(actualCommand, spec.managed.command)) {
|
|
6690
|
+
issues.push(`tap MCP command drift (${actualCommand})`);
|
|
6691
|
+
}
|
|
6692
|
+
const actualArgs = selectedMain.args;
|
|
6693
|
+
if (!Array.isArray(actualArgs)) {
|
|
6694
|
+
issues.push("tap MCP args missing");
|
|
6695
|
+
} else if (!sameStringArray(actualArgs, spec.managed.args)) {
|
|
6696
|
+
issues.push(`tap MCP args drift (${JSON.stringify(actualArgs)})`);
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
for (const key of CODEX_ENV_DRIFT_KEYS) {
|
|
6700
|
+
const expected = spec.managed.env[key];
|
|
6701
|
+
const actual = selectedEnv[key];
|
|
6702
|
+
if (typeof actual !== "string") {
|
|
6703
|
+
issues.push(`${key} missing`);
|
|
6704
|
+
continue;
|
|
6705
|
+
}
|
|
6706
|
+
if (!samePath(actual, expected)) {
|
|
6707
|
+
issues.push(`${key} drift (${actual})`);
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
for (const trustTarget of spec.trustTargets) {
|
|
6711
|
+
const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
|
|
6712
|
+
if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
|
|
6713
|
+
issues.push(`missing trust for ${trustTarget}`);
|
|
6714
|
+
}
|
|
6715
|
+
}
|
|
6716
|
+
if (issues.length === 0) {
|
|
6717
|
+
checks.push({
|
|
6718
|
+
name: "MCP config (~/.codex/config.toml)",
|
|
6719
|
+
status: PASS,
|
|
6720
|
+
message: spec.configPath
|
|
6721
|
+
});
|
|
6722
|
+
return checks;
|
|
6723
|
+
}
|
|
6724
|
+
checks.push({
|
|
6725
|
+
name: "MCP config (~/.codex/config.toml)",
|
|
6726
|
+
status: WARN,
|
|
6727
|
+
message: `${issues.join("; ")}. ${fixHint}`,
|
|
6728
|
+
fix: () => repairCodexConfig(repoRoot, commsDir)
|
|
6729
|
+
});
|
|
6730
|
+
return checks;
|
|
6731
|
+
}
|
|
6004
6732
|
function checkBridgeTurnHealth(repoRoot) {
|
|
6005
6733
|
const checks = [];
|
|
6006
6734
|
const tmpDir = join17(repoRoot, ".tmp");
|
|
@@ -6115,7 +6843,35 @@ function renderCheck(check, fixMode) {
|
|
|
6115
6843
|
const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
|
|
6116
6844
|
return ` ${icon} ${check.name}${msg}`;
|
|
6117
6845
|
}
|
|
6846
|
+
var DOCTOR_HELP = `
|
|
6847
|
+
Usage:
|
|
6848
|
+
tap doctor [options]
|
|
6849
|
+
|
|
6850
|
+
Description:
|
|
6851
|
+
Diagnose tap infrastructure health: comms directory, instances, bridges,
|
|
6852
|
+
message lifecycle, and MCP server configuration.
|
|
6853
|
+
|
|
6854
|
+
Options:
|
|
6855
|
+
--fix Auto-repair detected issues where possible
|
|
6856
|
+
--comms-dir <path> Override comms directory
|
|
6857
|
+
--help, -h Show help
|
|
6858
|
+
|
|
6859
|
+
Examples:
|
|
6860
|
+
npx @hua-labs/tap doctor
|
|
6861
|
+
npx @hua-labs/tap doctor --fix
|
|
6862
|
+
`.trim();
|
|
6118
6863
|
async function doctorCommand(args) {
|
|
6864
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
6865
|
+
log(DOCTOR_HELP);
|
|
6866
|
+
return {
|
|
6867
|
+
ok: true,
|
|
6868
|
+
command: "doctor",
|
|
6869
|
+
code: "TAP_NO_OP",
|
|
6870
|
+
message: DOCTOR_HELP,
|
|
6871
|
+
warnings: [],
|
|
6872
|
+
data: {}
|
|
6873
|
+
};
|
|
6874
|
+
}
|
|
6119
6875
|
const repoRoot = findRepoRoot();
|
|
6120
6876
|
const overrides = {};
|
|
6121
6877
|
let fixMode = false;
|
|
@@ -6137,6 +6893,7 @@ async function doctorCommand(args) {
|
|
|
6137
6893
|
checks.push(...checkInstances(repoRoot, config.stateDir));
|
|
6138
6894
|
checks.push(...checkMessageLifecycle(commsDir));
|
|
6139
6895
|
checks.push(...checkMcpServer(repoRoot));
|
|
6896
|
+
checks.push(...checkCodexConfig(repoRoot, commsDir));
|
|
6140
6897
|
checks.push(...checkBridgeTurnHealth(repoRoot));
|
|
6141
6898
|
return checks;
|
|
6142
6899
|
}
|
|
@@ -6228,12 +6985,12 @@ async function doctorCommand(args) {
|
|
|
6228
6985
|
}
|
|
6229
6986
|
|
|
6230
6987
|
// src/commands/comms.ts
|
|
6231
|
-
import { execSync as
|
|
6988
|
+
import { execSync as execSync6, spawnSync as spawnSync5 } from "child_process";
|
|
6232
6989
|
import * as fs17 from "fs";
|
|
6233
6990
|
import * as path18 from "path";
|
|
6234
6991
|
var COMMS_HELP = `
|
|
6235
6992
|
Usage:
|
|
6236
|
-
tap
|
|
6993
|
+
tap comms <subcommand>
|
|
6237
6994
|
|
|
6238
6995
|
Subcommands:
|
|
6239
6996
|
pull Pull latest changes from comms remote repo
|
|
@@ -6260,7 +7017,7 @@ function commsPull(commsDir) {
|
|
|
6260
7017
|
};
|
|
6261
7018
|
}
|
|
6262
7019
|
try {
|
|
6263
|
-
const output =
|
|
7020
|
+
const output = execSync6("git pull --rebase", {
|
|
6264
7021
|
cwd: commsDir,
|
|
6265
7022
|
encoding: "utf-8",
|
|
6266
7023
|
stdio: "pipe"
|
|
@@ -6302,8 +7059,8 @@ function commsPush(commsDir) {
|
|
|
6302
7059
|
};
|
|
6303
7060
|
}
|
|
6304
7061
|
try {
|
|
6305
|
-
|
|
6306
|
-
const status =
|
|
7062
|
+
execSync6("git add -A", { cwd: commsDir, stdio: "pipe" });
|
|
7063
|
+
const status = execSync6("git status --porcelain", {
|
|
6307
7064
|
cwd: commsDir,
|
|
6308
7065
|
encoding: "utf-8",
|
|
6309
7066
|
stdio: "pipe"
|
|
@@ -6320,11 +7077,23 @@ function commsPush(commsDir) {
|
|
|
6320
7077
|
};
|
|
6321
7078
|
}
|
|
6322
7079
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
7080
|
+
const commitResult = spawnSync5(
|
|
7081
|
+
"git",
|
|
7082
|
+
["commit", "-m", `chore(comms): sync ${timestamp}`],
|
|
7083
|
+
{ cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
|
|
7084
|
+
);
|
|
7085
|
+
if (commitResult.status !== 0) {
|
|
7086
|
+
const msg = commitResult.stderr || `git commit exited with code ${commitResult.status}`;
|
|
7087
|
+
return {
|
|
7088
|
+
ok: false,
|
|
7089
|
+
command: "comms",
|
|
7090
|
+
code: "TAP_COMMS_PUSH_FAILED",
|
|
7091
|
+
message: `Commit failed: ${msg}`,
|
|
7092
|
+
warnings: [],
|
|
7093
|
+
data: { commsDir }
|
|
7094
|
+
};
|
|
7095
|
+
}
|
|
7096
|
+
execSync6("git push", { cwd: commsDir, stdio: "pipe" });
|
|
6328
7097
|
logSuccess("Comms push complete");
|
|
6329
7098
|
return {
|
|
6330
7099
|
ok: true,
|
|
@@ -6390,7 +7159,12 @@ function emitResult(result, jsonMode) {
|
|
|
6390
7159
|
} else {
|
|
6391
7160
|
logError(result.message);
|
|
6392
7161
|
}
|
|
7162
|
+
const emittedWarnings = /* @__PURE__ */ new Set();
|
|
6393
7163
|
for (const w of result.warnings) {
|
|
7164
|
+
if (emittedWarnings.has(w) || wasWarningLogged(w)) {
|
|
7165
|
+
continue;
|
|
7166
|
+
}
|
|
7167
|
+
emittedWarnings.add(w);
|
|
6394
7168
|
logWarn(w);
|
|
6395
7169
|
}
|
|
6396
7170
|
}
|
|
@@ -6403,16 +7177,61 @@ function extractJsonFlag(args) {
|
|
|
6403
7177
|
return { jsonMode, cleanArgs };
|
|
6404
7178
|
}
|
|
6405
7179
|
|
|
7180
|
+
// src/cli-suggest.ts
|
|
7181
|
+
var COMMANDS = [
|
|
7182
|
+
"init",
|
|
7183
|
+
"init-worktree",
|
|
7184
|
+
"add",
|
|
7185
|
+
"remove",
|
|
7186
|
+
"status",
|
|
7187
|
+
"bridge",
|
|
7188
|
+
"up",
|
|
7189
|
+
"down",
|
|
7190
|
+
"comms",
|
|
7191
|
+
"dashboard",
|
|
7192
|
+
"doctor",
|
|
7193
|
+
"serve",
|
|
7194
|
+
"version"
|
|
7195
|
+
];
|
|
7196
|
+
function suggestCommand(input) {
|
|
7197
|
+
let best = null;
|
|
7198
|
+
let bestDist = Infinity;
|
|
7199
|
+
for (const cmd of COMMANDS) {
|
|
7200
|
+
const d = levenshtein(input.toLowerCase(), cmd);
|
|
7201
|
+
if (d < bestDist && d <= Math.max(2, Math.floor(cmd.length / 2))) {
|
|
7202
|
+
bestDist = d;
|
|
7203
|
+
best = cmd;
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
return best;
|
|
7207
|
+
}
|
|
7208
|
+
function levenshtein(a, b) {
|
|
7209
|
+
const m = a.length;
|
|
7210
|
+
const n = b.length;
|
|
7211
|
+
const dp = Array.from(
|
|
7212
|
+
{ length: m + 1 },
|
|
7213
|
+
() => Array.from({ length: n + 1 }).fill(0)
|
|
7214
|
+
);
|
|
7215
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
7216
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
7217
|
+
for (let i = 1; i <= m; i++) {
|
|
7218
|
+
for (let j = 1; j <= n; j++) {
|
|
7219
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
return dp[m][n];
|
|
7223
|
+
}
|
|
7224
|
+
|
|
6406
7225
|
// src/cli.ts
|
|
6407
7226
|
var HELP = `
|
|
6408
7227
|
@hua-labs/tap \u2014 Cross-model AI agent communication setup
|
|
6409
7228
|
|
|
6410
7229
|
Usage:
|
|
6411
|
-
tap
|
|
7230
|
+
tap <command> [options]
|
|
6412
7231
|
|
|
6413
7232
|
Commands:
|
|
6414
7233
|
init Initialize comms directory and state
|
|
6415
|
-
init-worktree Set up a new git worktree with tap
|
|
7234
|
+
init-worktree Set up a new git worktree with tap
|
|
6416
7235
|
add <runtime> Add a runtime instance (claude, codex, gemini)
|
|
6417
7236
|
remove <instance> Remove an instance and rollback config
|
|
6418
7237
|
status Show installed instances and bridge status
|
|
@@ -6422,7 +7241,7 @@ Commands:
|
|
|
6422
7241
|
comms <pull|push> Sync comms directory with remote repo
|
|
6423
7242
|
dashboard Show unified ops dashboard
|
|
6424
7243
|
doctor Diagnose tap infrastructure health
|
|
6425
|
-
serve Start tap
|
|
7244
|
+
serve Start tap MCP server (stdio)
|
|
6426
7245
|
version Show version
|
|
6427
7246
|
|
|
6428
7247
|
Options:
|
|
@@ -6459,6 +7278,7 @@ function normalizeCommandName(command) {
|
|
|
6459
7278
|
async function main() {
|
|
6460
7279
|
const rawArgs = process.argv.slice(2);
|
|
6461
7280
|
const { jsonMode, cleanArgs } = extractJsonFlag(rawArgs);
|
|
7281
|
+
resetLoggedWarnings();
|
|
6462
7282
|
setJsonMode(jsonMode);
|
|
6463
7283
|
const command = cleanArgs[0];
|
|
6464
7284
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -6516,21 +7336,27 @@ async function main() {
|
|
|
6516
7336
|
break;
|
|
6517
7337
|
case "serve": {
|
|
6518
7338
|
const serveResult = await serveCommand(commandArgs);
|
|
6519
|
-
if (!serveResult.ok) {
|
|
7339
|
+
if (!serveResult.ok || serveResult.code === "TAP_NO_OP") {
|
|
6520
7340
|
emitResult(serveResult, jsonMode);
|
|
6521
7341
|
}
|
|
6522
7342
|
process.exit(exitCode(serveResult));
|
|
6523
7343
|
break;
|
|
6524
7344
|
}
|
|
6525
|
-
default:
|
|
7345
|
+
default: {
|
|
7346
|
+
const suggestion = suggestCommand(command);
|
|
7347
|
+
const hint = suggestion ? `
|
|
7348
|
+
|
|
7349
|
+
Did you mean: tap ${suggestion}?` : "\n\nRun tap --help for a list of commands.";
|
|
6526
7350
|
result = {
|
|
6527
7351
|
ok: false,
|
|
6528
7352
|
command: "unknown",
|
|
6529
7353
|
code: "TAP_INVALID_ARGUMENT",
|
|
6530
|
-
message: `Unknown command: ${command}`,
|
|
7354
|
+
message: `Unknown command: ${command}${hint}`,
|
|
6531
7355
|
warnings: [],
|
|
6532
|
-
data: { requestedCommand: command }
|
|
7356
|
+
data: { requestedCommand: command, suggestion }
|
|
6533
7357
|
};
|
|
7358
|
+
break;
|
|
7359
|
+
}
|
|
6534
7360
|
}
|
|
6535
7361
|
} catch (err) {
|
|
6536
7362
|
const message = err instanceof Error ? err.message : String(err);
|