@hua-labs/tap 0.2.1 → 0.2.3
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 +2 -2
- package/dist/bridges/codex-app-server-bridge.mjs +8 -1
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +102 -29
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +456 -52
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +63 -1
- package/dist/index.mjs +302 -17
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +461 -198
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -219,7 +219,8 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
219
219
|
commsDir: "auto",
|
|
220
220
|
stateDir: "auto",
|
|
221
221
|
runtimeCommand: "auto",
|
|
222
|
-
appServerUrl: "auto"
|
|
222
|
+
appServerUrl: "auto",
|
|
223
|
+
towerName: "auto"
|
|
223
224
|
};
|
|
224
225
|
let commsDir;
|
|
225
226
|
if (overrides.commsDir) {
|
|
@@ -288,8 +289,16 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
288
289
|
} else {
|
|
289
290
|
appServerUrl = DEFAULT_APP_SERVER_URL;
|
|
290
291
|
}
|
|
292
|
+
const towerName = local.towerName ?? shared.towerName ?? null;
|
|
291
293
|
return {
|
|
292
|
-
config: {
|
|
294
|
+
config: {
|
|
295
|
+
repoRoot,
|
|
296
|
+
commsDir,
|
|
297
|
+
stateDir,
|
|
298
|
+
runtimeCommand,
|
|
299
|
+
appServerUrl,
|
|
300
|
+
towerName
|
|
301
|
+
},
|
|
293
302
|
sources
|
|
294
303
|
};
|
|
295
304
|
}
|
|
@@ -935,6 +944,10 @@ function canWriteOrCreate(filePath) {
|
|
|
935
944
|
return false;
|
|
936
945
|
}
|
|
937
946
|
}
|
|
947
|
+
function isEphemeralPath(p) {
|
|
948
|
+
const normalized = p.replace(/\\/g, "/").toLowerCase();
|
|
949
|
+
return normalized.includes("/_npx/") || normalized.includes("\\_npx\\") || normalized.includes("/fnm_multishells/") || normalized.includes("\\fnm_multishells\\") || normalized.includes("/tmp/") || normalized.includes("\\temp\\");
|
|
950
|
+
}
|
|
938
951
|
function findLocalTapCommsSource(ctx) {
|
|
939
952
|
const candidates = [
|
|
940
953
|
path7.join(
|
|
@@ -994,8 +1007,10 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
994
1007
|
const warnings = [];
|
|
995
1008
|
const issues = [];
|
|
996
1009
|
const env = {
|
|
997
|
-
TAP_AGENT_NAME: "<set-per-session>",
|
|
998
|
-
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
|
|
1010
|
+
TAP_AGENT_NAME: ctx.agentName ?? "<set-per-session>",
|
|
1011
|
+
TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir),
|
|
1012
|
+
TAP_STATE_DIR: toForwardSlashPath(ctx.stateDir),
|
|
1013
|
+
TAP_REPO_ROOT: toForwardSlashPath(ctx.repoRoot)
|
|
999
1014
|
};
|
|
1000
1015
|
if (instanceId) {
|
|
1001
1016
|
env.TAP_AGENT_ID = instanceId;
|
|
@@ -1007,9 +1022,25 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1007
1022
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1008
1023
|
}
|
|
1009
1024
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
1025
|
+
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
1010
1026
|
let command = bunCommand;
|
|
1011
|
-
|
|
1012
|
-
|
|
1027
|
+
let args = [toForwardSlashPath(sourcePath)];
|
|
1028
|
+
if (isEphemeralSource && isBundled) {
|
|
1029
|
+
command = "npx";
|
|
1030
|
+
args = ["@hua-labs/tap", "serve"];
|
|
1031
|
+
warnings.push(
|
|
1032
|
+
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
1033
|
+
);
|
|
1034
|
+
} else if (!command && isBundled) {
|
|
1035
|
+
const isEphemeralNode = isEphemeralPath(process.execPath);
|
|
1036
|
+
if (isEphemeralNode) {
|
|
1037
|
+
command = "node";
|
|
1038
|
+
warnings.push(
|
|
1039
|
+
"Detected ephemeral node path. Using `node` from PATH for MCP config stability."
|
|
1040
|
+
);
|
|
1041
|
+
} else {
|
|
1042
|
+
command = toForwardSlashPath(process.execPath);
|
|
1043
|
+
}
|
|
1013
1044
|
warnings.push(
|
|
1014
1045
|
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
1015
1046
|
);
|
|
@@ -1021,8 +1052,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1021
1052
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1022
1053
|
}
|
|
1023
1054
|
return {
|
|
1024
|
-
command
|
|
1025
|
-
args
|
|
1055
|
+
command,
|
|
1056
|
+
args,
|
|
1026
1057
|
env,
|
|
1027
1058
|
sourcePath,
|
|
1028
1059
|
warnings,
|
|
@@ -1386,13 +1417,12 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1386
1417
|
});
|
|
1387
1418
|
}
|
|
1388
1419
|
if (mainTable && managed.command) {
|
|
1420
|
+
const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
|
|
1389
1421
|
checks.push({
|
|
1390
1422
|
name: "Managed command configured",
|
|
1391
1423
|
passed: mainTable.includes(
|
|
1392
1424
|
`command = "${managed.command.replace(/\\/g, "\\\\")}"`
|
|
1393
|
-
) && mainTable.includes(
|
|
1394
|
-
`args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
|
|
1395
|
-
),
|
|
1425
|
+
) && mainTable.includes(`args = [${expectedArgs}]`),
|
|
1396
1426
|
message: "Managed tap-comms command/args do not match expected values"
|
|
1397
1427
|
});
|
|
1398
1428
|
}
|
|
@@ -2879,6 +2909,40 @@ function isBridgeRunning(stateDir, instanceId) {
|
|
|
2879
2909
|
if (!state) return false;
|
|
2880
2910
|
return isProcessAlive(state.pid);
|
|
2881
2911
|
}
|
|
2912
|
+
function resolveAgentName(instanceId, explicit, context) {
|
|
2913
|
+
if (explicit) return explicit;
|
|
2914
|
+
try {
|
|
2915
|
+
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
2916
|
+
const state = loadState(repoRoot);
|
|
2917
|
+
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
2918
|
+
if (stateAgent) return stateAgent;
|
|
2919
|
+
} catch {
|
|
2920
|
+
}
|
|
2921
|
+
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
2922
|
+
}
|
|
2923
|
+
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
2924
|
+
const wasManaged = bridgeState?.appServer != null;
|
|
2925
|
+
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
2926
|
+
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
2927
|
+
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
2928
|
+
return { manageAppServer, noAuth };
|
|
2929
|
+
}
|
|
2930
|
+
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
2931
|
+
const removed = [];
|
|
2932
|
+
if (!fs13.existsSync(inboxDir)) return removed;
|
|
2933
|
+
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
2934
|
+
const marker = `-headless-${normalizedAgent}-review-`;
|
|
2935
|
+
try {
|
|
2936
|
+
for (const file of fs13.readdirSync(inboxDir)) {
|
|
2937
|
+
if (file.includes(marker)) {
|
|
2938
|
+
fs13.unlinkSync(path13.join(inboxDir, file));
|
|
2939
|
+
removed.push(file);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
} catch {
|
|
2943
|
+
}
|
|
2944
|
+
return removed;
|
|
2945
|
+
}
|
|
2882
2946
|
async function startBridge(options) {
|
|
2883
2947
|
const {
|
|
2884
2948
|
instanceId,
|
|
@@ -2889,7 +2953,10 @@ async function startBridge(options) {
|
|
|
2889
2953
|
agentName,
|
|
2890
2954
|
port
|
|
2891
2955
|
} = options;
|
|
2892
|
-
const resolvedAgent = agentName
|
|
2956
|
+
const resolvedAgent = resolveAgentName(instanceId, agentName, {
|
|
2957
|
+
repoRoot: options.repoRoot,
|
|
2958
|
+
stateDir
|
|
2959
|
+
});
|
|
2893
2960
|
if (!resolvedAgent) {
|
|
2894
2961
|
throw new Error(
|
|
2895
2962
|
`No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
|
|
@@ -3041,6 +3108,35 @@ async function stopBridge(options) {
|
|
|
3041
3108
|
clearBridgeState(stateDir, instanceId);
|
|
3042
3109
|
return true;
|
|
3043
3110
|
}
|
|
3111
|
+
async function restartBridge(options) {
|
|
3112
|
+
const { instanceId, stateDir, platform } = options;
|
|
3113
|
+
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
3114
|
+
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
3115
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3116
|
+
const heartbeatPath = path13.join(runtimeStateDir, "heartbeat.json");
|
|
3117
|
+
if (fs13.existsSync(heartbeatPath)) {
|
|
3118
|
+
const startWait = Date.now();
|
|
3119
|
+
while (Date.now() - startWait < drainTimeout) {
|
|
3120
|
+
try {
|
|
3121
|
+
const hb = JSON.parse(fs13.readFileSync(heartbeatPath, "utf-8"));
|
|
3122
|
+
if (!hb.activeTurnId) break;
|
|
3123
|
+
} catch {
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
if (options.headless?.enabled && options.commsDir) {
|
|
3130
|
+
const agentName = options.agentName ?? instanceId;
|
|
3131
|
+
cleanupHeadlessDispatch(path13.join(options.commsDir, "inbox"), agentName);
|
|
3132
|
+
}
|
|
3133
|
+
await stopBridge({ instanceId, stateDir, platform });
|
|
3134
|
+
const restartOptions = {
|
|
3135
|
+
...options,
|
|
3136
|
+
processExistingMessages: true
|
|
3137
|
+
};
|
|
3138
|
+
return startBridge(restartOptions);
|
|
3139
|
+
}
|
|
3044
3140
|
function rotateLog(logPath) {
|
|
3045
3141
|
if (!fs13.existsSync(logPath)) return;
|
|
3046
3142
|
try {
|
|
@@ -3192,7 +3288,13 @@ async function addCommand(args) {
|
|
|
3192
3288
|
logHeader(`@hua-labs/tap add ${instanceId}`);
|
|
3193
3289
|
if (instanceName) log(`Instance name: ${instanceName}`);
|
|
3194
3290
|
if (port !== null) log(`Port: ${port}`);
|
|
3195
|
-
const
|
|
3291
|
+
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
3292
|
+
const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
|
|
3293
|
+
const ctx = {
|
|
3294
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
3295
|
+
instanceId,
|
|
3296
|
+
agentName: effectiveAgentName
|
|
3297
|
+
};
|
|
3196
3298
|
const adapter = getAdapter(runtime);
|
|
3197
3299
|
const warnings = [];
|
|
3198
3300
|
log("Probing runtime...");
|
|
@@ -3280,14 +3382,8 @@ async function addCommand(args) {
|
|
|
3280
3382
|
logWarn("Bridge script not found. Bridge not started.");
|
|
3281
3383
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
3282
3384
|
} else {
|
|
3283
|
-
const
|
|
3284
|
-
|
|
3285
|
-
logWarn(
|
|
3286
|
-
"No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
|
|
3287
|
-
);
|
|
3288
|
-
warnings.push("Bridge not auto-started: no agent name available.");
|
|
3289
|
-
} else {
|
|
3290
|
-
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3385
|
+
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3386
|
+
{
|
|
3291
3387
|
log(`Starting bridge: ${bridgeScript}`);
|
|
3292
3388
|
try {
|
|
3293
3389
|
bridge = await startBridge({
|
|
@@ -3297,7 +3393,7 @@ async function addCommand(args) {
|
|
|
3297
3393
|
commsDir: ctx.commsDir,
|
|
3298
3394
|
bridgeScript,
|
|
3299
3395
|
platform: ctx.platform,
|
|
3300
|
-
agentName:
|
|
3396
|
+
agentName: agentNameFlag ?? void 0,
|
|
3301
3397
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
3302
3398
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
3303
3399
|
repoRoot,
|
|
@@ -3313,7 +3409,6 @@ async function addCommand(args) {
|
|
|
3313
3409
|
}
|
|
3314
3410
|
}
|
|
3315
3411
|
}
|
|
3316
|
-
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
3317
3412
|
const instanceState = {
|
|
3318
3413
|
instanceId,
|
|
3319
3414
|
runtime,
|
|
@@ -4043,7 +4138,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4043
4138
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
4044
4139
|
}
|
|
4045
4140
|
}
|
|
4046
|
-
const updated = { ...instance, bridge };
|
|
4141
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
4047
4142
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4048
4143
|
saveState(repoRoot, newState);
|
|
4049
4144
|
return {
|
|
@@ -4534,6 +4629,121 @@ function bridgeStatusOne(identifier) {
|
|
|
4534
4629
|
}
|
|
4535
4630
|
};
|
|
4536
4631
|
}
|
|
4632
|
+
async function bridgeRestart(identifier, flags) {
|
|
4633
|
+
const repoRoot = findRepoRoot();
|
|
4634
|
+
const state = loadState(repoRoot);
|
|
4635
|
+
if (!state) {
|
|
4636
|
+
return {
|
|
4637
|
+
ok: false,
|
|
4638
|
+
command: "bridge",
|
|
4639
|
+
code: "TAP_NOT_INITIALIZED",
|
|
4640
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
4641
|
+
warnings: [],
|
|
4642
|
+
data: {}
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
4646
|
+
if (!resolved.ok) {
|
|
4647
|
+
return {
|
|
4648
|
+
ok: false,
|
|
4649
|
+
command: "bridge",
|
|
4650
|
+
code: resolved.code,
|
|
4651
|
+
message: resolved.message,
|
|
4652
|
+
warnings: [],
|
|
4653
|
+
data: {}
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
const instanceId = resolved.instanceId;
|
|
4657
|
+
const inst = state.instances[instanceId];
|
|
4658
|
+
if (!inst) {
|
|
4659
|
+
return {
|
|
4660
|
+
ok: false,
|
|
4661
|
+
command: "bridge",
|
|
4662
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
4663
|
+
message: `Instance not found: ${instanceId}`,
|
|
4664
|
+
warnings: [],
|
|
4665
|
+
data: {}
|
|
4666
|
+
};
|
|
4667
|
+
}
|
|
4668
|
+
const adapter = getAdapter(inst.runtime);
|
|
4669
|
+
const ctx = {
|
|
4670
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
4671
|
+
instanceId
|
|
4672
|
+
};
|
|
4673
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
4674
|
+
if (!bridgeScript) {
|
|
4675
|
+
return {
|
|
4676
|
+
ok: false,
|
|
4677
|
+
command: "bridge",
|
|
4678
|
+
instanceId,
|
|
4679
|
+
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
4680
|
+
message: `Bridge script not found for ${instanceId}`,
|
|
4681
|
+
warnings: [],
|
|
4682
|
+
data: {}
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
4686
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
|
|
4687
|
+
const drainTimeout = parseInt(drainStr, 10) || 30;
|
|
4688
|
+
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
4689
|
+
log(`Drain timeout: ${drainTimeout}s`);
|
|
4690
|
+
try {
|
|
4691
|
+
const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
|
|
4692
|
+
const { manageAppServer, noAuth } = inferRestartMode(
|
|
4693
|
+
currentBridgeState,
|
|
4694
|
+
{
|
|
4695
|
+
noServer: flags["no-server"] === true ? true : void 0,
|
|
4696
|
+
noAuth: flags["no-auth"] === true ? true : void 0
|
|
4697
|
+
},
|
|
4698
|
+
{
|
|
4699
|
+
manageAppServer: inst.manageAppServer,
|
|
4700
|
+
noAuth: inst.noAuth
|
|
4701
|
+
}
|
|
4702
|
+
);
|
|
4703
|
+
const bridge = await restartBridge({
|
|
4704
|
+
instanceId,
|
|
4705
|
+
runtime: inst.runtime,
|
|
4706
|
+
stateDir: ctx.stateDir,
|
|
4707
|
+
commsDir: ctx.commsDir,
|
|
4708
|
+
bridgeScript,
|
|
4709
|
+
platform: ctx.platform,
|
|
4710
|
+
agentName: inst.agentName ?? void 0,
|
|
4711
|
+
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
4712
|
+
appServerUrl: resolvedConfig.appServerUrl,
|
|
4713
|
+
repoRoot,
|
|
4714
|
+
port: inst.port ?? void 0,
|
|
4715
|
+
headless: inst.headless,
|
|
4716
|
+
drainTimeoutSeconds: drainTimeout,
|
|
4717
|
+
manageAppServer,
|
|
4718
|
+
noAuth
|
|
4719
|
+
});
|
|
4720
|
+
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
4721
|
+
const updated = { ...inst, bridge, manageAppServer, noAuth };
|
|
4722
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
4723
|
+
saveState(repoRoot, newState);
|
|
4724
|
+
return {
|
|
4725
|
+
ok: true,
|
|
4726
|
+
command: "bridge",
|
|
4727
|
+
instanceId,
|
|
4728
|
+
code: "TAP_BRIDGE_START_OK",
|
|
4729
|
+
message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
|
|
4730
|
+
warnings: [],
|
|
4731
|
+
data: { pid: bridge.pid }
|
|
4732
|
+
};
|
|
4733
|
+
} catch (err) {
|
|
4734
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4735
|
+
logError(msg);
|
|
4736
|
+
return {
|
|
4737
|
+
ok: false,
|
|
4738
|
+
command: "bridge",
|
|
4739
|
+
instanceId,
|
|
4740
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
4741
|
+
message: msg,
|
|
4742
|
+
warnings: [],
|
|
4743
|
+
data: {}
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4537
4747
|
async function bridgeCommand(args) {
|
|
4538
4748
|
const { positional, flags } = parseArgs(args);
|
|
4539
4749
|
const subcommand = positional[0];
|
|
@@ -4593,12 +4803,25 @@ async function bridgeCommand(args) {
|
|
|
4593
4803
|
}
|
|
4594
4804
|
return bridgeStatusAll();
|
|
4595
4805
|
}
|
|
4806
|
+
case "restart": {
|
|
4807
|
+
if (!identifierArg) {
|
|
4808
|
+
return {
|
|
4809
|
+
ok: false,
|
|
4810
|
+
command: "bridge",
|
|
4811
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4812
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
|
|
4813
|
+
warnings: [],
|
|
4814
|
+
data: {}
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
return bridgeRestart(identifierArg, flags);
|
|
4818
|
+
}
|
|
4596
4819
|
default:
|
|
4597
4820
|
return {
|
|
4598
4821
|
ok: false,
|
|
4599
4822
|
command: "bridge",
|
|
4600
4823
|
code: "TAP_INVALID_ARGUMENT",
|
|
4601
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
4824
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
4602
4825
|
warnings: [],
|
|
4603
4826
|
data: {}
|
|
4604
4827
|
};
|
|
@@ -4900,7 +5123,9 @@ async function serveCommand(args) {
|
|
|
4900
5123
|
data: {}
|
|
4901
5124
|
};
|
|
4902
5125
|
}
|
|
4903
|
-
const
|
|
5126
|
+
const serveCommand2 = managed.command === "npx" ? "node" : managed.command;
|
|
5127
|
+
const serveArgs = managed.command === "npx" && managed.sourcePath ? [managed.sourcePath] : managed.args;
|
|
5128
|
+
const child = spawn2(serveCommand2, serveArgs, {
|
|
4904
5129
|
stdio: "inherit",
|
|
4905
5130
|
env: {
|
|
4906
5131
|
...process.env,
|
|
@@ -5446,6 +5671,7 @@ import {
|
|
|
5446
5671
|
statSync as statSync2,
|
|
5447
5672
|
unlinkSync as unlinkSync3
|
|
5448
5673
|
} from "fs";
|
|
5674
|
+
import { execSync as execSync7 } from "child_process";
|
|
5449
5675
|
import { join as join17 } from "path";
|
|
5450
5676
|
var PASS = "pass";
|
|
5451
5677
|
var WARN = "warn";
|
|
@@ -5674,35 +5900,205 @@ function checkMessageLifecycle(commsDir) {
|
|
|
5674
5900
|
function checkMcpServer(repoRoot) {
|
|
5675
5901
|
const checks = [];
|
|
5676
5902
|
const mcpJson = join17(repoRoot, ".mcp.json");
|
|
5677
|
-
if (existsSync16(mcpJson)) {
|
|
5903
|
+
if (!existsSync16(mcpJson)) {
|
|
5904
|
+
checks.push({
|
|
5905
|
+
name: "MCP config (.mcp.json)",
|
|
5906
|
+
status: WARN,
|
|
5907
|
+
message: "Not found \u2014 MCP channel notifications won't work"
|
|
5908
|
+
});
|
|
5909
|
+
return checks;
|
|
5910
|
+
}
|
|
5911
|
+
let config;
|
|
5912
|
+
try {
|
|
5913
|
+
config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
|
|
5914
|
+
} catch {
|
|
5915
|
+
checks.push({
|
|
5916
|
+
name: "MCP config (.mcp.json)",
|
|
5917
|
+
status: WARN,
|
|
5918
|
+
message: "File exists but invalid JSON"
|
|
5919
|
+
});
|
|
5920
|
+
return checks;
|
|
5921
|
+
}
|
|
5922
|
+
const hasTapComms = config?.mcpServers?.["tap-comms"];
|
|
5923
|
+
if (!hasTapComms) {
|
|
5924
|
+
checks.push({
|
|
5925
|
+
name: "MCP config (.mcp.json)",
|
|
5926
|
+
status: WARN,
|
|
5927
|
+
message: "tap-comms not configured"
|
|
5928
|
+
});
|
|
5929
|
+
return checks;
|
|
5930
|
+
}
|
|
5931
|
+
checks.push({
|
|
5932
|
+
name: "MCP config (.mcp.json)",
|
|
5933
|
+
status: PASS,
|
|
5934
|
+
message: `command: ${hasTapComms.command}`
|
|
5935
|
+
});
|
|
5936
|
+
if (hasTapComms.command) {
|
|
5937
|
+
const cmd = hasTapComms.command;
|
|
5938
|
+
let cmdAvailable = existsSync16(cmd);
|
|
5939
|
+
if (!cmdAvailable) {
|
|
5940
|
+
try {
|
|
5941
|
+
execSync7(`"${cmd}" --version`, {
|
|
5942
|
+
stdio: "pipe",
|
|
5943
|
+
timeout: 5e3
|
|
5944
|
+
});
|
|
5945
|
+
cmdAvailable = true;
|
|
5946
|
+
} catch {
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
checks.push({
|
|
5950
|
+
name: "MCP command binary",
|
|
5951
|
+
status: cmdAvailable ? PASS : FAIL,
|
|
5952
|
+
message: cmdAvailable ? cmd : `Not found: ${cmd} (checked PATH and absolute)`
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
if (hasTapComms.args?.[0]) {
|
|
5956
|
+
const mcpScript = hasTapComms.args[0];
|
|
5957
|
+
checks.push({
|
|
5958
|
+
name: "MCP server script",
|
|
5959
|
+
status: existsSync16(mcpScript) ? PASS : FAIL,
|
|
5960
|
+
message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
5961
|
+
});
|
|
5962
|
+
if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
|
|
5963
|
+
checks.push({
|
|
5964
|
+
name: "MCP SQLite support",
|
|
5965
|
+
status: WARN,
|
|
5966
|
+
message: "Node + .mjs = no SQLite (bun:sqlite unavailable). Use bun or .ts source for full features."
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
if (!hasTapComms.cwd) {
|
|
5971
|
+
checks.push({
|
|
5972
|
+
name: "MCP cwd field",
|
|
5973
|
+
status: WARN,
|
|
5974
|
+
message: "No cwd in .mcp.json \u2014 worktree sessions may fail to resolve MCP server dependencies"
|
|
5975
|
+
});
|
|
5976
|
+
} else {
|
|
5977
|
+
checks.push({
|
|
5978
|
+
name: "MCP cwd field",
|
|
5979
|
+
status: PASS,
|
|
5980
|
+
message: hasTapComms.cwd
|
|
5981
|
+
});
|
|
5982
|
+
}
|
|
5983
|
+
const envCommsDir = hasTapComms.env?.TAP_COMMS_DIR;
|
|
5984
|
+
if (!envCommsDir) {
|
|
5985
|
+
checks.push({
|
|
5986
|
+
name: "MCP TAP_COMMS_DIR",
|
|
5987
|
+
status: FAIL,
|
|
5988
|
+
message: "TAP_COMMS_DIR not set in .mcp.json env \u2014 server will fail to start"
|
|
5989
|
+
});
|
|
5990
|
+
} else {
|
|
5991
|
+
checks.push({
|
|
5992
|
+
name: "MCP TAP_COMMS_DIR",
|
|
5993
|
+
status: existsSync16(envCommsDir) ? PASS : FAIL,
|
|
5994
|
+
message: existsSync16(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
checks.push({
|
|
5998
|
+
name: "MCP session cache",
|
|
5999
|
+
status: PASS,
|
|
6000
|
+
message: "If .mcp.json was changed mid-session, restart Claude (Ctrl+C \u2192 claude --resume) to reload"
|
|
6001
|
+
});
|
|
6002
|
+
return checks;
|
|
6003
|
+
}
|
|
6004
|
+
function checkBridgeTurnHealth(repoRoot) {
|
|
6005
|
+
const checks = [];
|
|
6006
|
+
const tmpDir = join17(repoRoot, ".tmp");
|
|
6007
|
+
if (!existsSync16(tmpDir)) return checks;
|
|
6008
|
+
const state = loadState(repoRoot);
|
|
6009
|
+
const activeMatchers = /* @__PURE__ */ new Set();
|
|
6010
|
+
if (state) {
|
|
6011
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
6012
|
+
if (inst?.installed && inst.bridgeMode === "app-server") {
|
|
6013
|
+
activeMatchers.add(id);
|
|
6014
|
+
if (inst.agentName) activeMatchers.add(inst.agentName);
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
let dirs;
|
|
6019
|
+
try {
|
|
6020
|
+
dirs = readdirSync4(tmpDir).filter((d) => {
|
|
6021
|
+
if (!d.startsWith("codex-app-server-bridge")) return false;
|
|
6022
|
+
const suffix = d.replace("codex-app-server-bridge-", "");
|
|
6023
|
+
if (activeMatchers.size === 0) return true;
|
|
6024
|
+
for (const matcher of activeMatchers) {
|
|
6025
|
+
if (suffix === matcher || suffix.startsWith(matcher)) return true;
|
|
6026
|
+
}
|
|
6027
|
+
return false;
|
|
6028
|
+
});
|
|
6029
|
+
} catch {
|
|
6030
|
+
return checks;
|
|
6031
|
+
}
|
|
6032
|
+
for (const dir of dirs) {
|
|
6033
|
+
const heartbeatPath = join17(tmpDir, dir, "heartbeat.json");
|
|
6034
|
+
if (!existsSync16(heartbeatPath)) continue;
|
|
6035
|
+
let heartbeat;
|
|
5678
6036
|
try {
|
|
5679
|
-
|
|
5680
|
-
|
|
6037
|
+
heartbeat = JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
|
|
6038
|
+
} catch {
|
|
6039
|
+
checks.push({
|
|
6040
|
+
name: `turn: ${dir}`,
|
|
6041
|
+
status: WARN,
|
|
6042
|
+
message: "heartbeat.json unreadable"
|
|
6043
|
+
});
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
const heartbeatAge = heartbeat.updatedAt ? Math.floor(
|
|
6047
|
+
(Date.now() - new Date(heartbeat.updatedAt).getTime()) / 1e3
|
|
6048
|
+
) : null;
|
|
6049
|
+
if (heartbeat.connected === false || heartbeat.initialized === false) {
|
|
5681
6050
|
checks.push({
|
|
5682
|
-
name:
|
|
5683
|
-
status:
|
|
5684
|
-
message:
|
|
6051
|
+
name: `turn: ${dir}`,
|
|
6052
|
+
status: FAIL,
|
|
6053
|
+
message: `disconnected (connected=${heartbeat.connected}, initialized=${heartbeat.initialized})${heartbeat.lastError ? ` \u2014 ${heartbeat.lastError}` : ""}`
|
|
5685
6054
|
});
|
|
5686
|
-
|
|
5687
|
-
|
|
6055
|
+
continue;
|
|
6056
|
+
}
|
|
6057
|
+
if (heartbeatAge !== null && heartbeatAge > 300) {
|
|
6058
|
+
checks.push({
|
|
6059
|
+
name: `turn: ${dir}`,
|
|
6060
|
+
status: FAIL,
|
|
6061
|
+
message: `dead \u2014 heartbeat ${Math.round(heartbeatAge)}s ago, no updates`
|
|
6062
|
+
});
|
|
6063
|
+
continue;
|
|
6064
|
+
}
|
|
6065
|
+
if (heartbeat.activeTurnId) {
|
|
6066
|
+
const ZOMBIE_THRESHOLD = 30 * 60;
|
|
6067
|
+
const lastNotifAge = heartbeat.lastNotificationAt ? Math.floor(
|
|
6068
|
+
(Date.now() - new Date(heartbeat.lastNotificationAt).getTime()) / 1e3
|
|
6069
|
+
) : null;
|
|
6070
|
+
if (lastNotifAge !== null && lastNotifAge > ZOMBIE_THRESHOLD) {
|
|
6071
|
+
checks.push({
|
|
6072
|
+
name: `turn: ${dir}`,
|
|
6073
|
+
status: WARN,
|
|
6074
|
+
message: `zombie \u2014 active turn ${heartbeat.activeTurnId}, last notification ${Math.round(lastNotifAge / 60)}m ago (${heartbeat.lastNotificationMethod ?? "?"}). MCP tools may not be exposed in app-server turns \u2014 try bridge restart${heartbeat.lastError ? `. Error: ${heartbeat.lastError}` : ""}`
|
|
6075
|
+
});
|
|
6076
|
+
continue;
|
|
6077
|
+
}
|
|
6078
|
+
const failures2 = heartbeat.consecutiveFailureCount ?? 0;
|
|
6079
|
+
if (failures2 > 0 && heartbeatAge !== null && heartbeatAge < 60) {
|
|
5688
6080
|
checks.push({
|
|
5689
|
-
name:
|
|
5690
|
-
status:
|
|
5691
|
-
message:
|
|
6081
|
+
name: `turn: ${dir}`,
|
|
6082
|
+
status: WARN,
|
|
6083
|
+
message: `zombie \u2014 active turn ${heartbeat.activeTurnId}, ${failures2} consecutive failures. MCP tools may not be exposed in app-server turns \u2014 try bridge restart${heartbeat.lastError ? `. Error: ${heartbeat.lastError}` : ""}`
|
|
5692
6084
|
});
|
|
6085
|
+
continue;
|
|
5693
6086
|
}
|
|
5694
|
-
}
|
|
6087
|
+
}
|
|
6088
|
+
const failures = heartbeat.consecutiveFailureCount ?? 0;
|
|
6089
|
+
if (failures > 5) {
|
|
5695
6090
|
checks.push({
|
|
5696
|
-
name:
|
|
6091
|
+
name: `turn: ${dir}`,
|
|
5697
6092
|
status: WARN,
|
|
5698
|
-
message:
|
|
6093
|
+
message: `slow \u2014 ${failures} consecutive failures, last: ${heartbeat.lastError ?? "unknown"}`
|
|
5699
6094
|
});
|
|
6095
|
+
continue;
|
|
5700
6096
|
}
|
|
5701
|
-
|
|
6097
|
+
const turnInfo = heartbeat.activeTurnId ? `active turn ${heartbeat.activeTurnId}` : `idle (last: ${heartbeat.lastTurnStatus ?? "none"})`;
|
|
5702
6098
|
checks.push({
|
|
5703
|
-
name:
|
|
5704
|
-
status:
|
|
5705
|
-
message:
|
|
6099
|
+
name: `turn: ${dir}`,
|
|
6100
|
+
status: PASS,
|
|
6101
|
+
message: `healthy \u2014 ${turnInfo}, heartbeat ${heartbeatAge ?? "?"}s ago`
|
|
5706
6102
|
});
|
|
5707
6103
|
}
|
|
5708
6104
|
return checks;
|
|
@@ -5741,10 +6137,17 @@ async function doctorCommand(args) {
|
|
|
5741
6137
|
checks.push(...checkInstances(repoRoot, config.stateDir));
|
|
5742
6138
|
checks.push(...checkMessageLifecycle(commsDir));
|
|
5743
6139
|
checks.push(...checkMcpServer(repoRoot));
|
|
6140
|
+
checks.push(...checkBridgeTurnHealth(repoRoot));
|
|
5744
6141
|
return checks;
|
|
5745
6142
|
}
|
|
5746
6143
|
const initialChecks = runAllChecks();
|
|
5747
|
-
for (const section of [
|
|
6144
|
+
for (const section of [
|
|
6145
|
+
"Comms",
|
|
6146
|
+
"Instances",
|
|
6147
|
+
"Messages",
|
|
6148
|
+
"MCP",
|
|
6149
|
+
"Turns"
|
|
6150
|
+
]) {
|
|
5748
6151
|
const sectionChecks = {
|
|
5749
6152
|
Comms: initialChecks.filter(
|
|
5750
6153
|
(c) => [
|
|
@@ -5763,7 +6166,8 @@ async function doctorCommand(args) {
|
|
|
5763
6166
|
),
|
|
5764
6167
|
MCP: initialChecks.filter(
|
|
5765
6168
|
(c) => c.name.startsWith("MCP") || c.name === "MCP server script"
|
|
5766
|
-
)
|
|
6169
|
+
),
|
|
6170
|
+
Turns: initialChecks.filter((c) => c.name.startsWith("turn:"))
|
|
5767
6171
|
}[section];
|
|
5768
6172
|
if (sectionChecks.length > 0) {
|
|
5769
6173
|
log(`${section}:`);
|
|
@@ -5824,7 +6228,7 @@ async function doctorCommand(args) {
|
|
|
5824
6228
|
}
|
|
5825
6229
|
|
|
5826
6230
|
// src/commands/comms.ts
|
|
5827
|
-
import { execSync as
|
|
6231
|
+
import { execSync as execSync8 } from "child_process";
|
|
5828
6232
|
import * as fs17 from "fs";
|
|
5829
6233
|
import * as path18 from "path";
|
|
5830
6234
|
var COMMS_HELP = `
|
|
@@ -5856,7 +6260,7 @@ function commsPull(commsDir) {
|
|
|
5856
6260
|
};
|
|
5857
6261
|
}
|
|
5858
6262
|
try {
|
|
5859
|
-
const output =
|
|
6263
|
+
const output = execSync8("git pull --rebase", {
|
|
5860
6264
|
cwd: commsDir,
|
|
5861
6265
|
encoding: "utf-8",
|
|
5862
6266
|
stdio: "pipe"
|
|
@@ -5898,8 +6302,8 @@ function commsPush(commsDir) {
|
|
|
5898
6302
|
};
|
|
5899
6303
|
}
|
|
5900
6304
|
try {
|
|
5901
|
-
|
|
5902
|
-
const status =
|
|
6305
|
+
execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
|
|
6306
|
+
const status = execSync8("git status --porcelain", {
|
|
5903
6307
|
cwd: commsDir,
|
|
5904
6308
|
encoding: "utf-8",
|
|
5905
6309
|
stdio: "pipe"
|
|
@@ -5916,11 +6320,11 @@ function commsPush(commsDir) {
|
|
|
5916
6320
|
};
|
|
5917
6321
|
}
|
|
5918
6322
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5919
|
-
|
|
6323
|
+
execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
|
|
5920
6324
|
cwd: commsDir,
|
|
5921
6325
|
stdio: "pipe"
|
|
5922
6326
|
});
|
|
5923
|
-
|
|
6327
|
+
execSync8("git push", { cwd: commsDir, stdio: "pipe" });
|
|
5924
6328
|
logSuccess("Comms push complete");
|
|
5925
6329
|
return {
|
|
5926
6330
|
ok: true,
|