@hua-labs/tap 0.2.0 → 0.2.2
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.d.mts +55 -0
- package/dist/bridges/codex-app-server-bridge.mjs +1365 -0
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +2 -1
- package/dist/bridges/codex-bridge-runner.mjs +143 -38
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +714 -64
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +94 -4
- package/dist/index.mjs +3795 -190
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/commands/init.ts
|
|
2
2
|
import * as fs6 from "fs";
|
|
3
3
|
import * as path6 from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
4
5
|
|
|
5
6
|
// src/state.ts
|
|
6
7
|
import * as fs3 from "fs";
|
|
@@ -186,7 +187,7 @@ function loadJsonFile(filePath) {
|
|
|
186
187
|
return null;
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
|
-
function
|
|
190
|
+
function loadSharedConfig2(repoRoot) {
|
|
190
191
|
return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
|
|
191
192
|
}
|
|
192
193
|
function loadLocalConfig(repoRoot) {
|
|
@@ -210,7 +211,7 @@ function loadLegacyShellConfig(repoRoot) {
|
|
|
210
211
|
}
|
|
211
212
|
function resolveConfig(overrides = {}, startDir) {
|
|
212
213
|
const repoRoot = findRepoRoot2(startDir);
|
|
213
|
-
const shared =
|
|
214
|
+
const shared = loadSharedConfig2(repoRoot) ?? {};
|
|
214
215
|
const local = loadLocalConfig(repoRoot) ?? {};
|
|
215
216
|
const legacy = loadLegacyShellConfig(repoRoot) ?? {};
|
|
216
217
|
const sources = {
|
|
@@ -218,7 +219,8 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
218
219
|
commsDir: "auto",
|
|
219
220
|
stateDir: "auto",
|
|
220
221
|
runtimeCommand: "auto",
|
|
221
|
-
appServerUrl: "auto"
|
|
222
|
+
appServerUrl: "auto",
|
|
223
|
+
towerName: "auto"
|
|
222
224
|
};
|
|
223
225
|
let commsDir;
|
|
224
226
|
if (overrides.commsDir) {
|
|
@@ -287,11 +289,25 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
287
289
|
} else {
|
|
288
290
|
appServerUrl = DEFAULT_APP_SERVER_URL;
|
|
289
291
|
}
|
|
292
|
+
const towerName = local.towerName ?? shared.towerName ?? null;
|
|
290
293
|
return {
|
|
291
|
-
config: {
|
|
294
|
+
config: {
|
|
295
|
+
repoRoot,
|
|
296
|
+
commsDir,
|
|
297
|
+
stateDir,
|
|
298
|
+
runtimeCommand,
|
|
299
|
+
appServerUrl,
|
|
300
|
+
towerName
|
|
301
|
+
},
|
|
292
302
|
sources
|
|
293
303
|
};
|
|
294
304
|
}
|
|
305
|
+
function saveSharedConfig(repoRoot, config) {
|
|
306
|
+
const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
|
|
307
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
308
|
+
fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
309
|
+
fs2.renameSync(tmp, filePath);
|
|
310
|
+
}
|
|
295
311
|
function resolvePath(repoRoot, p) {
|
|
296
312
|
const normalized = normalizeTapPath(p);
|
|
297
313
|
return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
|
|
@@ -753,6 +769,64 @@ async function initCommand(args) {
|
|
|
753
769
|
};
|
|
754
770
|
}
|
|
755
771
|
logHeader("@hua-labs/tap init");
|
|
772
|
+
const commsRepoIdx = args.indexOf("--comms-repo");
|
|
773
|
+
const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
|
|
774
|
+
if (commsRepoUrl) {
|
|
775
|
+
if (fs6.existsSync(commsDir) && fs6.readdirSync(commsDir).length > 0) {
|
|
776
|
+
const gitDir = path6.join(commsDir, ".git");
|
|
777
|
+
if (fs6.existsSync(gitDir)) {
|
|
778
|
+
log(`Comms directory exists: ${commsDir}`);
|
|
779
|
+
logSuccess("Comms directory is already a git repo \u2014 linking only");
|
|
780
|
+
} else {
|
|
781
|
+
logError(`Comms directory exists but is not a git repo: ${commsDir}`);
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
command: "init",
|
|
785
|
+
code: "TAP_INIT_CLONE_FAILED",
|
|
786
|
+
message: `Comms directory "${commsDir}" exists but is not a git repo. Remove it or use --force to reinitialize.`,
|
|
787
|
+
warnings: [],
|
|
788
|
+
data: { commsDir, commsRepoUrl }
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
log(`Cloning comms repo: ${commsRepoUrl}`);
|
|
793
|
+
try {
|
|
794
|
+
execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
|
|
795
|
+
stdio: "pipe",
|
|
796
|
+
encoding: "utf-8"
|
|
797
|
+
});
|
|
798
|
+
logSuccess(`Cloned comms repo to ${commsDir}`);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
801
|
+
logError(`Failed to clone comms repo: ${msg}`);
|
|
802
|
+
return {
|
|
803
|
+
ok: false,
|
|
804
|
+
command: "init",
|
|
805
|
+
code: "TAP_INIT_CLONE_FAILED",
|
|
806
|
+
message: `Failed to clone comms repo: ${msg}`,
|
|
807
|
+
warnings: [],
|
|
808
|
+
data: { commsRepoUrl }
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
{
|
|
814
|
+
const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
|
|
815
|
+
let configChanged = false;
|
|
816
|
+
if (commsRepoUrl) {
|
|
817
|
+
sharedConfig.commsRepoUrl = commsRepoUrl;
|
|
818
|
+
configChanged = true;
|
|
819
|
+
}
|
|
820
|
+
const commsDirRelative = path6.relative(repoRoot, commsDir);
|
|
821
|
+
if (commsDirRelative && commsDirRelative !== "tap-comms") {
|
|
822
|
+
sharedConfig.commsDir = commsDirRelative;
|
|
823
|
+
configChanged = true;
|
|
824
|
+
}
|
|
825
|
+
if (configChanged) {
|
|
826
|
+
saveSharedConfig(repoRoot, sharedConfig);
|
|
827
|
+
logSuccess("Saved comms config to tap-config.json");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
756
830
|
log(`Comms directory: ${commsDir}`);
|
|
757
831
|
for (const dir of COMMS_DIRS) {
|
|
758
832
|
const dirPath = path6.join(commsDir, dir);
|
|
@@ -829,7 +903,7 @@ ${entry}
|
|
|
829
903
|
// src/adapters/claude.ts
|
|
830
904
|
import * as fs8 from "fs";
|
|
831
905
|
import * as path8 from "path";
|
|
832
|
-
import { execSync } from "child_process";
|
|
906
|
+
import { execSync as execSync2 } from "child_process";
|
|
833
907
|
|
|
834
908
|
// src/adapters/common.ts
|
|
835
909
|
import * as fs7 from "fs";
|
|
@@ -870,6 +944,10 @@ function canWriteOrCreate(filePath) {
|
|
|
870
944
|
return false;
|
|
871
945
|
}
|
|
872
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
|
+
}
|
|
873
951
|
function findLocalTapCommsSource(ctx) {
|
|
874
952
|
const candidates = [
|
|
875
953
|
path7.join(
|
|
@@ -929,8 +1007,10 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
929
1007
|
const warnings = [];
|
|
930
1008
|
const issues = [];
|
|
931
1009
|
const env = {
|
|
932
|
-
TAP_AGENT_NAME: "<set-per-session>",
|
|
933
|
-
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)
|
|
934
1014
|
};
|
|
935
1015
|
if (instanceId) {
|
|
936
1016
|
env.TAP_AGENT_ID = instanceId;
|
|
@@ -943,11 +1023,29 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
943
1023
|
}
|
|
944
1024
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
945
1025
|
let command = bunCommand;
|
|
1026
|
+
let args = [toForwardSlashPath(sourcePath)];
|
|
946
1027
|
if (!command && isBundled) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1028
|
+
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
1029
|
+
const isEphemeralNode = isEphemeralPath(process.execPath);
|
|
1030
|
+
if (isEphemeralSource) {
|
|
1031
|
+
command = "npx";
|
|
1032
|
+
args = ["@hua-labs/tap", "serve"];
|
|
1033
|
+
warnings.push(
|
|
1034
|
+
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
1035
|
+
);
|
|
1036
|
+
} else 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
|
+
}
|
|
1044
|
+
if (!isEphemeralSource) {
|
|
1045
|
+
warnings.push(
|
|
1046
|
+
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
951
1049
|
}
|
|
952
1050
|
if (!command) {
|
|
953
1051
|
issues.push(
|
|
@@ -956,8 +1054,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
956
1054
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
957
1055
|
}
|
|
958
1056
|
return {
|
|
959
|
-
command
|
|
960
|
-
args
|
|
1057
|
+
command,
|
|
1058
|
+
args,
|
|
961
1059
|
env,
|
|
962
1060
|
sourcePath,
|
|
963
1061
|
warnings,
|
|
@@ -972,7 +1070,7 @@ function findMcpJsonPath(ctx) {
|
|
|
972
1070
|
}
|
|
973
1071
|
function findClaudeCommand() {
|
|
974
1072
|
try {
|
|
975
|
-
|
|
1073
|
+
execSync2("claude --version", { stdio: "pipe" });
|
|
976
1074
|
return "claude";
|
|
977
1075
|
} catch {
|
|
978
1076
|
return null;
|
|
@@ -1834,13 +1932,13 @@ import * as fs13 from "fs";
|
|
|
1834
1932
|
import * as net from "net";
|
|
1835
1933
|
import * as path13 from "path";
|
|
1836
1934
|
import { randomBytes } from "crypto";
|
|
1837
|
-
import { spawn, spawnSync as spawnSync2, execSync as
|
|
1935
|
+
import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
|
|
1838
1936
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1839
1937
|
|
|
1840
1938
|
// src/runtime/resolve-node.ts
|
|
1841
1939
|
import * as fs12 from "fs";
|
|
1842
1940
|
import * as path12 from "path";
|
|
1843
|
-
import { execSync as
|
|
1941
|
+
import { execSync as execSync3 } from "child_process";
|
|
1844
1942
|
function readNodeVersion(repoRoot) {
|
|
1845
1943
|
const nvFile = path12.join(repoRoot, ".node-version");
|
|
1846
1944
|
if (!fs12.existsSync(nvFile)) return null;
|
|
@@ -1883,7 +1981,7 @@ function probeFnmNode(desiredVersion) {
|
|
|
1883
1981
|
);
|
|
1884
1982
|
if (!fs12.existsSync(candidate)) continue;
|
|
1885
1983
|
try {
|
|
1886
|
-
const v =
|
|
1984
|
+
const v = execSync3(`"${candidate}" --version`, {
|
|
1887
1985
|
encoding: "utf-8",
|
|
1888
1986
|
timeout: 5e3
|
|
1889
1987
|
}).trim();
|
|
@@ -1897,7 +1995,7 @@ function probeFnmNode(desiredVersion) {
|
|
|
1897
1995
|
}
|
|
1898
1996
|
function detectNodeMajorVersion(command) {
|
|
1899
1997
|
try {
|
|
1900
|
-
const version2 =
|
|
1998
|
+
const version2 = execSync3(`"${command}" --version`, {
|
|
1901
1999
|
encoding: "utf-8",
|
|
1902
2000
|
timeout: 5e3
|
|
1903
2001
|
}).trim();
|
|
@@ -1911,7 +2009,7 @@ function checkStripTypesSupport(command) {
|
|
|
1911
2009
|
const major = detectNodeMajorVersion(command);
|
|
1912
2010
|
if (major !== null && major >= 22) return true;
|
|
1913
2011
|
try {
|
|
1914
|
-
|
|
2012
|
+
execSync3(`"${command}" --experimental-strip-types -e ""`, {
|
|
1915
2013
|
timeout: 5e3,
|
|
1916
2014
|
stdio: "pipe"
|
|
1917
2015
|
});
|
|
@@ -2469,7 +2567,7 @@ async function terminateProcess(pid, platform) {
|
|
|
2469
2567
|
}
|
|
2470
2568
|
try {
|
|
2471
2569
|
if (platform === "win32") {
|
|
2472
|
-
|
|
2570
|
+
execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2473
2571
|
} else {
|
|
2474
2572
|
process.kill(pid, "SIGTERM");
|
|
2475
2573
|
await delay(2e3);
|
|
@@ -2814,6 +2912,40 @@ function isBridgeRunning(stateDir, instanceId) {
|
|
|
2814
2912
|
if (!state) return false;
|
|
2815
2913
|
return isProcessAlive(state.pid);
|
|
2816
2914
|
}
|
|
2915
|
+
function resolveAgentName(instanceId, explicit, context) {
|
|
2916
|
+
if (explicit) return explicit;
|
|
2917
|
+
try {
|
|
2918
|
+
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
2919
|
+
const state = loadState(repoRoot);
|
|
2920
|
+
const stateAgent = state?.instances[instanceId]?.agentName;
|
|
2921
|
+
if (stateAgent) return stateAgent;
|
|
2922
|
+
} catch {
|
|
2923
|
+
}
|
|
2924
|
+
return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
|
|
2925
|
+
}
|
|
2926
|
+
function inferRestartMode(bridgeState, flags, savedMode) {
|
|
2927
|
+
const wasManaged = bridgeState?.appServer != null;
|
|
2928
|
+
const hadAuth = bridgeState?.appServer?.auth != null;
|
|
2929
|
+
const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
|
|
2930
|
+
const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
|
|
2931
|
+
return { manageAppServer, noAuth };
|
|
2932
|
+
}
|
|
2933
|
+
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
2934
|
+
const removed = [];
|
|
2935
|
+
if (!fs13.existsSync(inboxDir)) return removed;
|
|
2936
|
+
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
2937
|
+
const marker = `-headless-${normalizedAgent}-review-`;
|
|
2938
|
+
try {
|
|
2939
|
+
for (const file of fs13.readdirSync(inboxDir)) {
|
|
2940
|
+
if (file.includes(marker)) {
|
|
2941
|
+
fs13.unlinkSync(path13.join(inboxDir, file));
|
|
2942
|
+
removed.push(file);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
} catch {
|
|
2946
|
+
}
|
|
2947
|
+
return removed;
|
|
2948
|
+
}
|
|
2817
2949
|
async function startBridge(options) {
|
|
2818
2950
|
const {
|
|
2819
2951
|
instanceId,
|
|
@@ -2824,7 +2956,10 @@ async function startBridge(options) {
|
|
|
2824
2956
|
agentName,
|
|
2825
2957
|
port
|
|
2826
2958
|
} = options;
|
|
2827
|
-
const resolvedAgent = agentName
|
|
2959
|
+
const resolvedAgent = resolveAgentName(instanceId, agentName, {
|
|
2960
|
+
repoRoot: options.repoRoot,
|
|
2961
|
+
stateDir
|
|
2962
|
+
});
|
|
2828
2963
|
if (!resolvedAgent) {
|
|
2829
2964
|
throw new Error(
|
|
2830
2965
|
`No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
|
|
@@ -2976,6 +3111,35 @@ async function stopBridge(options) {
|
|
|
2976
3111
|
clearBridgeState(stateDir, instanceId);
|
|
2977
3112
|
return true;
|
|
2978
3113
|
}
|
|
3114
|
+
async function restartBridge(options) {
|
|
3115
|
+
const { instanceId, stateDir, platform } = options;
|
|
3116
|
+
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
3117
|
+
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
3118
|
+
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3119
|
+
const heartbeatPath = path13.join(runtimeStateDir, "heartbeat.json");
|
|
3120
|
+
if (fs13.existsSync(heartbeatPath)) {
|
|
3121
|
+
const startWait = Date.now();
|
|
3122
|
+
while (Date.now() - startWait < drainTimeout) {
|
|
3123
|
+
try {
|
|
3124
|
+
const hb = JSON.parse(fs13.readFileSync(heartbeatPath, "utf-8"));
|
|
3125
|
+
if (!hb.activeTurnId) break;
|
|
3126
|
+
} catch {
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (options.headless?.enabled && options.commsDir) {
|
|
3133
|
+
const agentName = options.agentName ?? instanceId;
|
|
3134
|
+
cleanupHeadlessDispatch(path13.join(options.commsDir, "inbox"), agentName);
|
|
3135
|
+
}
|
|
3136
|
+
await stopBridge({ instanceId, stateDir, platform });
|
|
3137
|
+
const restartOptions = {
|
|
3138
|
+
...options,
|
|
3139
|
+
processExistingMessages: true
|
|
3140
|
+
};
|
|
3141
|
+
return startBridge(restartOptions);
|
|
3142
|
+
}
|
|
2979
3143
|
function rotateLog(logPath) {
|
|
2980
3144
|
if (!fs13.existsSync(logPath)) return;
|
|
2981
3145
|
try {
|
|
@@ -3127,7 +3291,13 @@ async function addCommand(args) {
|
|
|
3127
3291
|
logHeader(`@hua-labs/tap add ${instanceId}`);
|
|
3128
3292
|
if (instanceName) log(`Instance name: ${instanceName}`);
|
|
3129
3293
|
if (port !== null) log(`Port: ${port}`);
|
|
3130
|
-
const
|
|
3294
|
+
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
3295
|
+
const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
|
|
3296
|
+
const ctx = {
|
|
3297
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
3298
|
+
instanceId,
|
|
3299
|
+
agentName: effectiveAgentName
|
|
3300
|
+
};
|
|
3131
3301
|
const adapter = getAdapter(runtime);
|
|
3132
3302
|
const warnings = [];
|
|
3133
3303
|
log("Probing runtime...");
|
|
@@ -3215,14 +3385,8 @@ async function addCommand(args) {
|
|
|
3215
3385
|
logWarn("Bridge script not found. Bridge not started.");
|
|
3216
3386
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
3217
3387
|
} else {
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
logWarn(
|
|
3221
|
-
"No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
|
|
3222
|
-
);
|
|
3223
|
-
warnings.push("Bridge not auto-started: no agent name available.");
|
|
3224
|
-
} else {
|
|
3225
|
-
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3388
|
+
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
3389
|
+
{
|
|
3226
3390
|
log(`Starting bridge: ${bridgeScript}`);
|
|
3227
3391
|
try {
|
|
3228
3392
|
bridge = await startBridge({
|
|
@@ -3232,7 +3396,7 @@ async function addCommand(args) {
|
|
|
3232
3396
|
commsDir: ctx.commsDir,
|
|
3233
3397
|
bridgeScript,
|
|
3234
3398
|
platform: ctx.platform,
|
|
3235
|
-
agentName:
|
|
3399
|
+
agentName: agentNameFlag ?? void 0,
|
|
3236
3400
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
3237
3401
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
3238
3402
|
repoRoot,
|
|
@@ -3248,7 +3412,6 @@ async function addCommand(args) {
|
|
|
3248
3412
|
}
|
|
3249
3413
|
}
|
|
3250
3414
|
}
|
|
3251
|
-
const existingAgentName = state.instances[instanceId]?.agentName ?? null;
|
|
3252
3415
|
const instanceState = {
|
|
3253
3416
|
instanceId,
|
|
3254
3417
|
runtime,
|
|
@@ -3978,7 +4141,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
3978
4141
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
3979
4142
|
}
|
|
3980
4143
|
}
|
|
3981
|
-
const updated = { ...instance, bridge };
|
|
4144
|
+
const updated = { ...instance, bridge, manageAppServer, noAuth };
|
|
3982
4145
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
3983
4146
|
saveState(repoRoot, newState);
|
|
3984
4147
|
return {
|
|
@@ -4469,6 +4632,121 @@ function bridgeStatusOne(identifier) {
|
|
|
4469
4632
|
}
|
|
4470
4633
|
};
|
|
4471
4634
|
}
|
|
4635
|
+
async function bridgeRestart(identifier, flags) {
|
|
4636
|
+
const repoRoot = findRepoRoot();
|
|
4637
|
+
const state = loadState(repoRoot);
|
|
4638
|
+
if (!state) {
|
|
4639
|
+
return {
|
|
4640
|
+
ok: false,
|
|
4641
|
+
command: "bridge",
|
|
4642
|
+
code: "TAP_NOT_INITIALIZED",
|
|
4643
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
4644
|
+
warnings: [],
|
|
4645
|
+
data: {}
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
4649
|
+
if (!resolved.ok) {
|
|
4650
|
+
return {
|
|
4651
|
+
ok: false,
|
|
4652
|
+
command: "bridge",
|
|
4653
|
+
code: resolved.code,
|
|
4654
|
+
message: resolved.message,
|
|
4655
|
+
warnings: [],
|
|
4656
|
+
data: {}
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
const instanceId = resolved.instanceId;
|
|
4660
|
+
const inst = state.instances[instanceId];
|
|
4661
|
+
if (!inst) {
|
|
4662
|
+
return {
|
|
4663
|
+
ok: false,
|
|
4664
|
+
command: "bridge",
|
|
4665
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
4666
|
+
message: `Instance not found: ${instanceId}`,
|
|
4667
|
+
warnings: [],
|
|
4668
|
+
data: {}
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
const adapter = getAdapter(inst.runtime);
|
|
4672
|
+
const ctx = {
|
|
4673
|
+
...createAdapterContext(state.commsDir, repoRoot),
|
|
4674
|
+
instanceId
|
|
4675
|
+
};
|
|
4676
|
+
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
4677
|
+
if (!bridgeScript) {
|
|
4678
|
+
return {
|
|
4679
|
+
ok: false,
|
|
4680
|
+
command: "bridge",
|
|
4681
|
+
instanceId,
|
|
4682
|
+
code: "TAP_BRIDGE_SCRIPT_MISSING",
|
|
4683
|
+
message: `Bridge script not found for ${instanceId}`,
|
|
4684
|
+
warnings: [],
|
|
4685
|
+
data: {}
|
|
4686
|
+
};
|
|
4687
|
+
}
|
|
4688
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
4689
|
+
const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
|
|
4690
|
+
const drainTimeout = parseInt(drainStr, 10) || 30;
|
|
4691
|
+
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
4692
|
+
log(`Drain timeout: ${drainTimeout}s`);
|
|
4693
|
+
try {
|
|
4694
|
+
const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
|
|
4695
|
+
const { manageAppServer, noAuth } = inferRestartMode(
|
|
4696
|
+
currentBridgeState,
|
|
4697
|
+
{
|
|
4698
|
+
noServer: flags["no-server"] === true ? true : void 0,
|
|
4699
|
+
noAuth: flags["no-auth"] === true ? true : void 0
|
|
4700
|
+
},
|
|
4701
|
+
{
|
|
4702
|
+
manageAppServer: inst.manageAppServer,
|
|
4703
|
+
noAuth: inst.noAuth
|
|
4704
|
+
}
|
|
4705
|
+
);
|
|
4706
|
+
const bridge = await restartBridge({
|
|
4707
|
+
instanceId,
|
|
4708
|
+
runtime: inst.runtime,
|
|
4709
|
+
stateDir: ctx.stateDir,
|
|
4710
|
+
commsDir: ctx.commsDir,
|
|
4711
|
+
bridgeScript,
|
|
4712
|
+
platform: ctx.platform,
|
|
4713
|
+
agentName: inst.agentName ?? void 0,
|
|
4714
|
+
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
4715
|
+
appServerUrl: resolvedConfig.appServerUrl,
|
|
4716
|
+
repoRoot,
|
|
4717
|
+
port: inst.port ?? void 0,
|
|
4718
|
+
headless: inst.headless,
|
|
4719
|
+
drainTimeoutSeconds: drainTimeout,
|
|
4720
|
+
manageAppServer,
|
|
4721
|
+
noAuth
|
|
4722
|
+
});
|
|
4723
|
+
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
4724
|
+
const updated = { ...inst, bridge, manageAppServer, noAuth };
|
|
4725
|
+
const newState = updateInstanceState(state, instanceId, updated);
|
|
4726
|
+
saveState(repoRoot, newState);
|
|
4727
|
+
return {
|
|
4728
|
+
ok: true,
|
|
4729
|
+
command: "bridge",
|
|
4730
|
+
instanceId,
|
|
4731
|
+
code: "TAP_BRIDGE_START_OK",
|
|
4732
|
+
message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
|
|
4733
|
+
warnings: [],
|
|
4734
|
+
data: { pid: bridge.pid }
|
|
4735
|
+
};
|
|
4736
|
+
} catch (err) {
|
|
4737
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4738
|
+
logError(msg);
|
|
4739
|
+
return {
|
|
4740
|
+
ok: false,
|
|
4741
|
+
command: "bridge",
|
|
4742
|
+
instanceId,
|
|
4743
|
+
code: "TAP_BRIDGE_START_FAILED",
|
|
4744
|
+
message: msg,
|
|
4745
|
+
warnings: [],
|
|
4746
|
+
data: {}
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4472
4750
|
async function bridgeCommand(args) {
|
|
4473
4751
|
const { positional, flags } = parseArgs(args);
|
|
4474
4752
|
const subcommand = positional[0];
|
|
@@ -4528,12 +4806,25 @@ async function bridgeCommand(args) {
|
|
|
4528
4806
|
}
|
|
4529
4807
|
return bridgeStatusAll();
|
|
4530
4808
|
}
|
|
4809
|
+
case "restart": {
|
|
4810
|
+
if (!identifierArg) {
|
|
4811
|
+
return {
|
|
4812
|
+
ok: false,
|
|
4813
|
+
command: "bridge",
|
|
4814
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
4815
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
|
|
4816
|
+
warnings: [],
|
|
4817
|
+
data: {}
|
|
4818
|
+
};
|
|
4819
|
+
}
|
|
4820
|
+
return bridgeRestart(identifierArg, flags);
|
|
4821
|
+
}
|
|
4531
4822
|
default:
|
|
4532
4823
|
return {
|
|
4533
4824
|
ok: false,
|
|
4534
4825
|
command: "bridge",
|
|
4535
4826
|
code: "TAP_INVALID_ARGUMENT",
|
|
4536
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
|
|
4827
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
4537
4828
|
warnings: [],
|
|
4538
4829
|
data: {}
|
|
4539
4830
|
};
|
|
@@ -4543,7 +4834,7 @@ async function bridgeCommand(args) {
|
|
|
4543
4834
|
// src/engine/dashboard.ts
|
|
4544
4835
|
import * as fs15 from "fs";
|
|
4545
4836
|
import * as path15 from "path";
|
|
4546
|
-
import { execSync as
|
|
4837
|
+
import { execSync as execSync5 } from "child_process";
|
|
4547
4838
|
function collectAgents(commsDir) {
|
|
4548
4839
|
const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
|
|
4549
4840
|
if (!fs15.existsSync(heartbeatsPath)) return [];
|
|
@@ -4622,7 +4913,7 @@ function collectBridges(repoRoot) {
|
|
|
4622
4913
|
}
|
|
4623
4914
|
function collectPRs() {
|
|
4624
4915
|
try {
|
|
4625
|
-
const output =
|
|
4916
|
+
const output = execSync5(
|
|
4626
4917
|
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
4627
4918
|
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
4628
4919
|
);
|
|
@@ -4835,7 +5126,9 @@ async function serveCommand(args) {
|
|
|
4835
5126
|
data: {}
|
|
4836
5127
|
};
|
|
4837
5128
|
}
|
|
4838
|
-
const
|
|
5129
|
+
const serveCommand2 = managed.command === "npx" ? "node" : managed.command;
|
|
5130
|
+
const serveArgs = managed.command === "npx" && managed.sourcePath ? [managed.sourcePath] : managed.args;
|
|
5131
|
+
const child = spawn2(serveCommand2, serveArgs, {
|
|
4839
5132
|
stdio: "inherit",
|
|
4840
5133
|
env: {
|
|
4841
5134
|
...process.env,
|
|
@@ -4869,7 +5162,7 @@ async function serveCommand(args) {
|
|
|
4869
5162
|
// src/commands/init-worktree.ts
|
|
4870
5163
|
import * as fs16 from "fs";
|
|
4871
5164
|
import * as path17 from "path";
|
|
4872
|
-
import { execSync as
|
|
5165
|
+
import { execSync as execSync6 } from "child_process";
|
|
4873
5166
|
var INIT_WORKTREE_HELP = `
|
|
4874
5167
|
Usage:
|
|
4875
5168
|
tap-comms init-worktree [options]
|
|
@@ -4893,7 +5186,7 @@ function warn(warnings, message) {
|
|
|
4893
5186
|
}
|
|
4894
5187
|
function run(cmd, opts) {
|
|
4895
5188
|
try {
|
|
4896
|
-
return
|
|
5189
|
+
return execSync6(cmd, {
|
|
4897
5190
|
cwd: opts?.cwd,
|
|
4898
5191
|
encoding: "utf-8",
|
|
4899
5192
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4910,7 +5203,7 @@ function toAbsolute(p) {
|
|
|
4910
5203
|
}
|
|
4911
5204
|
function probeBun(candidate) {
|
|
4912
5205
|
try {
|
|
4913
|
-
const out =
|
|
5206
|
+
const out = execSync6(`"${candidate}" --version`, {
|
|
4914
5207
|
encoding: "utf-8",
|
|
4915
5208
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4916
5209
|
timeout: 5e3
|
|
@@ -4924,7 +5217,7 @@ function findBun() {
|
|
|
4924
5217
|
const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
|
|
4925
5218
|
for (const name of candidates) {
|
|
4926
5219
|
try {
|
|
4927
|
-
const out =
|
|
5220
|
+
const out = execSync6(
|
|
4928
5221
|
process.platform === "win32" ? `where ${name}` : `which ${name}`,
|
|
4929
5222
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
|
|
4930
5223
|
).trim();
|
|
@@ -5376,11 +5669,12 @@ async function dashboardCommand(args) {
|
|
|
5376
5669
|
import {
|
|
5377
5670
|
existsSync as existsSync16,
|
|
5378
5671
|
mkdirSync as mkdirSync10,
|
|
5379
|
-
readdirSync as
|
|
5672
|
+
readdirSync as readdirSync4,
|
|
5380
5673
|
readFileSync as readFileSync14,
|
|
5381
5674
|
statSync as statSync2,
|
|
5382
5675
|
unlinkSync as unlinkSync3
|
|
5383
5676
|
} from "fs";
|
|
5677
|
+
import { execSync as execSync7 } from "child_process";
|
|
5384
5678
|
import { join as join17 } from "path";
|
|
5385
5679
|
var PASS = "pass";
|
|
5386
5680
|
var WARN = "warn";
|
|
@@ -5388,7 +5682,7 @@ var FAIL = "fail";
|
|
|
5388
5682
|
function countFiles(dir, ext = ".md") {
|
|
5389
5683
|
if (!existsSync16(dir)) return 0;
|
|
5390
5684
|
try {
|
|
5391
|
-
return
|
|
5685
|
+
return readdirSync4(dir).filter((f) => f.endsWith(ext)).length;
|
|
5392
5686
|
} catch {
|
|
5393
5687
|
return 0;
|
|
5394
5688
|
}
|
|
@@ -5398,7 +5692,7 @@ function recentFileCount(dir, withinMs) {
|
|
|
5398
5692
|
const cutoff = Date.now() - withinMs;
|
|
5399
5693
|
let count = 0;
|
|
5400
5694
|
try {
|
|
5401
|
-
for (const f of
|
|
5695
|
+
for (const f of readdirSync4(dir)) {
|
|
5402
5696
|
if (!f.endsWith(".md")) continue;
|
|
5403
5697
|
try {
|
|
5404
5698
|
if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
|
|
@@ -5409,6 +5703,21 @@ function recentFileCount(dir, withinMs) {
|
|
|
5409
5703
|
}
|
|
5410
5704
|
return count;
|
|
5411
5705
|
}
|
|
5706
|
+
function loadBridgeRuntimeHeartbeat(bridgeState) {
|
|
5707
|
+
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
5708
|
+
if (!runtimeStateDir) {
|
|
5709
|
+
return null;
|
|
5710
|
+
}
|
|
5711
|
+
const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
|
|
5712
|
+
if (!existsSync16(heartbeatPath)) {
|
|
5713
|
+
return null;
|
|
5714
|
+
}
|
|
5715
|
+
try {
|
|
5716
|
+
return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
|
|
5717
|
+
} catch {
|
|
5718
|
+
return null;
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5412
5721
|
function checkComms(commsDir) {
|
|
5413
5722
|
const checks = [];
|
|
5414
5723
|
checks.push({
|
|
@@ -5492,6 +5801,7 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
5492
5801
|
const running = isBridgeRunning(stateDir, id);
|
|
5493
5802
|
const bridgeState = loadBridgeState(stateDir, id);
|
|
5494
5803
|
const heartbeatAge = getHeartbeatAge(stateDir, id);
|
|
5804
|
+
const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
|
|
5495
5805
|
let status;
|
|
5496
5806
|
let message;
|
|
5497
5807
|
let fix;
|
|
@@ -5535,6 +5845,11 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
5535
5845
|
status = WARN;
|
|
5536
5846
|
message = "Not running";
|
|
5537
5847
|
}
|
|
5848
|
+
const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
|
|
5849
|
+
if (lastRuntimeError) {
|
|
5850
|
+
status = status === FAIL ? FAIL : WARN;
|
|
5851
|
+
message = `${message}; bridge last error: ${lastRuntimeError}`;
|
|
5852
|
+
}
|
|
5538
5853
|
checks.push({ name: `bridge: ${id}`, status, message, fix });
|
|
5539
5854
|
} else {
|
|
5540
5855
|
checks.push({
|
|
@@ -5588,35 +5903,205 @@ function checkMessageLifecycle(commsDir) {
|
|
|
5588
5903
|
function checkMcpServer(repoRoot) {
|
|
5589
5904
|
const checks = [];
|
|
5590
5905
|
const mcpJson = join17(repoRoot, ".mcp.json");
|
|
5591
|
-
if (existsSync16(mcpJson)) {
|
|
5906
|
+
if (!existsSync16(mcpJson)) {
|
|
5907
|
+
checks.push({
|
|
5908
|
+
name: "MCP config (.mcp.json)",
|
|
5909
|
+
status: WARN,
|
|
5910
|
+
message: "Not found \u2014 MCP channel notifications won't work"
|
|
5911
|
+
});
|
|
5912
|
+
return checks;
|
|
5913
|
+
}
|
|
5914
|
+
let config;
|
|
5915
|
+
try {
|
|
5916
|
+
config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
|
|
5917
|
+
} catch {
|
|
5918
|
+
checks.push({
|
|
5919
|
+
name: "MCP config (.mcp.json)",
|
|
5920
|
+
status: WARN,
|
|
5921
|
+
message: "File exists but invalid JSON"
|
|
5922
|
+
});
|
|
5923
|
+
return checks;
|
|
5924
|
+
}
|
|
5925
|
+
const hasTapComms = config?.mcpServers?.["tap-comms"];
|
|
5926
|
+
if (!hasTapComms) {
|
|
5927
|
+
checks.push({
|
|
5928
|
+
name: "MCP config (.mcp.json)",
|
|
5929
|
+
status: WARN,
|
|
5930
|
+
message: "tap-comms not configured"
|
|
5931
|
+
});
|
|
5932
|
+
return checks;
|
|
5933
|
+
}
|
|
5934
|
+
checks.push({
|
|
5935
|
+
name: "MCP config (.mcp.json)",
|
|
5936
|
+
status: PASS,
|
|
5937
|
+
message: `command: ${hasTapComms.command}`
|
|
5938
|
+
});
|
|
5939
|
+
if (hasTapComms.command) {
|
|
5940
|
+
const cmd = hasTapComms.command;
|
|
5941
|
+
let cmdAvailable = existsSync16(cmd);
|
|
5942
|
+
if (!cmdAvailable) {
|
|
5943
|
+
try {
|
|
5944
|
+
execSync7(`"${cmd}" --version`, {
|
|
5945
|
+
stdio: "pipe",
|
|
5946
|
+
timeout: 5e3
|
|
5947
|
+
});
|
|
5948
|
+
cmdAvailable = true;
|
|
5949
|
+
} catch {
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
checks.push({
|
|
5953
|
+
name: "MCP command binary",
|
|
5954
|
+
status: cmdAvailable ? PASS : FAIL,
|
|
5955
|
+
message: cmdAvailable ? cmd : `Not found: ${cmd} (checked PATH and absolute)`
|
|
5956
|
+
});
|
|
5957
|
+
}
|
|
5958
|
+
if (hasTapComms.args?.[0]) {
|
|
5959
|
+
const mcpScript = hasTapComms.args[0];
|
|
5960
|
+
checks.push({
|
|
5961
|
+
name: "MCP server script",
|
|
5962
|
+
status: existsSync16(mcpScript) ? PASS : FAIL,
|
|
5963
|
+
message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
5964
|
+
});
|
|
5965
|
+
if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
|
|
5966
|
+
checks.push({
|
|
5967
|
+
name: "MCP SQLite support",
|
|
5968
|
+
status: WARN,
|
|
5969
|
+
message: "Node + .mjs = no SQLite (bun:sqlite unavailable). Use bun or .ts source for full features."
|
|
5970
|
+
});
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
if (!hasTapComms.cwd) {
|
|
5974
|
+
checks.push({
|
|
5975
|
+
name: "MCP cwd field",
|
|
5976
|
+
status: WARN,
|
|
5977
|
+
message: "No cwd in .mcp.json \u2014 worktree sessions may fail to resolve MCP server dependencies"
|
|
5978
|
+
});
|
|
5979
|
+
} else {
|
|
5980
|
+
checks.push({
|
|
5981
|
+
name: "MCP cwd field",
|
|
5982
|
+
status: PASS,
|
|
5983
|
+
message: hasTapComms.cwd
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
const envCommsDir = hasTapComms.env?.TAP_COMMS_DIR;
|
|
5987
|
+
if (!envCommsDir) {
|
|
5988
|
+
checks.push({
|
|
5989
|
+
name: "MCP TAP_COMMS_DIR",
|
|
5990
|
+
status: FAIL,
|
|
5991
|
+
message: "TAP_COMMS_DIR not set in .mcp.json env \u2014 server will fail to start"
|
|
5992
|
+
});
|
|
5993
|
+
} else {
|
|
5994
|
+
checks.push({
|
|
5995
|
+
name: "MCP TAP_COMMS_DIR",
|
|
5996
|
+
status: existsSync16(envCommsDir) ? PASS : FAIL,
|
|
5997
|
+
message: existsSync16(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
|
|
5998
|
+
});
|
|
5999
|
+
}
|
|
6000
|
+
checks.push({
|
|
6001
|
+
name: "MCP session cache",
|
|
6002
|
+
status: PASS,
|
|
6003
|
+
message: "If .mcp.json was changed mid-session, restart Claude (Ctrl+C \u2192 claude --resume) to reload"
|
|
6004
|
+
});
|
|
6005
|
+
return checks;
|
|
6006
|
+
}
|
|
6007
|
+
function checkBridgeTurnHealth(repoRoot) {
|
|
6008
|
+
const checks = [];
|
|
6009
|
+
const tmpDir = join17(repoRoot, ".tmp");
|
|
6010
|
+
if (!existsSync16(tmpDir)) return checks;
|
|
6011
|
+
const state = loadState(repoRoot);
|
|
6012
|
+
const activeMatchers = /* @__PURE__ */ new Set();
|
|
6013
|
+
if (state) {
|
|
6014
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
6015
|
+
if (inst?.installed && inst.bridgeMode === "app-server") {
|
|
6016
|
+
activeMatchers.add(id);
|
|
6017
|
+
if (inst.agentName) activeMatchers.add(inst.agentName);
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
let dirs;
|
|
6022
|
+
try {
|
|
6023
|
+
dirs = readdirSync4(tmpDir).filter((d) => {
|
|
6024
|
+
if (!d.startsWith("codex-app-server-bridge")) return false;
|
|
6025
|
+
const suffix = d.replace("codex-app-server-bridge-", "");
|
|
6026
|
+
if (activeMatchers.size === 0) return true;
|
|
6027
|
+
for (const matcher of activeMatchers) {
|
|
6028
|
+
if (suffix === matcher || suffix.startsWith(matcher)) return true;
|
|
6029
|
+
}
|
|
6030
|
+
return false;
|
|
6031
|
+
});
|
|
6032
|
+
} catch {
|
|
6033
|
+
return checks;
|
|
6034
|
+
}
|
|
6035
|
+
for (const dir of dirs) {
|
|
6036
|
+
const heartbeatPath = join17(tmpDir, dir, "heartbeat.json");
|
|
6037
|
+
if (!existsSync16(heartbeatPath)) continue;
|
|
6038
|
+
let heartbeat;
|
|
5592
6039
|
try {
|
|
5593
|
-
|
|
5594
|
-
|
|
6040
|
+
heartbeat = JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
|
|
6041
|
+
} catch {
|
|
6042
|
+
checks.push({
|
|
6043
|
+
name: `turn: ${dir}`,
|
|
6044
|
+
status: WARN,
|
|
6045
|
+
message: "heartbeat.json unreadable"
|
|
6046
|
+
});
|
|
6047
|
+
continue;
|
|
6048
|
+
}
|
|
6049
|
+
const heartbeatAge = heartbeat.updatedAt ? Math.floor(
|
|
6050
|
+
(Date.now() - new Date(heartbeat.updatedAt).getTime()) / 1e3
|
|
6051
|
+
) : null;
|
|
6052
|
+
if (heartbeat.connected === false || heartbeat.initialized === false) {
|
|
5595
6053
|
checks.push({
|
|
5596
|
-
name:
|
|
5597
|
-
status:
|
|
5598
|
-
message:
|
|
6054
|
+
name: `turn: ${dir}`,
|
|
6055
|
+
status: FAIL,
|
|
6056
|
+
message: `disconnected (connected=${heartbeat.connected}, initialized=${heartbeat.initialized})${heartbeat.lastError ? ` \u2014 ${heartbeat.lastError}` : ""}`
|
|
5599
6057
|
});
|
|
5600
|
-
|
|
5601
|
-
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
6060
|
+
if (heartbeatAge !== null && heartbeatAge > 300) {
|
|
6061
|
+
checks.push({
|
|
6062
|
+
name: `turn: ${dir}`,
|
|
6063
|
+
status: FAIL,
|
|
6064
|
+
message: `dead \u2014 heartbeat ${Math.round(heartbeatAge)}s ago, no updates`
|
|
6065
|
+
});
|
|
6066
|
+
continue;
|
|
6067
|
+
}
|
|
6068
|
+
if (heartbeat.activeTurnId) {
|
|
6069
|
+
const ZOMBIE_THRESHOLD = 30 * 60;
|
|
6070
|
+
const lastNotifAge = heartbeat.lastNotificationAt ? Math.floor(
|
|
6071
|
+
(Date.now() - new Date(heartbeat.lastNotificationAt).getTime()) / 1e3
|
|
6072
|
+
) : null;
|
|
6073
|
+
if (lastNotifAge !== null && lastNotifAge > ZOMBIE_THRESHOLD) {
|
|
5602
6074
|
checks.push({
|
|
5603
|
-
name:
|
|
5604
|
-
status:
|
|
5605
|
-
message:
|
|
6075
|
+
name: `turn: ${dir}`,
|
|
6076
|
+
status: WARN,
|
|
6077
|
+
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}` : ""}`
|
|
5606
6078
|
});
|
|
6079
|
+
continue;
|
|
5607
6080
|
}
|
|
5608
|
-
|
|
6081
|
+
const failures2 = heartbeat.consecutiveFailureCount ?? 0;
|
|
6082
|
+
if (failures2 > 0 && heartbeatAge !== null && heartbeatAge < 60) {
|
|
6083
|
+
checks.push({
|
|
6084
|
+
name: `turn: ${dir}`,
|
|
6085
|
+
status: WARN,
|
|
6086
|
+
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}` : ""}`
|
|
6087
|
+
});
|
|
6088
|
+
continue;
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
const failures = heartbeat.consecutiveFailureCount ?? 0;
|
|
6092
|
+
if (failures > 5) {
|
|
5609
6093
|
checks.push({
|
|
5610
|
-
name:
|
|
6094
|
+
name: `turn: ${dir}`,
|
|
5611
6095
|
status: WARN,
|
|
5612
|
-
message:
|
|
6096
|
+
message: `slow \u2014 ${failures} consecutive failures, last: ${heartbeat.lastError ?? "unknown"}`
|
|
5613
6097
|
});
|
|
6098
|
+
continue;
|
|
5614
6099
|
}
|
|
5615
|
-
|
|
6100
|
+
const turnInfo = heartbeat.activeTurnId ? `active turn ${heartbeat.activeTurnId}` : `idle (last: ${heartbeat.lastTurnStatus ?? "none"})`;
|
|
5616
6101
|
checks.push({
|
|
5617
|
-
name:
|
|
5618
|
-
status:
|
|
5619
|
-
message:
|
|
6102
|
+
name: `turn: ${dir}`,
|
|
6103
|
+
status: PASS,
|
|
6104
|
+
message: `healthy \u2014 ${turnInfo}, heartbeat ${heartbeatAge ?? "?"}s ago`
|
|
5620
6105
|
});
|
|
5621
6106
|
}
|
|
5622
6107
|
return checks;
|
|
@@ -5655,10 +6140,17 @@ async function doctorCommand(args) {
|
|
|
5655
6140
|
checks.push(...checkInstances(repoRoot, config.stateDir));
|
|
5656
6141
|
checks.push(...checkMessageLifecycle(commsDir));
|
|
5657
6142
|
checks.push(...checkMcpServer(repoRoot));
|
|
6143
|
+
checks.push(...checkBridgeTurnHealth(repoRoot));
|
|
5658
6144
|
return checks;
|
|
5659
6145
|
}
|
|
5660
6146
|
const initialChecks = runAllChecks();
|
|
5661
|
-
for (const section of [
|
|
6147
|
+
for (const section of [
|
|
6148
|
+
"Comms",
|
|
6149
|
+
"Instances",
|
|
6150
|
+
"Messages",
|
|
6151
|
+
"MCP",
|
|
6152
|
+
"Turns"
|
|
6153
|
+
]) {
|
|
5662
6154
|
const sectionChecks = {
|
|
5663
6155
|
Comms: initialChecks.filter(
|
|
5664
6156
|
(c) => [
|
|
@@ -5677,7 +6169,8 @@ async function doctorCommand(args) {
|
|
|
5677
6169
|
),
|
|
5678
6170
|
MCP: initialChecks.filter(
|
|
5679
6171
|
(c) => c.name.startsWith("MCP") || c.name === "MCP server script"
|
|
5680
|
-
)
|
|
6172
|
+
),
|
|
6173
|
+
Turns: initialChecks.filter((c) => c.name.startsWith("turn:"))
|
|
5681
6174
|
}[section];
|
|
5682
6175
|
if (sectionChecks.length > 0) {
|
|
5683
6176
|
log(`${section}:`);
|
|
@@ -5737,6 +6230,158 @@ async function doctorCommand(args) {
|
|
|
5737
6230
|
};
|
|
5738
6231
|
}
|
|
5739
6232
|
|
|
6233
|
+
// src/commands/comms.ts
|
|
6234
|
+
import { execSync as execSync8 } from "child_process";
|
|
6235
|
+
import * as fs17 from "fs";
|
|
6236
|
+
import * as path18 from "path";
|
|
6237
|
+
var COMMS_HELP = `
|
|
6238
|
+
Usage:
|
|
6239
|
+
tap-comms comms <subcommand>
|
|
6240
|
+
|
|
6241
|
+
Subcommands:
|
|
6242
|
+
pull Pull latest changes from comms remote repo
|
|
6243
|
+
push Commit and push comms changes to remote repo
|
|
6244
|
+
|
|
6245
|
+
Examples:
|
|
6246
|
+
npx @hua-labs/tap comms pull
|
|
6247
|
+
npx @hua-labs/tap comms push
|
|
6248
|
+
`.trim();
|
|
6249
|
+
function isGitRepo(dir) {
|
|
6250
|
+
return fs17.existsSync(path18.join(dir, ".git"));
|
|
6251
|
+
}
|
|
6252
|
+
function commsPull(commsDir) {
|
|
6253
|
+
logHeader("tap comms pull");
|
|
6254
|
+
if (!isGitRepo(commsDir)) {
|
|
6255
|
+
logError(`${commsDir} is not a git repository`);
|
|
6256
|
+
return {
|
|
6257
|
+
ok: false,
|
|
6258
|
+
command: "comms",
|
|
6259
|
+
code: "TAP_COMMS_NOT_REPO",
|
|
6260
|
+
message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
|
|
6261
|
+
warnings: [],
|
|
6262
|
+
data: { commsDir }
|
|
6263
|
+
};
|
|
6264
|
+
}
|
|
6265
|
+
try {
|
|
6266
|
+
const output = execSync8("git pull --rebase", {
|
|
6267
|
+
cwd: commsDir,
|
|
6268
|
+
encoding: "utf-8",
|
|
6269
|
+
stdio: "pipe"
|
|
6270
|
+
});
|
|
6271
|
+
logSuccess("Comms pull complete");
|
|
6272
|
+
if (output.trim()) log(output.trim());
|
|
6273
|
+
return {
|
|
6274
|
+
ok: true,
|
|
6275
|
+
command: "comms",
|
|
6276
|
+
code: "TAP_COMMS_PULL_OK",
|
|
6277
|
+
message: "Comms pull complete",
|
|
6278
|
+
warnings: [],
|
|
6279
|
+
data: { commsDir }
|
|
6280
|
+
};
|
|
6281
|
+
} catch (err) {
|
|
6282
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6283
|
+
logError(`Pull failed: ${msg}`);
|
|
6284
|
+
return {
|
|
6285
|
+
ok: false,
|
|
6286
|
+
command: "comms",
|
|
6287
|
+
code: "TAP_COMMS_PULL_FAILED",
|
|
6288
|
+
message: `Pull failed: ${msg}`,
|
|
6289
|
+
warnings: [],
|
|
6290
|
+
data: { commsDir }
|
|
6291
|
+
};
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
function commsPush(commsDir) {
|
|
6295
|
+
logHeader("tap comms push");
|
|
6296
|
+
if (!isGitRepo(commsDir)) {
|
|
6297
|
+
logError(`${commsDir} is not a git repository`);
|
|
6298
|
+
return {
|
|
6299
|
+
ok: false,
|
|
6300
|
+
command: "comms",
|
|
6301
|
+
code: "TAP_COMMS_NOT_REPO",
|
|
6302
|
+
message: `Comms directory is not a git repo. Use 'tap init --comms-repo <url>' to set up.`,
|
|
6303
|
+
warnings: [],
|
|
6304
|
+
data: { commsDir }
|
|
6305
|
+
};
|
|
6306
|
+
}
|
|
6307
|
+
try {
|
|
6308
|
+
execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
|
|
6309
|
+
const status = execSync8("git status --porcelain", {
|
|
6310
|
+
cwd: commsDir,
|
|
6311
|
+
encoding: "utf-8",
|
|
6312
|
+
stdio: "pipe"
|
|
6313
|
+
}).trim();
|
|
6314
|
+
if (!status) {
|
|
6315
|
+
log("Nothing to push \u2014 comms directory is clean");
|
|
6316
|
+
return {
|
|
6317
|
+
ok: true,
|
|
6318
|
+
command: "comms",
|
|
6319
|
+
code: "TAP_COMMS_PUSH_OK",
|
|
6320
|
+
message: "Nothing to push",
|
|
6321
|
+
warnings: [],
|
|
6322
|
+
data: { commsDir, changed: false }
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6326
|
+
execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
|
|
6327
|
+
cwd: commsDir,
|
|
6328
|
+
stdio: "pipe"
|
|
6329
|
+
});
|
|
6330
|
+
execSync8("git push", { cwd: commsDir, stdio: "pipe" });
|
|
6331
|
+
logSuccess("Comms push complete");
|
|
6332
|
+
return {
|
|
6333
|
+
ok: true,
|
|
6334
|
+
command: "comms",
|
|
6335
|
+
code: "TAP_COMMS_PUSH_OK",
|
|
6336
|
+
message: "Comms push complete",
|
|
6337
|
+
warnings: [],
|
|
6338
|
+
data: { commsDir, changed: true }
|
|
6339
|
+
};
|
|
6340
|
+
} catch (err) {
|
|
6341
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6342
|
+
logError(`Push failed: ${msg}`);
|
|
6343
|
+
return {
|
|
6344
|
+
ok: false,
|
|
6345
|
+
command: "comms",
|
|
6346
|
+
code: "TAP_COMMS_PUSH_FAILED",
|
|
6347
|
+
message: `Push failed: ${msg}`,
|
|
6348
|
+
warnings: [],
|
|
6349
|
+
data: { commsDir }
|
|
6350
|
+
};
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
async function commsCommand(args) {
|
|
6354
|
+
const subcommand = args[0];
|
|
6355
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
6356
|
+
log(COMMS_HELP);
|
|
6357
|
+
return {
|
|
6358
|
+
ok: true,
|
|
6359
|
+
command: "comms",
|
|
6360
|
+
code: "TAP_NO_OP",
|
|
6361
|
+
message: COMMS_HELP,
|
|
6362
|
+
warnings: [],
|
|
6363
|
+
data: {}
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
const repoRoot = findRepoRoot();
|
|
6367
|
+
const commsDir = resolveCommsDir(args, repoRoot);
|
|
6368
|
+
switch (subcommand) {
|
|
6369
|
+
case "pull":
|
|
6370
|
+
return commsPull(commsDir);
|
|
6371
|
+
case "push":
|
|
6372
|
+
return commsPush(commsDir);
|
|
6373
|
+
default:
|
|
6374
|
+
return {
|
|
6375
|
+
ok: false,
|
|
6376
|
+
command: "comms",
|
|
6377
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
6378
|
+
message: `Unknown comms subcommand: ${subcommand}. Use pull or push.`,
|
|
6379
|
+
warnings: [],
|
|
6380
|
+
data: {}
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
|
|
5740
6385
|
// src/output.ts
|
|
5741
6386
|
function emitResult(result, jsonMode) {
|
|
5742
6387
|
if (jsonMode) {
|
|
@@ -5777,6 +6422,7 @@ Commands:
|
|
|
5777
6422
|
bridge <sub> [inst] Manage bridges (start, stop, status)
|
|
5778
6423
|
up Start all registered bridge daemons
|
|
5779
6424
|
down Stop all running bridge daemons
|
|
6425
|
+
comms <pull|push> Sync comms directory with remote repo
|
|
5780
6426
|
dashboard Show unified ops dashboard
|
|
5781
6427
|
doctor Diagnose tap infrastructure health
|
|
5782
6428
|
serve Start tap-comms MCP server (stdio)
|
|
@@ -5804,6 +6450,7 @@ function normalizeCommandName(command) {
|
|
|
5804
6450
|
case "bridge":
|
|
5805
6451
|
case "up":
|
|
5806
6452
|
case "down":
|
|
6453
|
+
case "comms":
|
|
5807
6454
|
case "dashboard":
|
|
5808
6455
|
case "doctor":
|
|
5809
6456
|
case "serve":
|
|
@@ -5861,6 +6508,9 @@ async function main() {
|
|
|
5861
6508
|
case "down":
|
|
5862
6509
|
result = await downCommand(commandArgs);
|
|
5863
6510
|
break;
|
|
6511
|
+
case "comms":
|
|
6512
|
+
result = await commsCommand(commandArgs);
|
|
6513
|
+
break;
|
|
5864
6514
|
case "dashboard":
|
|
5865
6515
|
result = await dashboardCommand(commandArgs);
|
|
5866
6516
|
break;
|