@hua-labs/tap 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -87
- package/dist/bridges/codex-app-server-auth-gateway.mjs +23 -4
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +5 -224
- package/dist/bridges/codex-app-server-bridge.mjs +706 -1109
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +15 -1
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/gemini-ide-companion-runner.mjs +28 -21973
- package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
- package/dist/cli.mjs +2014 -646
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +59 -1
- package/dist/index.mjs +5550 -26566
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +1235 -21479
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +6 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
14
21
|
|
|
15
22
|
// src/utils.ts
|
|
16
23
|
import * as fs from "fs";
|
|
17
24
|
import * as path from "path";
|
|
18
|
-
var VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
19
25
|
function isValidRuntime(name) {
|
|
20
26
|
return VALID_RUNTIMES.includes(name);
|
|
21
27
|
}
|
|
22
28
|
function detectPlatform() {
|
|
23
29
|
return process.platform;
|
|
24
30
|
}
|
|
25
|
-
var _noGitWarned = false;
|
|
26
|
-
var _loggedWarnings = /* @__PURE__ */ new Set();
|
|
27
31
|
function _setNoGitWarned() {
|
|
28
32
|
_noGitWarned = true;
|
|
29
33
|
}
|
|
@@ -97,7 +101,6 @@ function parseArgs(args) {
|
|
|
97
101
|
}
|
|
98
102
|
return { positional, flags };
|
|
99
103
|
}
|
|
100
|
-
var _jsonMode = false;
|
|
101
104
|
function setJsonMode(enabled) {
|
|
102
105
|
_jsonMode = enabled;
|
|
103
106
|
}
|
|
@@ -156,7 +159,17 @@ function resolveInstanceId(identifier, state) {
|
|
|
156
159
|
message: `Instance not found: ${identifier}`
|
|
157
160
|
};
|
|
158
161
|
}
|
|
162
|
+
function validateInstanceName(name) {
|
|
163
|
+
if (/[/\\]/.test(name) || name.includes("..")) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid instance name "${name}": must not contain path separators or ".." sequences`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
159
169
|
function buildInstanceId(runtime, name) {
|
|
170
|
+
if (name) {
|
|
171
|
+
validateInstanceName(name);
|
|
172
|
+
}
|
|
160
173
|
return name ? `${runtime}-${name}` : runtime;
|
|
161
174
|
}
|
|
162
175
|
function findPortConflict(state, port, excludeInstanceId) {
|
|
@@ -165,13 +178,21 @@ function findPortConflict(state, port, excludeInstanceId) {
|
|
|
165
178
|
}
|
|
166
179
|
return null;
|
|
167
180
|
}
|
|
181
|
+
var VALID_RUNTIMES, _noGitWarned, _loggedWarnings, _jsonMode;
|
|
182
|
+
var init_utils = __esm({
|
|
183
|
+
"src/utils.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
init_config();
|
|
186
|
+
VALID_RUNTIMES = ["claude", "codex", "gemini"];
|
|
187
|
+
_noGitWarned = false;
|
|
188
|
+
_loggedWarnings = /* @__PURE__ */ new Set();
|
|
189
|
+
_jsonMode = false;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
168
192
|
|
|
169
193
|
// src/config/resolve.ts
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
var LEGACY_CONFIG_FILE = ".tap-config";
|
|
173
|
-
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
174
|
-
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
194
|
+
import * as fs2 from "fs";
|
|
195
|
+
import * as path2 from "path";
|
|
175
196
|
function findRepoRoot2(startDir = process.cwd()) {
|
|
176
197
|
let dir = path2.resolve(startDir);
|
|
177
198
|
while (true) {
|
|
@@ -344,19 +365,430 @@ function normalizeTapPath(input) {
|
|
|
344
365
|
}
|
|
345
366
|
return trimmed;
|
|
346
367
|
}
|
|
368
|
+
var SHARED_CONFIG_FILE, LOCAL_CONFIG_FILE, LEGACY_CONFIG_FILE, DEFAULT_RUNTIME_COMMAND, DEFAULT_APP_SERVER_URL;
|
|
369
|
+
var init_resolve = __esm({
|
|
370
|
+
"src/config/resolve.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
init_utils();
|
|
373
|
+
SHARED_CONFIG_FILE = "tap-config.json";
|
|
374
|
+
LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
375
|
+
LEGACY_CONFIG_FILE = ".tap-config";
|
|
376
|
+
DEFAULT_RUNTIME_COMMAND = "node";
|
|
377
|
+
DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// src/permissions/presets.ts
|
|
382
|
+
function createPermissionFromRole(role) {
|
|
383
|
+
const preset = ROLE_PRESETS[role];
|
|
384
|
+
return {
|
|
385
|
+
...preset,
|
|
386
|
+
allowedTools: [...preset.allowedTools],
|
|
387
|
+
deniedTools: [...preset.deniedTools],
|
|
388
|
+
allowedPaths: [...preset.allowedPaths]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
var ROLE_PRESETS;
|
|
392
|
+
var init_presets = __esm({
|
|
393
|
+
"src/permissions/presets.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
ROLE_PRESETS = {
|
|
396
|
+
tower: {
|
|
397
|
+
role: "tower",
|
|
398
|
+
mode: "full-access",
|
|
399
|
+
allowedTools: ["*"],
|
|
400
|
+
deniedTools: [],
|
|
401
|
+
allowedPaths: ["**"],
|
|
402
|
+
escalateTo: null
|
|
403
|
+
},
|
|
404
|
+
implementer: {
|
|
405
|
+
role: "implementer",
|
|
406
|
+
mode: "workspace-write",
|
|
407
|
+
allowedTools: [
|
|
408
|
+
"Read",
|
|
409
|
+
"Edit",
|
|
410
|
+
"Write",
|
|
411
|
+
"Bash",
|
|
412
|
+
"Grep",
|
|
413
|
+
"Glob",
|
|
414
|
+
"mcp__tap__*"
|
|
415
|
+
],
|
|
416
|
+
deniedTools: ["Bash(git push --force:*)", "Bash(git reset --hard:*)"],
|
|
417
|
+
allowedPaths: ["packages/**", "apps/**", "docs/**"],
|
|
418
|
+
escalateTo: "tower"
|
|
419
|
+
},
|
|
420
|
+
reviewer: {
|
|
421
|
+
role: "reviewer",
|
|
422
|
+
mode: "readonly",
|
|
423
|
+
allowedTools: [
|
|
424
|
+
"Read",
|
|
425
|
+
"Grep",
|
|
426
|
+
"Glob",
|
|
427
|
+
"Bash(grep:*)",
|
|
428
|
+
"Bash(git diff:*)",
|
|
429
|
+
"mcp__tap__*"
|
|
430
|
+
],
|
|
431
|
+
deniedTools: ["Edit", "Write", "Bash(rm:*)"],
|
|
432
|
+
allowedPaths: ["hua-comms/reviews/**"],
|
|
433
|
+
escalateTo: "tower"
|
|
434
|
+
},
|
|
435
|
+
custom: {
|
|
436
|
+
role: "custom",
|
|
437
|
+
mode: "prompt",
|
|
438
|
+
allowedTools: [],
|
|
439
|
+
deniedTools: [],
|
|
440
|
+
allowedPaths: [],
|
|
441
|
+
escalateTo: "tower"
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// src/config/instance-config.ts
|
|
448
|
+
var instance_config_exports = {};
|
|
449
|
+
__export(instance_config_exports, {
|
|
450
|
+
createInstanceConfig: () => createInstanceConfig,
|
|
451
|
+
deleteInstanceConfig: () => deleteInstanceConfig,
|
|
452
|
+
listInstanceConfigs: () => listInstanceConfigs,
|
|
453
|
+
loadInstanceConfig: () => loadInstanceConfig,
|
|
454
|
+
saveInstanceConfig: () => saveInstanceConfig,
|
|
455
|
+
updateInstanceConfig: () => updateInstanceConfig
|
|
456
|
+
});
|
|
457
|
+
import * as fs3 from "fs";
|
|
458
|
+
import * as path3 from "path";
|
|
459
|
+
function instancesDir(stateDir) {
|
|
460
|
+
return path3.join(stateDir, "instances");
|
|
461
|
+
}
|
|
462
|
+
function instanceConfigPath(stateDir, instanceId) {
|
|
463
|
+
if (instanceId.includes("/") || instanceId.includes("\\") || instanceId.includes("..")) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Invalid instanceId "${instanceId}": must not contain path separators or ".." sequences`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return path3.join(instancesDir(stateDir), `${instanceId}.json`);
|
|
469
|
+
}
|
|
470
|
+
function loadInstanceConfig(stateDir, instanceId) {
|
|
471
|
+
const filePath = instanceConfigPath(stateDir, instanceId);
|
|
472
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
473
|
+
try {
|
|
474
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
475
|
+
const parsed = JSON.parse(raw);
|
|
476
|
+
if (!parsed.permission) {
|
|
477
|
+
parsed.permission = createPermissionFromRole("custom");
|
|
478
|
+
}
|
|
479
|
+
return parsed;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function saveInstanceConfig(stateDir, config) {
|
|
485
|
+
const dir = instancesDir(stateDir);
|
|
486
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
487
|
+
const filePath = instanceConfigPath(stateDir, config.instanceId);
|
|
488
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
489
|
+
fs3.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
490
|
+
fs3.renameSync(tmp, filePath);
|
|
491
|
+
return filePath;
|
|
492
|
+
}
|
|
493
|
+
function listInstanceConfigs(stateDir) {
|
|
494
|
+
const dir = instancesDir(stateDir);
|
|
495
|
+
if (!fs3.existsSync(dir)) return [];
|
|
496
|
+
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
497
|
+
const configs = [];
|
|
498
|
+
for (const file of files) {
|
|
499
|
+
try {
|
|
500
|
+
const raw = fs3.readFileSync(path3.join(dir, file), "utf-8");
|
|
501
|
+
configs.push(JSON.parse(raw));
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return configs;
|
|
506
|
+
}
|
|
507
|
+
function deleteInstanceConfig(stateDir, instanceId) {
|
|
508
|
+
const filePath = instanceConfigPath(stateDir, instanceId);
|
|
509
|
+
if (!fs3.existsSync(filePath)) return false;
|
|
510
|
+
fs3.unlinkSync(filePath);
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
function createInstanceConfig(opts) {
|
|
514
|
+
const parts = opts.instanceId.split("-");
|
|
515
|
+
if (parts.length > 1) {
|
|
516
|
+
validateInstanceName(parts.slice(1).join("-"));
|
|
517
|
+
}
|
|
518
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
519
|
+
const config = {
|
|
520
|
+
schemaVersion: INSTANCE_CONFIG_SCHEMA_VERSION,
|
|
521
|
+
instanceId: opts.instanceId,
|
|
522
|
+
runtime: opts.runtime,
|
|
523
|
+
agentName: opts.agentName,
|
|
524
|
+
agentId: opts.agentId,
|
|
525
|
+
port: opts.port,
|
|
526
|
+
appServerUrl: opts.appServerUrl,
|
|
527
|
+
permission: createPermissionFromRole(opts.role ?? "custom"),
|
|
528
|
+
// Top-level overrides consumed by resolveTrackedConfig
|
|
529
|
+
commsDir: opts.commsDir,
|
|
530
|
+
stateDir: opts.stateDir,
|
|
531
|
+
mcpEnv: {
|
|
532
|
+
TAP_COMMS_DIR: opts.commsDir,
|
|
533
|
+
TAP_STATE_DIR: opts.stateDir,
|
|
534
|
+
TAP_REPO_ROOT: opts.repoRoot,
|
|
535
|
+
TAP_AGENT_NAME: opts.agentName ?? "<set-per-session>"
|
|
536
|
+
},
|
|
537
|
+
configHash: "",
|
|
538
|
+
lastSyncedToRuntime: null,
|
|
539
|
+
runtimeConfigHash: "",
|
|
540
|
+
createdAt: now,
|
|
541
|
+
updatedAt: now
|
|
542
|
+
};
|
|
543
|
+
config.configHash = computeInstanceConfigHash(config);
|
|
544
|
+
return config;
|
|
545
|
+
}
|
|
546
|
+
function updateInstanceConfig(existing, updates) {
|
|
547
|
+
const updated = {
|
|
548
|
+
...existing,
|
|
549
|
+
...updates,
|
|
550
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
551
|
+
};
|
|
552
|
+
if (updates.agentName !== void 0) {
|
|
553
|
+
updated.mcpEnv = {
|
|
554
|
+
...updated.mcpEnv,
|
|
555
|
+
TAP_AGENT_NAME: updates.agentName ?? "<set-per-session>"
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
updated.configHash = computeInstanceConfigHash(updated);
|
|
559
|
+
return updated;
|
|
560
|
+
}
|
|
561
|
+
function computeInstanceConfigHash(config) {
|
|
562
|
+
const hashInput = {
|
|
563
|
+
instanceId: config.instanceId,
|
|
564
|
+
runtime: config.runtime,
|
|
565
|
+
agentName: config.agentName,
|
|
566
|
+
agentId: config.agentId,
|
|
567
|
+
port: config.port,
|
|
568
|
+
appServerUrl: config.appServerUrl,
|
|
569
|
+
mcpEnv: config.mcpEnv,
|
|
570
|
+
permission: config.permission
|
|
571
|
+
};
|
|
572
|
+
const serialized = JSON.stringify(hashInput, Object.keys(hashInput).sort());
|
|
573
|
+
let hash = 2166136261;
|
|
574
|
+
for (let i = 0; i < serialized.length; i++) {
|
|
575
|
+
hash ^= serialized.charCodeAt(i);
|
|
576
|
+
hash = Math.imul(hash, 16777619);
|
|
577
|
+
}
|
|
578
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
579
|
+
}
|
|
580
|
+
var INSTANCE_CONFIG_SCHEMA_VERSION;
|
|
581
|
+
var init_instance_config = __esm({
|
|
582
|
+
"src/config/instance-config.ts"() {
|
|
583
|
+
"use strict";
|
|
584
|
+
init_utils();
|
|
585
|
+
init_presets();
|
|
586
|
+
INSTANCE_CONFIG_SCHEMA_VERSION = 1;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// src/config/drift-detector.ts
|
|
591
|
+
var drift_detector_exports = {};
|
|
592
|
+
__export(drift_detector_exports, {
|
|
593
|
+
checkAllDrift: () => checkAllDrift,
|
|
594
|
+
checkInstanceDrift: () => checkInstanceDrift,
|
|
595
|
+
computeFileHash: () => computeFileHash
|
|
596
|
+
});
|
|
597
|
+
import * as fs4 from "fs";
|
|
598
|
+
import * as crypto from "crypto";
|
|
599
|
+
function computeFileHash(filePath) {
|
|
600
|
+
if (!fs4.existsSync(filePath)) return "";
|
|
601
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
602
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
603
|
+
}
|
|
604
|
+
function checkInstanceDrift(stateDir, instanceId, state) {
|
|
605
|
+
const checks = [];
|
|
606
|
+
const instConfig = loadInstanceConfig(stateDir, instanceId);
|
|
607
|
+
const stateInstance = state?.instances[instanceId] ?? null;
|
|
608
|
+
if (!instConfig) {
|
|
609
|
+
if (stateInstance?.installed) {
|
|
610
|
+
if (!stateInstance.configSourceFile) {
|
|
611
|
+
return { instanceId, status: "ok", checks };
|
|
612
|
+
}
|
|
613
|
+
checks.push({
|
|
614
|
+
name: "instance config exists",
|
|
615
|
+
source: "instance-config",
|
|
616
|
+
target: "state-json",
|
|
617
|
+
status: "missing",
|
|
618
|
+
details: `Instance "${instanceId}" is in state.json but has no instance config file. Run "tap add ${instanceId} --force" to recreate.`,
|
|
619
|
+
autoFixable: false
|
|
620
|
+
// Cannot generate config from state alone
|
|
621
|
+
});
|
|
622
|
+
return { instanceId, status: "missing", checks };
|
|
623
|
+
}
|
|
624
|
+
return { instanceId, status: "ok", checks };
|
|
625
|
+
}
|
|
626
|
+
if (!stateInstance) {
|
|
627
|
+
checks.push({
|
|
628
|
+
name: "instance registered",
|
|
629
|
+
source: "instance-config",
|
|
630
|
+
target: "state-json",
|
|
631
|
+
status: "missing",
|
|
632
|
+
details: `Instance config exists for "${instanceId}" but not registered in state.json`,
|
|
633
|
+
autoFixable: false
|
|
634
|
+
});
|
|
635
|
+
return { instanceId, status: "orphaned", checks };
|
|
636
|
+
}
|
|
637
|
+
const fieldMismatches = [];
|
|
638
|
+
if (instConfig.agentName !== stateInstance.agentName) {
|
|
639
|
+
fieldMismatches.push(
|
|
640
|
+
`agentName: instance="${instConfig.agentName}" vs state="${stateInstance.agentName}"`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (instConfig.port !== stateInstance.port) {
|
|
644
|
+
fieldMismatches.push(
|
|
645
|
+
`port: instance=${instConfig.port} vs state=${stateInstance.port}`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (fieldMismatches.length > 0) {
|
|
649
|
+
checks.push({
|
|
650
|
+
name: "state consistency",
|
|
651
|
+
source: "instance-config",
|
|
652
|
+
target: "state-json",
|
|
653
|
+
status: "drifted",
|
|
654
|
+
details: fieldMismatches.join("; "),
|
|
655
|
+
autoFixable: true
|
|
656
|
+
});
|
|
657
|
+
} else {
|
|
658
|
+
checks.push({
|
|
659
|
+
name: "state consistency",
|
|
660
|
+
source: "instance-config",
|
|
661
|
+
target: "state-json",
|
|
662
|
+
status: "ok",
|
|
663
|
+
details: null,
|
|
664
|
+
autoFixable: false
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const stateHash = stateInstance.configHash ?? "";
|
|
668
|
+
if (!stateHash) {
|
|
669
|
+
checks.push({
|
|
670
|
+
name: "config hash baseline",
|
|
671
|
+
source: "instance-config",
|
|
672
|
+
target: "state-json",
|
|
673
|
+
status: "drifted",
|
|
674
|
+
details: `configHash not baselined for "${instanceId}" \u2014 needs backfill`,
|
|
675
|
+
autoFixable: true
|
|
676
|
+
});
|
|
677
|
+
} else if (instConfig.configHash !== stateHash) {
|
|
678
|
+
checks.push({
|
|
679
|
+
name: "config hash",
|
|
680
|
+
source: "instance-config",
|
|
681
|
+
target: "state-json",
|
|
682
|
+
status: "drifted",
|
|
683
|
+
details: `instance hash="${instConfig.configHash}" vs state hash="${stateHash}"`,
|
|
684
|
+
autoFixable: true
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
checks.push({
|
|
688
|
+
name: "config hash",
|
|
689
|
+
source: "instance-config",
|
|
690
|
+
target: "state-json",
|
|
691
|
+
status: "ok",
|
|
692
|
+
details: null,
|
|
693
|
+
autoFixable: false
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (stateInstance.configPath && fs4.existsSync(stateInstance.configPath)) {
|
|
697
|
+
const currentRuntimeHash = computeFileHash(stateInstance.configPath);
|
|
698
|
+
const lastSyncedHash = instConfig.runtimeConfigHash || "";
|
|
699
|
+
if (!lastSyncedHash) {
|
|
700
|
+
checks.push({
|
|
701
|
+
name: "runtime config baseline",
|
|
702
|
+
source: "instance-config",
|
|
703
|
+
target: "runtime-config",
|
|
704
|
+
status: "drifted",
|
|
705
|
+
details: `runtimeConfigHash not baselined for "${instanceId}" \u2014 needs backfill`,
|
|
706
|
+
autoFixable: true
|
|
707
|
+
});
|
|
708
|
+
} else if (currentRuntimeHash !== lastSyncedHash) {
|
|
709
|
+
checks.push({
|
|
710
|
+
name: "runtime config",
|
|
711
|
+
source: "instance-config",
|
|
712
|
+
target: "runtime-config",
|
|
713
|
+
status: "drifted",
|
|
714
|
+
details: `${stateInstance.configPath} has changed since last sync (hash: ${currentRuntimeHash.slice(0, 8)} vs synced: ${lastSyncedHash.slice(0, 8)})`,
|
|
715
|
+
autoFixable: true
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
718
|
+
checks.push({
|
|
719
|
+
name: "runtime config",
|
|
720
|
+
source: "instance-config",
|
|
721
|
+
target: "runtime-config",
|
|
722
|
+
status: "ok",
|
|
723
|
+
details: null,
|
|
724
|
+
autoFixable: false
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const hasDrift = checks.some((c) => c.status !== "ok");
|
|
729
|
+
return {
|
|
730
|
+
instanceId,
|
|
731
|
+
status: hasDrift ? "drifted" : "ok",
|
|
732
|
+
checks
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function checkAllDrift(stateDir, state) {
|
|
736
|
+
const results = [];
|
|
737
|
+
const checkedIds = /* @__PURE__ */ new Set();
|
|
738
|
+
if (state) {
|
|
739
|
+
for (const instanceId of Object.keys(state.instances)) {
|
|
740
|
+
checkedIds.add(instanceId);
|
|
741
|
+
results.push(checkInstanceDrift(stateDir, instanceId, state));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const instancesDir2 = `${stateDir}/instances`;
|
|
745
|
+
if (fs4.existsSync(instancesDir2)) {
|
|
746
|
+
for (const file of fs4.readdirSync(instancesDir2)) {
|
|
747
|
+
if (!file.endsWith(".json")) continue;
|
|
748
|
+
const id = file.replace(/\.json$/, "");
|
|
749
|
+
if (!checkedIds.has(id)) {
|
|
750
|
+
results.push(checkInstanceDrift(stateDir, id, state));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return results;
|
|
755
|
+
}
|
|
756
|
+
var init_drift_detector = __esm({
|
|
757
|
+
"src/config/drift-detector.ts"() {
|
|
758
|
+
"use strict";
|
|
759
|
+
init_instance_config();
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// src/config/index.ts
|
|
764
|
+
var init_config = __esm({
|
|
765
|
+
"src/config/index.ts"() {
|
|
766
|
+
"use strict";
|
|
767
|
+
init_resolve();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// src/commands/init.ts
|
|
772
|
+
import * as fs8 from "fs";
|
|
773
|
+
import * as path7 from "path";
|
|
774
|
+
import { spawnSync } from "child_process";
|
|
347
775
|
|
|
348
776
|
// src/state.ts
|
|
777
|
+
init_config();
|
|
778
|
+
import * as fs5 from "fs";
|
|
779
|
+
import * as path4 from "path";
|
|
780
|
+
import * as crypto2 from "crypto";
|
|
349
781
|
var STATE_FILE = "state.json";
|
|
350
|
-
var SCHEMA_VERSION =
|
|
782
|
+
var SCHEMA_VERSION = 3;
|
|
351
783
|
function getStateDir(repoRoot) {
|
|
352
784
|
const { config } = resolveConfig({}, repoRoot);
|
|
353
785
|
return config.stateDir;
|
|
354
786
|
}
|
|
355
787
|
function getStatePath(repoRoot) {
|
|
356
|
-
return
|
|
788
|
+
return path4.join(getStateDir(repoRoot), STATE_FILE);
|
|
357
789
|
}
|
|
358
790
|
function stateExists(repoRoot) {
|
|
359
|
-
return
|
|
791
|
+
return fs5.existsSync(getStatePath(repoRoot));
|
|
360
792
|
}
|
|
361
793
|
function migrateStateV1toV2(v1) {
|
|
362
794
|
const instances = {};
|
|
@@ -382,25 +814,46 @@ function migrateStateV1toV2(v1) {
|
|
|
382
814
|
instances
|
|
383
815
|
};
|
|
384
816
|
}
|
|
817
|
+
function migrateStateV2toV3(v2) {
|
|
818
|
+
const instances = {};
|
|
819
|
+
for (const [id, inst] of Object.entries(v2.instances)) {
|
|
820
|
+
instances[id] = {
|
|
821
|
+
...inst,
|
|
822
|
+
configHash: inst.configHash ?? "",
|
|
823
|
+
configSourceFile: inst.configSourceFile ?? ""
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
...v2,
|
|
828
|
+
schemaVersion: SCHEMA_VERSION,
|
|
829
|
+
instances
|
|
830
|
+
};
|
|
831
|
+
}
|
|
385
832
|
function loadState(repoRoot) {
|
|
386
833
|
const statePath = getStatePath(repoRoot);
|
|
387
|
-
if (!
|
|
388
|
-
const raw =
|
|
834
|
+
if (!fs5.existsSync(statePath)) return null;
|
|
835
|
+
const raw = fs5.readFileSync(statePath, "utf-8");
|
|
389
836
|
const parsed = JSON.parse(raw);
|
|
390
837
|
if (parsed.schemaVersion === 1 || parsed.runtimes) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
838
|
+
const v2 = migrateStateV1toV2(parsed);
|
|
839
|
+
const v3 = migrateStateV2toV3(v2);
|
|
840
|
+
saveState(repoRoot, v3);
|
|
841
|
+
return v3;
|
|
842
|
+
}
|
|
843
|
+
if (parsed.schemaVersion === 2) {
|
|
844
|
+
const v3 = migrateStateV2toV3(parsed);
|
|
845
|
+
saveState(repoRoot, v3);
|
|
846
|
+
return v3;
|
|
394
847
|
}
|
|
395
848
|
return parsed;
|
|
396
849
|
}
|
|
397
850
|
function saveState(repoRoot, state) {
|
|
398
851
|
const stateDir = getStateDir(repoRoot);
|
|
399
|
-
|
|
852
|
+
fs5.mkdirSync(stateDir, { recursive: true });
|
|
400
853
|
const statePath = getStatePath(repoRoot);
|
|
401
854
|
const tmp = `${statePath}.tmp.${process.pid}`;
|
|
402
|
-
|
|
403
|
-
|
|
855
|
+
fs5.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
856
|
+
fs5.renameSync(tmp, statePath);
|
|
404
857
|
}
|
|
405
858
|
function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
406
859
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -408,8 +861,8 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
|
408
861
|
schemaVersion: SCHEMA_VERSION,
|
|
409
862
|
createdAt: now,
|
|
410
863
|
updatedAt: now,
|
|
411
|
-
commsDir:
|
|
412
|
-
repoRoot:
|
|
864
|
+
commsDir: path4.resolve(commsDir),
|
|
865
|
+
repoRoot: path4.resolve(repoRoot),
|
|
413
866
|
packageVersion,
|
|
414
867
|
instances: {}
|
|
415
868
|
};
|
|
@@ -438,33 +891,36 @@ function getInstalledInstances(state) {
|
|
|
438
891
|
);
|
|
439
892
|
}
|
|
440
893
|
function ensureBackupDir(stateDir, instanceId) {
|
|
441
|
-
const backupDir =
|
|
442
|
-
|
|
894
|
+
const backupDir = path4.join(stateDir, "backups", instanceId);
|
|
895
|
+
fs5.mkdirSync(backupDir, { recursive: true });
|
|
443
896
|
return backupDir;
|
|
444
897
|
}
|
|
445
898
|
function backupFile(filePath, backupDir) {
|
|
446
|
-
const basename3 =
|
|
899
|
+
const basename3 = path4.basename(filePath);
|
|
447
900
|
const hash = fileHash(filePath);
|
|
448
|
-
const backupPath =
|
|
449
|
-
|
|
901
|
+
const backupPath = path4.join(backupDir, `${basename3}.${hash}.bak`);
|
|
902
|
+
fs5.copyFileSync(filePath, backupPath);
|
|
450
903
|
return backupPath;
|
|
451
904
|
}
|
|
452
905
|
function fileHash(filePath) {
|
|
453
|
-
if (!
|
|
454
|
-
const content =
|
|
455
|
-
return
|
|
906
|
+
if (!fs5.existsSync(filePath)) return "";
|
|
907
|
+
const content = fs5.readFileSync(filePath);
|
|
908
|
+
return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
456
909
|
}
|
|
457
910
|
|
|
911
|
+
// src/commands/init.ts
|
|
912
|
+
init_utils();
|
|
913
|
+
|
|
458
914
|
// src/version.ts
|
|
459
|
-
import * as
|
|
460
|
-
import * as
|
|
915
|
+
import * as fs6 from "fs";
|
|
916
|
+
import * as path5 from "path";
|
|
461
917
|
import { fileURLToPath } from "url";
|
|
462
918
|
var FALLBACK_VERSION = "0.0.0";
|
|
463
919
|
function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
464
|
-
const moduleDir =
|
|
465
|
-
const packageJsonPath =
|
|
920
|
+
const moduleDir = path5.dirname(fileURLToPath(metaUrl));
|
|
921
|
+
const packageJsonPath = path5.join(moduleDir, "..", "package.json");
|
|
466
922
|
try {
|
|
467
|
-
const parsed = JSON.parse(
|
|
923
|
+
const parsed = JSON.parse(fs6.readFileSync(packageJsonPath, "utf-8"));
|
|
468
924
|
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
469
925
|
return parsed.version;
|
|
470
926
|
}
|
|
@@ -475,8 +931,9 @@ function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
|
475
931
|
var version = resolvePackageVersion();
|
|
476
932
|
|
|
477
933
|
// src/permissions.ts
|
|
478
|
-
|
|
479
|
-
import * as
|
|
934
|
+
init_utils();
|
|
935
|
+
import * as fs7 from "fs";
|
|
936
|
+
import * as path6 from "path";
|
|
480
937
|
import * as os from "os";
|
|
481
938
|
|
|
482
939
|
// src/toml.ts
|
|
@@ -606,13 +1063,13 @@ var CLAUDE_DENY_RULES = [
|
|
|
606
1063
|
];
|
|
607
1064
|
function applyClaudePermissions(repoRoot, mode) {
|
|
608
1065
|
const warnings = [];
|
|
609
|
-
const claudeDir =
|
|
610
|
-
const settingsPath =
|
|
611
|
-
|
|
1066
|
+
const claudeDir = path6.join(repoRoot, ".claude");
|
|
1067
|
+
const settingsPath = path6.join(claudeDir, "settings.local.json");
|
|
1068
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
612
1069
|
let settings = {};
|
|
613
|
-
if (
|
|
1070
|
+
if (fs7.existsSync(settingsPath)) {
|
|
614
1071
|
try {
|
|
615
|
-
settings = JSON.parse(
|
|
1072
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
616
1073
|
} catch {
|
|
617
1074
|
warnings.push(
|
|
618
1075
|
".claude/settings.local.json was invalid JSON. Starting fresh."
|
|
@@ -626,8 +1083,8 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
626
1083
|
const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
|
|
627
1084
|
settings.deny = cleaned;
|
|
628
1085
|
const tmp2 = `${settingsPath}.tmp.${process.pid}`;
|
|
629
|
-
|
|
630
|
-
|
|
1086
|
+
fs7.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1087
|
+
fs7.renameSync(tmp2, settingsPath);
|
|
631
1088
|
logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
|
|
632
1089
|
warnings.push("Full permission mode: tap deny rules removed.");
|
|
633
1090
|
return { applied: true, warnings };
|
|
@@ -635,18 +1092,18 @@ function applyClaudePermissions(repoRoot, mode) {
|
|
|
635
1092
|
const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
|
|
636
1093
|
settings.deny = newDeny;
|
|
637
1094
|
const tmp = `${settingsPath}.tmp.${process.pid}`;
|
|
638
|
-
|
|
639
|
-
|
|
1095
|
+
fs7.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1096
|
+
fs7.renameSync(tmp, settingsPath);
|
|
640
1097
|
logSuccess(
|
|
641
1098
|
`Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
|
|
642
1099
|
);
|
|
643
1100
|
return { applied: true, warnings };
|
|
644
1101
|
}
|
|
645
1102
|
function findCodexConfigPath() {
|
|
646
|
-
return
|
|
1103
|
+
return path6.join(os.homedir(), ".codex", "config.toml");
|
|
647
1104
|
}
|
|
648
1105
|
function canonicalizeTrustPath(targetPath) {
|
|
649
|
-
let resolved =
|
|
1106
|
+
let resolved = path6.resolve(targetPath).replace(/\//g, "\\");
|
|
650
1107
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
651
1108
|
if (!driveRoot.test(resolved)) {
|
|
652
1109
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -656,10 +1113,10 @@ function canonicalizeTrustPath(targetPath) {
|
|
|
656
1113
|
function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
657
1114
|
const warnings = [];
|
|
658
1115
|
const configPath = findCodexConfigPath();
|
|
659
|
-
|
|
1116
|
+
fs7.mkdirSync(path6.dirname(configPath), { recursive: true });
|
|
660
1117
|
let content = "";
|
|
661
|
-
if (
|
|
662
|
-
content =
|
|
1118
|
+
if (fs7.existsSync(configPath)) {
|
|
1119
|
+
content = fs7.readFileSync(configPath, "utf-8");
|
|
663
1120
|
}
|
|
664
1121
|
const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
|
|
665
1122
|
if (mode === "full") {
|
|
@@ -721,8 +1178,8 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
721
1178
|
);
|
|
722
1179
|
}
|
|
723
1180
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
724
|
-
|
|
725
|
-
|
|
1181
|
+
fs7.writeFileSync(tmp, content, "utf-8");
|
|
1182
|
+
fs7.renameSync(tmp, configPath);
|
|
726
1183
|
const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
|
|
727
1184
|
logSuccess(
|
|
728
1185
|
`Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
|
|
@@ -731,12 +1188,12 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
|
|
|
731
1188
|
}
|
|
732
1189
|
function getCodexWritableRoots(repoRoot, commsDir) {
|
|
733
1190
|
const roots = [repoRoot, commsDir];
|
|
734
|
-
const parent =
|
|
1191
|
+
const parent = path6.dirname(repoRoot);
|
|
735
1192
|
for (let i = 1; i <= 4; i++) {
|
|
736
|
-
const wtPath =
|
|
737
|
-
if (
|
|
1193
|
+
const wtPath = path6.join(parent, `hua-wt-${i}`);
|
|
1194
|
+
if (fs7.existsSync(wtPath)) roots.push(wtPath);
|
|
738
1195
|
}
|
|
739
|
-
return [...new Set(roots.map((r) =>
|
|
1196
|
+
return [...new Set(roots.map((r) => path6.resolve(r)))];
|
|
740
1197
|
}
|
|
741
1198
|
function buildPermissionSummary(mode, repoRoot, commsDir) {
|
|
742
1199
|
const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
|
|
@@ -756,6 +1213,7 @@ function buildPermissionSummary(mode, repoRoot, commsDir) {
|
|
|
756
1213
|
}
|
|
757
1214
|
|
|
758
1215
|
// src/commands/init.ts
|
|
1216
|
+
init_config();
|
|
759
1217
|
var COMMS_DIRS = [
|
|
760
1218
|
"inbox",
|
|
761
1219
|
"reviews",
|
|
@@ -823,9 +1281,9 @@ async function initCommand(args) {
|
|
|
823
1281
|
const commsRepoIdx = args.indexOf("--comms-repo");
|
|
824
1282
|
const commsRepoUrl = commsRepoIdx !== -1 && args[commsRepoIdx + 1] ? args[commsRepoIdx + 1] : void 0;
|
|
825
1283
|
if (commsRepoUrl) {
|
|
826
|
-
if (
|
|
827
|
-
const gitDir =
|
|
828
|
-
if (
|
|
1284
|
+
if (fs8.existsSync(commsDir) && fs8.readdirSync(commsDir).length > 0) {
|
|
1285
|
+
const gitDir = path7.join(commsDir, ".git");
|
|
1286
|
+
if (fs8.existsSync(gitDir)) {
|
|
829
1287
|
log(`Comms directory exists: ${commsDir}`);
|
|
830
1288
|
logSuccess("Comms directory is already a git repo \u2014 linking only");
|
|
831
1289
|
} else {
|
|
@@ -877,7 +1335,7 @@ async function initCommand(args) {
|
|
|
877
1335
|
sharedConfig.commsRepoUrl = commsRepoUrl;
|
|
878
1336
|
configChanged = true;
|
|
879
1337
|
}
|
|
880
|
-
const commsDirRelative =
|
|
1338
|
+
const commsDirRelative = path7.relative(repoRoot, commsDir);
|
|
881
1339
|
if (commsDirRelative && commsDirRelative !== "tap-comms") {
|
|
882
1340
|
sharedConfig.commsDir = commsDirRelative;
|
|
883
1341
|
configChanged = true;
|
|
@@ -889,13 +1347,13 @@ async function initCommand(args) {
|
|
|
889
1347
|
}
|
|
890
1348
|
log(`Comms directory: ${commsDir}`);
|
|
891
1349
|
for (const dir of COMMS_DIRS) {
|
|
892
|
-
const dirPath =
|
|
893
|
-
|
|
1350
|
+
const dirPath = path7.join(commsDir, dir);
|
|
1351
|
+
fs8.mkdirSync(dirPath, { recursive: true });
|
|
894
1352
|
logSuccess(`Created ${dir}/`);
|
|
895
1353
|
}
|
|
896
|
-
const gitignorePath =
|
|
897
|
-
if (!
|
|
898
|
-
|
|
1354
|
+
const gitignorePath = path7.join(commsDir, ".gitignore");
|
|
1355
|
+
if (!fs8.existsSync(gitignorePath)) {
|
|
1356
|
+
fs8.writeFileSync(
|
|
899
1357
|
gitignorePath,
|
|
900
1358
|
["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
|
|
901
1359
|
"utf-8"
|
|
@@ -904,12 +1362,12 @@ async function initCommand(args) {
|
|
|
904
1362
|
}
|
|
905
1363
|
const { config } = resolveConfig({}, repoRoot);
|
|
906
1364
|
const stateDir = config.stateDir;
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const stateDirRel =
|
|
1365
|
+
fs8.mkdirSync(path7.join(stateDir, "pids"), { recursive: true });
|
|
1366
|
+
fs8.mkdirSync(path7.join(stateDir, "logs"), { recursive: true });
|
|
1367
|
+
fs8.mkdirSync(path7.join(stateDir, "backups"), { recursive: true });
|
|
1368
|
+
const stateDirRel = path7.relative(repoRoot, stateDir);
|
|
911
1369
|
logSuccess(`Created ${stateDirRel}/ state directory`);
|
|
912
|
-
const repoGitignore =
|
|
1370
|
+
const repoGitignore = path7.join(repoRoot, ".gitignore");
|
|
913
1371
|
const gitignoreEntries = [
|
|
914
1372
|
{ entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
|
|
915
1373
|
{
|
|
@@ -917,11 +1375,11 @@ async function initCommand(args) {
|
|
|
917
1375
|
label: "tap-comms local config (machine-specific)"
|
|
918
1376
|
}
|
|
919
1377
|
];
|
|
920
|
-
if (
|
|
921
|
-
const content =
|
|
1378
|
+
if (fs8.existsSync(repoGitignore)) {
|
|
1379
|
+
const content = fs8.readFileSync(repoGitignore, "utf-8");
|
|
922
1380
|
for (const { entry, label } of gitignoreEntries) {
|
|
923
1381
|
if (!content.includes(entry)) {
|
|
924
|
-
|
|
1382
|
+
fs8.appendFileSync(repoGitignore, `
|
|
925
1383
|
# ${label}
|
|
926
1384
|
${entry}
|
|
927
1385
|
`);
|
|
@@ -960,15 +1418,18 @@ ${entry}
|
|
|
960
1418
|
};
|
|
961
1419
|
}
|
|
962
1420
|
|
|
1421
|
+
// src/commands/add.ts
|
|
1422
|
+
init_utils();
|
|
1423
|
+
|
|
963
1424
|
// src/adapters/claude.ts
|
|
964
|
-
import * as
|
|
965
|
-
import * as
|
|
1425
|
+
import * as fs10 from "fs";
|
|
1426
|
+
import * as path9 from "path";
|
|
966
1427
|
import { execSync } from "child_process";
|
|
967
1428
|
|
|
968
1429
|
// src/adapters/common.ts
|
|
969
|
-
import * as
|
|
1430
|
+
import * as fs9 from "fs";
|
|
970
1431
|
import * as os2 from "os";
|
|
971
|
-
import * as
|
|
1432
|
+
import * as path8 from "path";
|
|
972
1433
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
973
1434
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
974
1435
|
function probeCommand(candidates) {
|
|
@@ -986,7 +1447,7 @@ function probeCommand(candidates) {
|
|
|
986
1447
|
return { command: null, version: null };
|
|
987
1448
|
}
|
|
988
1449
|
function resolveCommandPath(command) {
|
|
989
|
-
if (
|
|
1450
|
+
if (path8.isAbsolute(command)) return command;
|
|
990
1451
|
const whichCmd = process.platform === "win32" ? "where.exe" : "which";
|
|
991
1452
|
try {
|
|
992
1453
|
const result = spawnSync2(whichCmd, [command], {
|
|
@@ -997,19 +1458,19 @@ function resolveCommandPath(command) {
|
|
|
997
1458
|
const lines = result.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
998
1459
|
if (lines.length === 0) return null;
|
|
999
1460
|
if (process.platform === "win32") {
|
|
1000
|
-
const candidateExt =
|
|
1461
|
+
const candidateExt = path8.extname(command).toLowerCase();
|
|
1001
1462
|
if (candidateExt) {
|
|
1002
1463
|
const extMatch = lines.find(
|
|
1003
|
-
(l) =>
|
|
1464
|
+
(l) => path8.extname(l).toLowerCase() === candidateExt && fs9.existsSync(l)
|
|
1004
1465
|
);
|
|
1005
1466
|
if (extMatch) return extMatch;
|
|
1006
1467
|
}
|
|
1007
1468
|
const executableMatch = lines.find(
|
|
1008
|
-
(l) => /\.(cmd|exe|ps1)$/i.test(l) &&
|
|
1469
|
+
(l) => /\.(cmd|exe|ps1)$/i.test(l) && fs9.existsSync(l)
|
|
1009
1470
|
);
|
|
1010
1471
|
if (executableMatch) return executableMatch;
|
|
1011
1472
|
}
|
|
1012
|
-
const firstValid = lines.find((l) =>
|
|
1473
|
+
const firstValid = lines.find((l) => fs9.existsSync(l));
|
|
1013
1474
|
return firstValid ?? null;
|
|
1014
1475
|
} catch {
|
|
1015
1476
|
return null;
|
|
@@ -1019,17 +1480,17 @@ function getHomeDir() {
|
|
|
1019
1480
|
return os2.homedir();
|
|
1020
1481
|
}
|
|
1021
1482
|
function toForwardSlashPath(filePath) {
|
|
1022
|
-
return
|
|
1483
|
+
return path8.resolve(filePath).replace(/\\/g, "/");
|
|
1023
1484
|
}
|
|
1024
1485
|
function canWriteOrCreate(filePath) {
|
|
1025
1486
|
try {
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1487
|
+
if (fs9.existsSync(filePath)) {
|
|
1488
|
+
fs9.accessSync(filePath, fs9.constants.W_OK);
|
|
1028
1489
|
return true;
|
|
1029
1490
|
}
|
|
1030
|
-
const parent =
|
|
1031
|
-
|
|
1032
|
-
|
|
1491
|
+
const parent = path8.dirname(filePath);
|
|
1492
|
+
fs9.mkdirSync(parent, { recursive: true });
|
|
1493
|
+
fs9.accessSync(parent, fs9.constants.W_OK);
|
|
1033
1494
|
return true;
|
|
1034
1495
|
} catch {
|
|
1035
1496
|
return false;
|
|
@@ -1041,14 +1502,14 @@ function isEphemeralPath(p) {
|
|
|
1041
1502
|
}
|
|
1042
1503
|
function findLocalTapCommsSource(ctx) {
|
|
1043
1504
|
const candidates = [
|
|
1044
|
-
|
|
1505
|
+
path8.join(
|
|
1045
1506
|
ctx.repoRoot,
|
|
1046
1507
|
"packages",
|
|
1047
1508
|
"tap-plugin",
|
|
1048
1509
|
"channels",
|
|
1049
1510
|
"tap-comms.ts"
|
|
1050
1511
|
),
|
|
1051
|
-
|
|
1512
|
+
path8.join(
|
|
1052
1513
|
ctx.repoRoot,
|
|
1053
1514
|
"node_modules",
|
|
1054
1515
|
"@hua-labs",
|
|
@@ -1058,19 +1519,19 @@ function findLocalTapCommsSource(ctx) {
|
|
|
1058
1519
|
)
|
|
1059
1520
|
];
|
|
1060
1521
|
for (const candidate of candidates) {
|
|
1061
|
-
if (
|
|
1522
|
+
if (fs9.existsSync(candidate)) return candidate;
|
|
1062
1523
|
}
|
|
1063
1524
|
return null;
|
|
1064
1525
|
}
|
|
1065
1526
|
function findBundledTapCommsSource(metaUrl = import.meta.url) {
|
|
1066
|
-
const moduleDir =
|
|
1527
|
+
const moduleDir = path8.dirname(fileURLToPath2(metaUrl));
|
|
1067
1528
|
const candidates = [
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1529
|
+
path8.join(moduleDir, "mcp-server.mjs"),
|
|
1530
|
+
path8.join(moduleDir, "..", "mcp-server.mjs"),
|
|
1531
|
+
path8.join(moduleDir, "..", "mcp-server.ts")
|
|
1071
1532
|
];
|
|
1072
1533
|
for (const candidate of candidates) {
|
|
1073
|
-
if (
|
|
1534
|
+
if (fs9.existsSync(candidate)) return candidate;
|
|
1074
1535
|
}
|
|
1075
1536
|
return null;
|
|
1076
1537
|
}
|
|
@@ -1079,15 +1540,15 @@ function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
|
|
|
1079
1540
|
}
|
|
1080
1541
|
function findPreferredBunCommand() {
|
|
1081
1542
|
const home = getHomeDir();
|
|
1082
|
-
const candidates = process.platform === "win32" ? [
|
|
1543
|
+
const candidates = process.platform === "win32" ? [path8.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path8.join(home, ".bun", "bin", "bun"), "bun"];
|
|
1083
1544
|
for (const candidate of candidates) {
|
|
1084
|
-
if (
|
|
1545
|
+
if (path8.isAbsolute(candidate) && !fs9.existsSync(candidate)) continue;
|
|
1085
1546
|
const result = spawnSync2(candidate, ["--version"], {
|
|
1086
1547
|
encoding: "utf-8",
|
|
1087
1548
|
shell: process.platform === "win32"
|
|
1088
1549
|
});
|
|
1089
1550
|
if (result.status === 0) {
|
|
1090
|
-
return
|
|
1551
|
+
return path8.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
|
|
1091
1552
|
}
|
|
1092
1553
|
}
|
|
1093
1554
|
return null;
|
|
@@ -1114,7 +1575,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1114
1575
|
}
|
|
1115
1576
|
const isBundled = sourcePath.endsWith(".mjs");
|
|
1116
1577
|
const isEphemeralSource = isEphemeralPath(sourcePath);
|
|
1117
|
-
let command =
|
|
1578
|
+
let command = null;
|
|
1118
1579
|
let args = [toForwardSlashPath(sourcePath)];
|
|
1119
1580
|
if (isEphemeralSource && isBundled) {
|
|
1120
1581
|
command = "npx";
|
|
@@ -1122,23 +1583,17 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1122
1583
|
warnings.push(
|
|
1123
1584
|
"Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
|
|
1124
1585
|
);
|
|
1125
|
-
} else if (
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
command = "node";
|
|
1129
|
-
warnings.push(
|
|
1130
|
-
"Detected ephemeral node path. Using `node` from PATH for MCP config stability."
|
|
1131
|
-
);
|
|
1132
|
-
} else {
|
|
1133
|
-
command = toForwardSlashPath(process.execPath);
|
|
1134
|
-
}
|
|
1135
|
-
warnings.push(
|
|
1136
|
-
"bun not found; using node to run the compiled MCP server. Install bun for better performance."
|
|
1586
|
+
} else if (isBundled) {
|
|
1587
|
+
const nodeProbe = probeCommand(
|
|
1588
|
+
process.platform === "win32" ? ["node", "node.exe"] : ["node"]
|
|
1137
1589
|
);
|
|
1590
|
+
command = nodeProbe.command ?? "node";
|
|
1591
|
+
} else {
|
|
1592
|
+
command = bunCommand;
|
|
1138
1593
|
}
|
|
1139
1594
|
if (!command) {
|
|
1140
1595
|
issues.push(
|
|
1141
|
-
"bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
|
|
1596
|
+
isBundled ? "node is required to run the compiled MCP server (.mjs). Ensure node is in PATH." : "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
|
|
1142
1597
|
);
|
|
1143
1598
|
return { command: null, args: [], env, sourcePath, warnings, issues };
|
|
1144
1599
|
}
|
|
@@ -1156,7 +1611,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
|
|
|
1156
1611
|
var MCP_SERVER_KEY = "tap";
|
|
1157
1612
|
var OLD_MCP_SERVER_KEY = "tap-comms";
|
|
1158
1613
|
function findMcpJsonPath(ctx) {
|
|
1159
|
-
return
|
|
1614
|
+
return path9.join(ctx.repoRoot, ".mcp.json");
|
|
1160
1615
|
}
|
|
1161
1616
|
function findClaudeCommand() {
|
|
1162
1617
|
try {
|
|
@@ -1182,11 +1637,11 @@ var claudeAdapter = {
|
|
|
1182
1637
|
const warnings = [];
|
|
1183
1638
|
const issues = [];
|
|
1184
1639
|
const configPath = findMcpJsonPath(ctx);
|
|
1185
|
-
const configExists =
|
|
1640
|
+
const configExists = fs10.existsSync(configPath);
|
|
1186
1641
|
const runtimeCommand = findClaudeCommand();
|
|
1187
1642
|
const canWrite = configExists ? (() => {
|
|
1188
1643
|
try {
|
|
1189
|
-
|
|
1644
|
+
fs10.accessSync(configPath, fs10.constants.W_OK);
|
|
1190
1645
|
return true;
|
|
1191
1646
|
} catch {
|
|
1192
1647
|
return false;
|
|
@@ -1200,7 +1655,7 @@ var claudeAdapter = {
|
|
|
1200
1655
|
const managed = buildManagedMcpServerSpec(ctx);
|
|
1201
1656
|
warnings.push(...managed.warnings);
|
|
1202
1657
|
issues.push(...managed.issues);
|
|
1203
|
-
if (!
|
|
1658
|
+
if (!fs10.existsSync(ctx.commsDir)) {
|
|
1204
1659
|
issues.push(
|
|
1205
1660
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1206
1661
|
);
|
|
@@ -1224,7 +1679,7 @@ var claudeAdapter = {
|
|
|
1224
1679
|
const operations = [];
|
|
1225
1680
|
const ownedArtifacts = [];
|
|
1226
1681
|
if (probe.configExists) {
|
|
1227
|
-
const raw =
|
|
1682
|
+
const raw = fs10.readFileSync(configPath, "utf-8");
|
|
1228
1683
|
try {
|
|
1229
1684
|
const config = JSON.parse(raw);
|
|
1230
1685
|
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
@@ -1288,9 +1743,9 @@ var claudeAdapter = {
|
|
|
1288
1743
|
try {
|
|
1289
1744
|
if (op.type === "set" || op.type === "merge") {
|
|
1290
1745
|
let config = {};
|
|
1291
|
-
if (
|
|
1746
|
+
if (fs10.existsSync(op.path)) {
|
|
1292
1747
|
backupFile(op.path, plan.backupDir);
|
|
1293
|
-
const raw =
|
|
1748
|
+
const raw = fs10.readFileSync(op.path, "utf-8");
|
|
1294
1749
|
try {
|
|
1295
1750
|
config = JSON.parse(raw);
|
|
1296
1751
|
} catch {
|
|
@@ -1307,12 +1762,12 @@ var claudeAdapter = {
|
|
|
1307
1762
|
setNestedKey(config, op.key, op.value);
|
|
1308
1763
|
}
|
|
1309
1764
|
const tmp = `${op.path}.tmp.${process.pid}`;
|
|
1310
|
-
|
|
1765
|
+
fs10.writeFileSync(
|
|
1311
1766
|
tmp,
|
|
1312
1767
|
JSON.stringify(config, null, 2) + "\n",
|
|
1313
1768
|
"utf-8"
|
|
1314
1769
|
);
|
|
1315
|
-
|
|
1770
|
+
fs10.renameSync(tmp, op.path);
|
|
1316
1771
|
changedFiles.push(op.path);
|
|
1317
1772
|
appliedOps++;
|
|
1318
1773
|
}
|
|
@@ -1341,12 +1796,12 @@ var claudeAdapter = {
|
|
|
1341
1796
|
if (configPath) {
|
|
1342
1797
|
checks.push({
|
|
1343
1798
|
name: "Config file exists",
|
|
1344
|
-
passed:
|
|
1345
|
-
message:
|
|
1799
|
+
passed: fs10.existsSync(configPath),
|
|
1800
|
+
message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1346
1801
|
});
|
|
1347
|
-
if (
|
|
1802
|
+
if (fs10.existsSync(configPath)) {
|
|
1348
1803
|
try {
|
|
1349
|
-
const raw =
|
|
1804
|
+
const raw = fs10.readFileSync(configPath, "utf-8");
|
|
1350
1805
|
const config = JSON.parse(raw);
|
|
1351
1806
|
checks.push({ name: "Config is valid JSON", passed: true });
|
|
1352
1807
|
const entry = config.mcpServers?.[MCP_SERVER_KEY];
|
|
@@ -1374,8 +1829,8 @@ var claudeAdapter = {
|
|
|
1374
1829
|
}
|
|
1375
1830
|
checks.push({
|
|
1376
1831
|
name: "Comms directory exists",
|
|
1377
|
-
passed:
|
|
1378
|
-
message:
|
|
1832
|
+
passed: fs10.existsSync(ctx.commsDir),
|
|
1833
|
+
message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1379
1834
|
});
|
|
1380
1835
|
const cmd = findClaudeCommand();
|
|
1381
1836
|
checks.push({
|
|
@@ -1408,35 +1863,35 @@ function setNestedKey(obj, keyPath, value) {
|
|
|
1408
1863
|
current[keys[keys.length - 1]] = value;
|
|
1409
1864
|
}
|
|
1410
1865
|
function normalizeTapCommsDir(value) {
|
|
1411
|
-
return typeof value === "string" ?
|
|
1866
|
+
return typeof value === "string" ? path9.resolve(value).replace(/\\/g, "/") : "";
|
|
1412
1867
|
}
|
|
1413
1868
|
|
|
1414
1869
|
// src/adapters/codex.ts
|
|
1415
|
-
import * as
|
|
1416
|
-
import * as
|
|
1870
|
+
import * as fs12 from "fs";
|
|
1871
|
+
import * as path11 from "path";
|
|
1417
1872
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1418
1873
|
|
|
1419
1874
|
// src/artifact-backups.ts
|
|
1420
|
-
import * as
|
|
1421
|
-
import * as
|
|
1422
|
-
import * as
|
|
1875
|
+
import * as crypto3 from "crypto";
|
|
1876
|
+
import * as fs11 from "fs";
|
|
1877
|
+
import * as path10 from "path";
|
|
1423
1878
|
function selectorHash(selector) {
|
|
1424
|
-
return
|
|
1879
|
+
return crypto3.createHash("sha256").update(selector).digest("hex").slice(0, 12);
|
|
1425
1880
|
}
|
|
1426
1881
|
function artifactBackupPath(backupDir, kind, selector) {
|
|
1427
1882
|
const safeKind = kind.replace(/[^a-z-]/gi, "-");
|
|
1428
|
-
return
|
|
1883
|
+
return path10.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
|
|
1429
1884
|
}
|
|
1430
1885
|
function writeArtifactBackup(backupPath, payload) {
|
|
1431
|
-
|
|
1886
|
+
fs11.mkdirSync(path10.dirname(backupPath), { recursive: true });
|
|
1432
1887
|
const tmp = `${backupPath}.tmp.${process.pid}`;
|
|
1433
|
-
|
|
1434
|
-
|
|
1888
|
+
fs11.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1889
|
+
fs11.renameSync(tmp, backupPath);
|
|
1435
1890
|
}
|
|
1436
1891
|
function readArtifactBackup(backupPath) {
|
|
1437
|
-
if (!
|
|
1892
|
+
if (!fs11.existsSync(backupPath)) return null;
|
|
1438
1893
|
try {
|
|
1439
|
-
const raw =
|
|
1894
|
+
const raw = fs11.readFileSync(backupPath, "utf-8");
|
|
1440
1895
|
return JSON.parse(raw);
|
|
1441
1896
|
} catch {
|
|
1442
1897
|
return null;
|
|
@@ -1450,10 +1905,10 @@ var SESSION_NEUTRAL_AGENT_NAME = "<set-per-session>";
|
|
|
1450
1905
|
var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1451
1906
|
var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1452
1907
|
function findCodexConfigPath2() {
|
|
1453
|
-
return
|
|
1908
|
+
return path11.join(getHomeDir(), ".codex", "config.toml");
|
|
1454
1909
|
}
|
|
1455
1910
|
function canonicalizeTrustPath2(targetPath) {
|
|
1456
|
-
let resolved =
|
|
1911
|
+
let resolved = path11.resolve(targetPath).replace(/\//g, "\\");
|
|
1457
1912
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
1458
1913
|
if (!driveRoot.test(resolved)) {
|
|
1459
1914
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -1465,7 +1920,7 @@ function trustSelector(targetPath) {
|
|
|
1465
1920
|
}
|
|
1466
1921
|
function getTrustTargets(ctx) {
|
|
1467
1922
|
const targets = [ctx.repoRoot, process.cwd()];
|
|
1468
|
-
return [...new Set(targets.map((value) =>
|
|
1923
|
+
return [...new Set(targets.map((value) => path11.resolve(value)))];
|
|
1469
1924
|
}
|
|
1470
1925
|
function buildManagedArtifacts(configPath, ctx) {
|
|
1471
1926
|
const artifacts = [
|
|
@@ -1482,14 +1937,14 @@ function buildManagedArtifacts(configPath, ctx) {
|
|
|
1482
1937
|
return artifacts;
|
|
1483
1938
|
}
|
|
1484
1939
|
function readConfigOrEmpty(configPath) {
|
|
1485
|
-
if (!
|
|
1486
|
-
return
|
|
1940
|
+
if (!fs12.existsSync(configPath)) return "";
|
|
1941
|
+
return fs12.readFileSync(configPath, "utf-8");
|
|
1487
1942
|
}
|
|
1488
1943
|
function writeTomlFile(filePath, content) {
|
|
1489
|
-
|
|
1944
|
+
fs12.mkdirSync(path11.dirname(filePath), { recursive: true });
|
|
1490
1945
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
1491
|
-
|
|
1492
|
-
|
|
1946
|
+
fs12.writeFileSync(tmp, content, "utf-8");
|
|
1947
|
+
fs12.renameSync(tmp, filePath);
|
|
1493
1948
|
}
|
|
1494
1949
|
function buildSessionNeutralCodexSpec(ctx) {
|
|
1495
1950
|
const managed = buildManagedMcpServerSpec(ctx);
|
|
@@ -1515,8 +1970,8 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1515
1970
|
const envTable = extractTomlTable(content, ENV_SELECTOR);
|
|
1516
1971
|
checks.push({
|
|
1517
1972
|
name: "Codex config exists",
|
|
1518
|
-
passed:
|
|
1519
|
-
message:
|
|
1973
|
+
passed: fs12.existsSync(configPath),
|
|
1974
|
+
message: fs12.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1520
1975
|
});
|
|
1521
1976
|
checks.push({
|
|
1522
1977
|
name: "tap MCP table present",
|
|
@@ -1547,6 +2002,14 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1547
2002
|
message: "Managed tap command/args do not match expected values"
|
|
1548
2003
|
});
|
|
1549
2004
|
}
|
|
2005
|
+
if (mainTable) {
|
|
2006
|
+
const mainValues = parseTomlAssignments(mainTable);
|
|
2007
|
+
checks.push({
|
|
2008
|
+
name: "approval_mode is auto",
|
|
2009
|
+
passed: mainValues.approval_mode === "auto",
|
|
2010
|
+
message: mainValues.approval_mode ? `approval_mode is "${mainValues.approval_mode}", expected "auto"` : 'approval_mode missing, expected "auto"'
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
1550
2013
|
if (envTable) {
|
|
1551
2014
|
const envValues = parseTomlAssignments(envTable);
|
|
1552
2015
|
checks.push({
|
|
@@ -1568,7 +2031,7 @@ var codexAdapter = {
|
|
|
1568
2031
|
const warnings = [];
|
|
1569
2032
|
const issues = [];
|
|
1570
2033
|
const configPath = findCodexConfigPath2();
|
|
1571
|
-
const configExists =
|
|
2034
|
+
const configExists = fs12.existsSync(configPath);
|
|
1572
2035
|
const runtimeProbe = probeCommand(
|
|
1573
2036
|
ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
|
|
1574
2037
|
);
|
|
@@ -1577,7 +2040,7 @@ var codexAdapter = {
|
|
|
1577
2040
|
"Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1578
2041
|
);
|
|
1579
2042
|
}
|
|
1580
|
-
if (!
|
|
2043
|
+
if (!fs12.existsSync(ctx.commsDir)) {
|
|
1581
2044
|
issues.push(
|
|
1582
2045
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1583
2046
|
);
|
|
@@ -1658,7 +2121,7 @@ var codexAdapter = {
|
|
|
1658
2121
|
};
|
|
1659
2122
|
}
|
|
1660
2123
|
const existingContent = readConfigOrEmpty(configPath);
|
|
1661
|
-
if (
|
|
2124
|
+
if (fs12.existsSync(configPath) && existingContent) {
|
|
1662
2125
|
backupFile(configPath, plan.backupDir);
|
|
1663
2126
|
}
|
|
1664
2127
|
const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
|
|
@@ -1690,7 +2153,8 @@ var codexAdapter = {
|
|
|
1690
2153
|
MCP_SELECTOR,
|
|
1691
2154
|
{
|
|
1692
2155
|
command: managed.command,
|
|
1693
|
-
args: managed.args
|
|
2156
|
+
args: managed.args,
|
|
2157
|
+
approval_mode: "auto"
|
|
1694
2158
|
},
|
|
1695
2159
|
extractTomlTable(existingContent, MCP_SELECTOR)
|
|
1696
2160
|
)
|
|
@@ -1741,8 +2205,8 @@ var codexAdapter = {
|
|
|
1741
2205
|
const checks = verifyManagedToml(content, ctx, configPath);
|
|
1742
2206
|
checks.push({
|
|
1743
2207
|
name: "Comms directory exists",
|
|
1744
|
-
passed:
|
|
1745
|
-
message:
|
|
2208
|
+
passed: fs12.existsSync(ctx.commsDir),
|
|
2209
|
+
message: fs12.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1746
2210
|
});
|
|
1747
2211
|
checks.push({
|
|
1748
2212
|
name: "Codex CLI found",
|
|
@@ -1765,12 +2229,12 @@ var codexAdapter = {
|
|
|
1765
2229
|
return "app-server";
|
|
1766
2230
|
},
|
|
1767
2231
|
resolveBridgeScript(ctx) {
|
|
1768
|
-
const distDir =
|
|
2232
|
+
const distDir = path11.dirname(fileURLToPath3(import.meta.url));
|
|
1769
2233
|
const candidates = [
|
|
1770
2234
|
// 1. Relative to bundled CLI (npm install / npx)
|
|
1771
|
-
|
|
2235
|
+
path11.join(distDir, "bridges", "codex-bridge-runner.mjs"),
|
|
1772
2236
|
// 2. Monorepo development — dist inside repo
|
|
1773
|
-
|
|
2237
|
+
path11.join(
|
|
1774
2238
|
ctx.repoRoot,
|
|
1775
2239
|
"packages",
|
|
1776
2240
|
"tap-comms",
|
|
@@ -1779,7 +2243,7 @@ var codexAdapter = {
|
|
|
1779
2243
|
"codex-bridge-runner.mjs"
|
|
1780
2244
|
),
|
|
1781
2245
|
// 3. Source file — dev mode with strip-types
|
|
1782
|
-
|
|
2246
|
+
path11.join(
|
|
1783
2247
|
ctx.repoRoot,
|
|
1784
2248
|
"packages",
|
|
1785
2249
|
"tap-comms",
|
|
@@ -1789,31 +2253,47 @@ var codexAdapter = {
|
|
|
1789
2253
|
)
|
|
1790
2254
|
];
|
|
1791
2255
|
for (const candidate of candidates) {
|
|
1792
|
-
if (
|
|
2256
|
+
if (fs12.existsSync(candidate)) return candidate;
|
|
1793
2257
|
}
|
|
1794
2258
|
return null;
|
|
1795
2259
|
}
|
|
1796
2260
|
};
|
|
2261
|
+
function patchCodexApprovalMode() {
|
|
2262
|
+
const configPath = findCodexConfigPath2();
|
|
2263
|
+
if (!fs12.existsSync(configPath)) return null;
|
|
2264
|
+
const content = fs12.readFileSync(configPath, "utf-8");
|
|
2265
|
+
const tapTable = extractTomlTable(content, MCP_SELECTOR);
|
|
2266
|
+
if (!tapTable) return null;
|
|
2267
|
+
const values = parseTomlAssignments(tapTable);
|
|
2268
|
+
if (values.approval_mode === "auto") return null;
|
|
2269
|
+
const patched = replaceTomlTable(
|
|
2270
|
+
content,
|
|
2271
|
+
MCP_SELECTOR,
|
|
2272
|
+
renderTomlTable(MCP_SELECTOR, { approval_mode: "auto" }, tapTable)
|
|
2273
|
+
);
|
|
2274
|
+
writeTomlFile(configPath, patched);
|
|
2275
|
+
return configPath;
|
|
2276
|
+
}
|
|
1797
2277
|
|
|
1798
2278
|
// src/adapters/gemini.ts
|
|
1799
|
-
import * as
|
|
1800
|
-
import * as
|
|
2279
|
+
import * as fs13 from "fs";
|
|
2280
|
+
import * as path12 from "path";
|
|
1801
2281
|
var GEMINI_SELECTOR = "mcpServers.tap";
|
|
1802
2282
|
var OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
|
|
1803
2283
|
function candidateConfigPaths(ctx) {
|
|
1804
2284
|
const home = getHomeDir();
|
|
1805
2285
|
return [
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2286
|
+
path12.join(ctx.repoRoot, ".gemini", "settings.json"),
|
|
2287
|
+
path12.join(home, ".gemini", "settings.json"),
|
|
2288
|
+
path12.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
1809
2289
|
];
|
|
1810
2290
|
}
|
|
1811
2291
|
function chooseGeminiConfigPath(ctx) {
|
|
1812
2292
|
const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
|
|
1813
|
-
if (
|
|
1814
|
-
if (
|
|
1815
|
-
if (
|
|
1816
|
-
const raw =
|
|
2293
|
+
if (fs13.existsSync(workspaceConfig)) return workspaceConfig;
|
|
2294
|
+
if (fs13.existsSync(homeConfig)) return homeConfig;
|
|
2295
|
+
if (fs13.existsSync(antigravityConfig)) {
|
|
2296
|
+
const raw = fs13.readFileSync(antigravityConfig, "utf-8").trim();
|
|
1817
2297
|
if (raw) {
|
|
1818
2298
|
try {
|
|
1819
2299
|
JSON.parse(raw);
|
|
@@ -1825,8 +2305,8 @@ function chooseGeminiConfigPath(ctx) {
|
|
|
1825
2305
|
return workspaceConfig;
|
|
1826
2306
|
}
|
|
1827
2307
|
function readJsonFile(filePath) {
|
|
1828
|
-
if (!
|
|
1829
|
-
const raw =
|
|
2308
|
+
if (!fs13.existsSync(filePath)) return {};
|
|
2309
|
+
const raw = fs13.readFileSync(filePath, "utf-8").trim();
|
|
1830
2310
|
if (!raw) return {};
|
|
1831
2311
|
return JSON.parse(raw);
|
|
1832
2312
|
}
|
|
@@ -1857,8 +2337,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1857
2337
|
const entry = readNestedKey(config, GEMINI_SELECTOR);
|
|
1858
2338
|
checks.push({
|
|
1859
2339
|
name: "Gemini config exists",
|
|
1860
|
-
passed:
|
|
1861
|
-
message:
|
|
2340
|
+
passed: fs13.existsSync(configPath),
|
|
2341
|
+
message: fs13.existsSync(configPath) ? void 0 : `${configPath} not found`
|
|
1862
2342
|
});
|
|
1863
2343
|
checks.push({
|
|
1864
2344
|
name: "tap entry present",
|
|
@@ -1867,8 +2347,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
|
|
|
1867
2347
|
});
|
|
1868
2348
|
checks.push({
|
|
1869
2349
|
name: "Comms directory exists",
|
|
1870
|
-
passed:
|
|
1871
|
-
message:
|
|
2350
|
+
passed: fs13.existsSync(ctx.commsDir),
|
|
2351
|
+
message: fs13.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
|
|
1872
2352
|
});
|
|
1873
2353
|
if (entry?.env && typeof entry.env === "object") {
|
|
1874
2354
|
checks.push({
|
|
@@ -1885,7 +2365,7 @@ var geminiAdapter = {
|
|
|
1885
2365
|
const warnings = [];
|
|
1886
2366
|
const issues = [];
|
|
1887
2367
|
const configPath = chooseGeminiConfigPath(ctx);
|
|
1888
|
-
const configExists =
|
|
2368
|
+
const configExists = fs13.existsSync(configPath);
|
|
1889
2369
|
const runtimeProbe = probeCommand(
|
|
1890
2370
|
ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
|
|
1891
2371
|
);
|
|
@@ -1894,7 +2374,7 @@ var geminiAdapter = {
|
|
|
1894
2374
|
"Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
|
|
1895
2375
|
);
|
|
1896
2376
|
}
|
|
1897
|
-
if (!
|
|
2377
|
+
if (!fs13.existsSync(ctx.commsDir)) {
|
|
1898
2378
|
issues.push(
|
|
1899
2379
|
`Comms directory not found: ${ctx.commsDir}. Run "init" first.`
|
|
1900
2380
|
);
|
|
@@ -1973,8 +2453,8 @@ var geminiAdapter = {
|
|
|
1973
2453
|
}
|
|
1974
2454
|
let config = {};
|
|
1975
2455
|
let previousValue = void 0;
|
|
1976
|
-
if (
|
|
1977
|
-
if (
|
|
2456
|
+
if (fs13.existsSync(configPath)) {
|
|
2457
|
+
if (fs13.readFileSync(configPath, "utf-8").trim()) {
|
|
1978
2458
|
backupFile(configPath, plan.backupDir);
|
|
1979
2459
|
}
|
|
1980
2460
|
try {
|
|
@@ -2011,10 +2491,10 @@ var geminiAdapter = {
|
|
|
2011
2491
|
args: managed.args,
|
|
2012
2492
|
env: managed.env
|
|
2013
2493
|
});
|
|
2014
|
-
|
|
2494
|
+
fs13.mkdirSync(path12.dirname(configPath), { recursive: true });
|
|
2015
2495
|
const tmp = `${configPath}.tmp.${process.pid}`;
|
|
2016
|
-
|
|
2017
|
-
|
|
2496
|
+
fs13.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2497
|
+
fs13.renameSync(tmp, configPath);
|
|
2018
2498
|
changedFiles.push(configPath);
|
|
2019
2499
|
return {
|
|
2020
2500
|
success: true,
|
|
@@ -2085,57 +2565,83 @@ function getAdapter(runtime) {
|
|
|
2085
2565
|
}
|
|
2086
2566
|
|
|
2087
2567
|
// src/engine/bridge-paths.ts
|
|
2088
|
-
import * as
|
|
2568
|
+
import * as path13 from "path";
|
|
2569
|
+
function assertPathContained(resolved, stateDir, subDir) {
|
|
2570
|
+
const expectedDir = path13.resolve(stateDir, subDir) + path13.sep;
|
|
2571
|
+
const normalizedResolved = path13.resolve(resolved);
|
|
2572
|
+
if (!normalizedResolved.startsWith(expectedDir)) {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`Path traversal blocked: resolved path escapes "${subDir}/" directory`
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
return normalizedResolved;
|
|
2578
|
+
}
|
|
2089
2579
|
function appServerLogFilePath(stateDir, instanceId) {
|
|
2090
|
-
return
|
|
2580
|
+
return assertPathContained(
|
|
2581
|
+
path13.join(stateDir, "logs", `app-server-${instanceId}.log`),
|
|
2582
|
+
stateDir,
|
|
2583
|
+
"logs"
|
|
2584
|
+
);
|
|
2091
2585
|
}
|
|
2092
2586
|
function appServerGatewayLogFilePath(stateDir, instanceId) {
|
|
2093
|
-
return
|
|
2587
|
+
return assertPathContained(
|
|
2588
|
+
path13.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`),
|
|
2589
|
+
stateDir,
|
|
2590
|
+
"logs"
|
|
2591
|
+
);
|
|
2094
2592
|
}
|
|
2095
2593
|
function appServerGatewayTokenFilePath(stateDir, instanceId) {
|
|
2096
|
-
return
|
|
2594
|
+
return assertPathContained(
|
|
2595
|
+
path13.join(stateDir, "secrets", `app-server-gateway-${instanceId}.token`),
|
|
2097
2596
|
stateDir,
|
|
2098
|
-
"secrets"
|
|
2099
|
-
`app-server-gateway-${instanceId}.token`
|
|
2597
|
+
"secrets"
|
|
2100
2598
|
);
|
|
2101
2599
|
}
|
|
2102
2600
|
function stderrLogFilePath(logPath) {
|
|
2103
2601
|
return `${logPath}.stderr`;
|
|
2104
2602
|
}
|
|
2105
2603
|
function pidFilePath(stateDir, instanceId) {
|
|
2106
|
-
return
|
|
2604
|
+
return assertPathContained(
|
|
2605
|
+
path13.join(stateDir, "pids", `bridge-${instanceId}.json`),
|
|
2606
|
+
stateDir,
|
|
2607
|
+
"pids"
|
|
2608
|
+
);
|
|
2107
2609
|
}
|
|
2108
2610
|
function logFilePath(stateDir, instanceId) {
|
|
2109
|
-
return
|
|
2611
|
+
return assertPathContained(
|
|
2612
|
+
path13.join(stateDir, "logs", `bridge-${instanceId}.log`),
|
|
2613
|
+
stateDir,
|
|
2614
|
+
"logs"
|
|
2615
|
+
);
|
|
2110
2616
|
}
|
|
2111
2617
|
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
2112
|
-
return
|
|
2618
|
+
return path13.join(runtimeStateDir, "heartbeat.json");
|
|
2113
2619
|
}
|
|
2114
2620
|
function runtimeThreadStateFilePath(runtimeStateDir) {
|
|
2115
|
-
return
|
|
2621
|
+
return path13.join(runtimeStateDir, "thread.json");
|
|
2116
2622
|
}
|
|
2117
2623
|
|
|
2118
2624
|
// src/engine/bridge-file-io.ts
|
|
2119
|
-
import * as
|
|
2120
|
-
import * as
|
|
2625
|
+
import * as fs14 from "fs";
|
|
2626
|
+
import * as path14 from "path";
|
|
2121
2627
|
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
2122
2628
|
function writeProtectedTextFile(filePath, content) {
|
|
2123
|
-
|
|
2629
|
+
fs14.mkdirSync(path14.dirname(filePath), { recursive: true });
|
|
2124
2630
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
2125
|
-
|
|
2631
|
+
fs14.writeFileSync(tmp, content, {
|
|
2126
2632
|
encoding: "utf-8",
|
|
2127
2633
|
mode: APP_SERVER_AUTH_FILE_MODE
|
|
2128
2634
|
});
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2635
|
+
fs14.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
2636
|
+
fs14.renameSync(tmp, filePath);
|
|
2637
|
+
fs14.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
2132
2638
|
}
|
|
2133
2639
|
function removeFileIfExists(filePath) {
|
|
2134
|
-
if (!filePath || !
|
|
2640
|
+
if (!filePath || !fs14.existsSync(filePath)) {
|
|
2135
2641
|
return;
|
|
2136
2642
|
}
|
|
2137
2643
|
try {
|
|
2138
|
-
|
|
2644
|
+
fs14.unlinkSync(filePath);
|
|
2139
2645
|
} catch {
|
|
2140
2646
|
}
|
|
2141
2647
|
}
|
|
@@ -2154,14 +2660,14 @@ function getWebSocketCtor() {
|
|
|
2154
2660
|
return typeof candidate === "function" ? candidate : null;
|
|
2155
2661
|
}
|
|
2156
2662
|
function delay(ms) {
|
|
2157
|
-
return new Promise((
|
|
2663
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
2158
2664
|
}
|
|
2159
2665
|
function isLoopbackHost(hostname) {
|
|
2160
2666
|
return hostname === "127.0.0.1" || hostname === "localhost";
|
|
2161
2667
|
}
|
|
2162
2668
|
async function allocateLoopbackPort(hostname) {
|
|
2163
2669
|
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2164
|
-
return await new Promise((
|
|
2670
|
+
return await new Promise((resolve15, reject) => {
|
|
2165
2671
|
const server = net.createServer();
|
|
2166
2672
|
server.unref();
|
|
2167
2673
|
server.once("error", reject);
|
|
@@ -2179,19 +2685,19 @@ async function allocateLoopbackPort(hostname) {
|
|
|
2179
2685
|
reject(error);
|
|
2180
2686
|
return;
|
|
2181
2687
|
}
|
|
2182
|
-
|
|
2688
|
+
resolve15(port);
|
|
2183
2689
|
});
|
|
2184
2690
|
});
|
|
2185
2691
|
});
|
|
2186
2692
|
}
|
|
2187
2693
|
async function isTcpPortAvailable(hostname, port) {
|
|
2188
2694
|
const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
|
|
2189
|
-
return await new Promise((
|
|
2695
|
+
return await new Promise((resolve15) => {
|
|
2190
2696
|
const server = net.createServer();
|
|
2191
2697
|
server.unref();
|
|
2192
|
-
server.once("error", () =>
|
|
2698
|
+
server.once("error", () => resolve15(false));
|
|
2193
2699
|
server.listen(port, bindHost, () => {
|
|
2194
|
-
server.close((error) =>
|
|
2700
|
+
server.close((error) => resolve15(!error));
|
|
2195
2701
|
});
|
|
2196
2702
|
});
|
|
2197
2703
|
}
|
|
@@ -2243,8 +2749,8 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
|
|
|
2243
2749
|
}
|
|
2244
2750
|
|
|
2245
2751
|
// src/engine/bridge-codex-command.ts
|
|
2246
|
-
import * as
|
|
2247
|
-
import * as
|
|
2752
|
+
import * as fs15 from "fs";
|
|
2753
|
+
import * as path15 from "path";
|
|
2248
2754
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2249
2755
|
function resolveCodexCommand(platform) {
|
|
2250
2756
|
const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
|
|
@@ -2259,20 +2765,18 @@ function resolveCodexCommand(platform) {
|
|
|
2259
2765
|
function unwrapNpmCmdShim(cmdPath) {
|
|
2260
2766
|
let content;
|
|
2261
2767
|
try {
|
|
2262
|
-
content =
|
|
2768
|
+
content = fs15.readFileSync(cmdPath, "utf-8");
|
|
2263
2769
|
} catch {
|
|
2264
2770
|
return null;
|
|
2265
2771
|
}
|
|
2266
|
-
const match = content.match(
|
|
2267
|
-
/"%_prog%"\s+"(%dp0%\\[^"]+)"\s+%\*/
|
|
2268
|
-
);
|
|
2772
|
+
const match = content.match(/"%_prog%"\s+"(%dp0%\\[^"]+)"\s+%\*/);
|
|
2269
2773
|
if (!match) return null;
|
|
2270
|
-
const dp0 =
|
|
2774
|
+
const dp0 = path15.dirname(cmdPath);
|
|
2271
2775
|
const scriptRelative = match[1].replace(/%dp0%\\/g, "");
|
|
2272
|
-
const scriptPath =
|
|
2273
|
-
if (!
|
|
2274
|
-
const localNode =
|
|
2275
|
-
const nodeCommand =
|
|
2776
|
+
const scriptPath = path15.resolve(dp0, scriptRelative);
|
|
2777
|
+
if (!fs15.existsSync(scriptPath)) return null;
|
|
2778
|
+
const localNode = path15.join(dp0, "node.exe");
|
|
2779
|
+
const nodeCommand = fs15.existsSync(localNode) ? localNode : probeCommand(["node.exe", "node"]).command ?? "node";
|
|
2276
2780
|
return `${nodeCommand}\0${scriptPath}`;
|
|
2277
2781
|
}
|
|
2278
2782
|
function splitResolvedCommand(resolved) {
|
|
@@ -2286,14 +2790,16 @@ function resolvePowerShellCommand() {
|
|
|
2286
2790
|
return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
|
|
2287
2791
|
}
|
|
2288
2792
|
function resolveAuthGatewayScript(repoRoot) {
|
|
2289
|
-
const moduleDir =
|
|
2793
|
+
const moduleDir = path15.dirname(fileURLToPath4(import.meta.url));
|
|
2794
|
+
const resolvedModuleDir = path15.resolve(moduleDir);
|
|
2795
|
+
const resolvedRepoRoot = path15.resolve(repoRoot);
|
|
2290
2796
|
const candidates = [
|
|
2291
2797
|
// Bundled: dist/bridges/ sibling (npm install / built package)
|
|
2292
|
-
|
|
2798
|
+
path15.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
|
|
2293
2799
|
// Source: src/bridges/ sibling (monorepo dev with ts runner)
|
|
2294
|
-
|
|
2800
|
+
path15.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
|
|
2295
2801
|
// Monorepo dist fallback
|
|
2296
|
-
|
|
2802
|
+
path15.join(
|
|
2297
2803
|
repoRoot,
|
|
2298
2804
|
"packages",
|
|
2299
2805
|
"tap-comms",
|
|
@@ -2301,7 +2807,7 @@ function resolveAuthGatewayScript(repoRoot) {
|
|
|
2301
2807
|
"bridges",
|
|
2302
2808
|
"codex-app-server-auth-gateway.mjs"
|
|
2303
2809
|
),
|
|
2304
|
-
|
|
2810
|
+
path15.join(
|
|
2305
2811
|
repoRoot,
|
|
2306
2812
|
"packages",
|
|
2307
2813
|
"tap-comms",
|
|
@@ -2311,17 +2817,21 @@ function resolveAuthGatewayScript(repoRoot) {
|
|
|
2311
2817
|
)
|
|
2312
2818
|
];
|
|
2313
2819
|
for (const candidate of candidates) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2820
|
+
const resolved = path15.resolve(candidate);
|
|
2821
|
+
if (!resolved.startsWith(resolvedModuleDir + path15.sep) && !resolved.startsWith(resolvedRepoRoot + path15.sep)) {
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
if (fs15.existsSync(resolved)) {
|
|
2825
|
+
return resolved;
|
|
2316
2826
|
}
|
|
2317
2827
|
}
|
|
2318
2828
|
return null;
|
|
2319
2829
|
}
|
|
2320
2830
|
|
|
2321
2831
|
// src/engine/bridge-windows-spawn.ts
|
|
2322
|
-
import * as
|
|
2832
|
+
import * as fs16 from "fs";
|
|
2323
2833
|
import * as os3 from "os";
|
|
2324
|
-
import * as
|
|
2834
|
+
import * as path16 from "path";
|
|
2325
2835
|
import { randomBytes } from "crypto";
|
|
2326
2836
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2327
2837
|
var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
|
|
@@ -2329,7 +2839,7 @@ var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
|
|
|
2329
2839
|
function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
|
|
2330
2840
|
let entries;
|
|
2331
2841
|
try {
|
|
2332
|
-
entries =
|
|
2842
|
+
entries = fs16.readdirSync(os3.tmpdir());
|
|
2333
2843
|
} catch {
|
|
2334
2844
|
return;
|
|
2335
2845
|
}
|
|
@@ -2337,13 +2847,13 @@ function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
|
|
|
2337
2847
|
if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
|
|
2338
2848
|
continue;
|
|
2339
2849
|
}
|
|
2340
|
-
const wrapperPath =
|
|
2850
|
+
const wrapperPath = path16.join(os3.tmpdir(), entry);
|
|
2341
2851
|
try {
|
|
2342
|
-
const stats =
|
|
2852
|
+
const stats = fs16.statSync(wrapperPath);
|
|
2343
2853
|
if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
|
|
2344
2854
|
continue;
|
|
2345
2855
|
}
|
|
2346
|
-
|
|
2856
|
+
fs16.unlinkSync(wrapperPath);
|
|
2347
2857
|
} catch {
|
|
2348
2858
|
}
|
|
2349
2859
|
}
|
|
@@ -2378,11 +2888,11 @@ function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = pro
|
|
|
2378
2888
|
const stderrLogPath = stderrLogFilePath(logPath);
|
|
2379
2889
|
const powerShellCommand = resolvePowerShellCommand();
|
|
2380
2890
|
cleanupStaleWindowsSpawnWrappers();
|
|
2381
|
-
const wrapperPath =
|
|
2891
|
+
const wrapperPath = path16.join(
|
|
2382
2892
|
os3.tmpdir(),
|
|
2383
2893
|
`${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
|
|
2384
2894
|
);
|
|
2385
|
-
|
|
2895
|
+
fs16.writeFileSync(
|
|
2386
2896
|
wrapperPath,
|
|
2387
2897
|
buildWindowsDetachedWrapperScript(
|
|
2388
2898
|
command,
|
|
@@ -2468,7 +2978,7 @@ function findListeningProcessId(url, platform) {
|
|
|
2468
2978
|
}
|
|
2469
2979
|
|
|
2470
2980
|
// src/engine/bridge-unix-spawn.ts
|
|
2471
|
-
import * as
|
|
2981
|
+
import * as fs17 from "fs";
|
|
2472
2982
|
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
2473
2983
|
var DEFAULT_UNIX_PLATFORM = process.platform === "darwin" ? "darwin" : "linux";
|
|
2474
2984
|
function resolveUnixSpawnCommand(command, args, platform) {
|
|
@@ -2515,8 +3025,8 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
|
|
|
2515
3025
|
let logFd = null;
|
|
2516
3026
|
let stderrFd = null;
|
|
2517
3027
|
try {
|
|
2518
|
-
logFd =
|
|
2519
|
-
stderrFd =
|
|
3028
|
+
logFd = fs17.openSync(logPath, "a");
|
|
3029
|
+
stderrFd = fs17.openSync(stderrPath, "a");
|
|
2520
3030
|
const launch = resolveUnixSpawnCommand(command, args, platform);
|
|
2521
3031
|
const child = spawn(launch.command, launch.args, {
|
|
2522
3032
|
cwd: repoRoot,
|
|
@@ -2529,10 +3039,10 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
|
|
|
2529
3039
|
return child.pid ?? null;
|
|
2530
3040
|
} finally {
|
|
2531
3041
|
if (logFd != null) {
|
|
2532
|
-
|
|
3042
|
+
fs17.closeSync(logFd);
|
|
2533
3043
|
}
|
|
2534
3044
|
if (stderrFd != null) {
|
|
2535
|
-
|
|
3045
|
+
fs17.closeSync(stderrFd);
|
|
2536
3046
|
}
|
|
2537
3047
|
}
|
|
2538
3048
|
}
|
|
@@ -2638,10 +3148,18 @@ async function stopManagedAppServer(appServer, platform) {
|
|
|
2638
3148
|
}
|
|
2639
3149
|
|
|
2640
3150
|
// src/engine/bridge-config.ts
|
|
2641
|
-
import * as
|
|
2642
|
-
import * as
|
|
3151
|
+
import * as fs18 from "fs";
|
|
3152
|
+
import * as path17 from "path";
|
|
3153
|
+
init_instance_config();
|
|
2643
3154
|
function resolveAgentName(instanceId, explicit, context) {
|
|
2644
3155
|
if (explicit) return explicit;
|
|
3156
|
+
if (context?.stateDir) {
|
|
3157
|
+
try {
|
|
3158
|
+
const instConfig = loadInstanceConfig(context.stateDir, instanceId);
|
|
3159
|
+
if (instConfig?.agentName) return instConfig.agentName;
|
|
3160
|
+
} catch {
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
2645
3163
|
try {
|
|
2646
3164
|
const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
|
|
2647
3165
|
const state = loadState(repoRoot);
|
|
@@ -2660,13 +3178,13 @@ function inferRestartMode(bridgeState, flags, savedMode) {
|
|
|
2660
3178
|
}
|
|
2661
3179
|
function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
2662
3180
|
const removed = [];
|
|
2663
|
-
if (!
|
|
3181
|
+
if (!fs18.existsSync(inboxDir)) return removed;
|
|
2664
3182
|
const normalizedAgent = agentName.replace(/-/g, "_");
|
|
2665
3183
|
const marker = `-headless-${normalizedAgent}-review-`;
|
|
2666
3184
|
try {
|
|
2667
|
-
for (const file of
|
|
3185
|
+
for (const file of fs18.readdirSync(inboxDir)) {
|
|
2668
3186
|
if (file.includes(marker)) {
|
|
2669
|
-
|
|
3187
|
+
fs18.unlinkSync(path17.join(inboxDir, file));
|
|
2670
3188
|
removed.push(file);
|
|
2671
3189
|
}
|
|
2672
3190
|
}
|
|
@@ -2676,19 +3194,31 @@ function cleanupHeadlessDispatch(inboxDir, agentName) {
|
|
|
2676
3194
|
}
|
|
2677
3195
|
|
|
2678
3196
|
// src/engine/bridge-state.ts
|
|
2679
|
-
import * as
|
|
3197
|
+
import * as fs19 from "fs";
|
|
3198
|
+
function transitionBridgeLifecycle(previous, nextState, reason, options) {
|
|
3199
|
+
const at = options?.at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3200
|
+
const changed = previous?.state !== nextState;
|
|
3201
|
+
return {
|
|
3202
|
+
state: nextState,
|
|
3203
|
+
since: changed || !previous?.since ? at : previous.since,
|
|
3204
|
+
updatedAt: at,
|
|
3205
|
+
lastTransitionAt: changed || !previous?.lastTransitionAt ? at : previous.lastTransitionAt,
|
|
3206
|
+
lastTransitionReason: changed || previous?.lastTransitionReason == null ? reason : previous.lastTransitionReason,
|
|
3207
|
+
restartCount: (previous?.restartCount ?? 0) + (options?.incrementRestart ? 1 : 0)
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
2680
3210
|
function loadRuntimeBridgeHeartbeat(bridgeState) {
|
|
2681
3211
|
const runtimeStateDir = bridgeState?.runtimeStateDir;
|
|
2682
3212
|
if (!runtimeStateDir) {
|
|
2683
3213
|
return null;
|
|
2684
3214
|
}
|
|
2685
3215
|
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
2686
|
-
if (!
|
|
3216
|
+
if (!fs19.existsSync(heartbeatPath)) {
|
|
2687
3217
|
return null;
|
|
2688
3218
|
}
|
|
2689
3219
|
try {
|
|
2690
3220
|
return JSON.parse(
|
|
2691
|
-
|
|
3221
|
+
fs19.readFileSync(heartbeatPath, "utf-8")
|
|
2692
3222
|
);
|
|
2693
3223
|
} catch {
|
|
2694
3224
|
return null;
|
|
@@ -2700,12 +3230,12 @@ function loadRuntimeBridgeThreadState(bridgeState) {
|
|
|
2700
3230
|
return null;
|
|
2701
3231
|
}
|
|
2702
3232
|
const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
|
|
2703
|
-
if (!
|
|
3233
|
+
if (!fs19.existsSync(threadPath)) {
|
|
2704
3234
|
return null;
|
|
2705
3235
|
}
|
|
2706
3236
|
try {
|
|
2707
3237
|
const parsed = JSON.parse(
|
|
2708
|
-
|
|
3238
|
+
fs19.readFileSync(threadPath, "utf-8")
|
|
2709
3239
|
);
|
|
2710
3240
|
return parsed.threadId ? parsed : null;
|
|
2711
3241
|
} catch {
|
|
@@ -2714,9 +3244,9 @@ function loadRuntimeBridgeThreadState(bridgeState) {
|
|
|
2714
3244
|
}
|
|
2715
3245
|
function loadBridgeState(stateDir, instanceId) {
|
|
2716
3246
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
2717
|
-
if (!
|
|
3247
|
+
if (!fs19.existsSync(pidPath)) return null;
|
|
2718
3248
|
try {
|
|
2719
|
-
const raw =
|
|
3249
|
+
const raw = fs19.readFileSync(pidPath, "utf-8");
|
|
2720
3250
|
return JSON.parse(raw);
|
|
2721
3251
|
} catch {
|
|
2722
3252
|
return null;
|
|
@@ -2732,8 +3262,8 @@ function saveBridgeState(stateDir, instanceId, state) {
|
|
|
2732
3262
|
}
|
|
2733
3263
|
function clearBridgeState(stateDir, instanceId) {
|
|
2734
3264
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
2735
|
-
if (
|
|
2736
|
-
|
|
3265
|
+
if (fs19.existsSync(pidPath)) {
|
|
3266
|
+
fs19.unlinkSync(pidPath);
|
|
2737
3267
|
}
|
|
2738
3268
|
}
|
|
2739
3269
|
function isBridgeRunning(stateDir, instanceId) {
|
|
@@ -2743,7 +3273,7 @@ function isBridgeRunning(stateDir, instanceId) {
|
|
|
2743
3273
|
}
|
|
2744
3274
|
|
|
2745
3275
|
// src/engine/bridge-observability.ts
|
|
2746
|
-
import * as
|
|
3276
|
+
import * as fs20 from "fs";
|
|
2747
3277
|
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
2748
3278
|
const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
|
|
2749
3279
|
return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
|
|
@@ -2795,16 +3325,251 @@ function isTurnStuck(stateDir, instanceId, thresholdSeconds = 300) {
|
|
|
2795
3325
|
return info?.stuck ?? false;
|
|
2796
3326
|
}
|
|
2797
3327
|
function rotateLog(logPath) {
|
|
2798
|
-
if (!
|
|
3328
|
+
if (!fs20.existsSync(logPath)) return;
|
|
2799
3329
|
try {
|
|
2800
|
-
const stats =
|
|
3330
|
+
const stats = fs20.statSync(logPath);
|
|
2801
3331
|
if (stats.size === 0) return;
|
|
2802
3332
|
const prevPath = `${logPath}.prev`;
|
|
2803
|
-
|
|
3333
|
+
fs20.renameSync(logPath, prevPath);
|
|
2804
3334
|
} catch {
|
|
2805
3335
|
}
|
|
2806
3336
|
}
|
|
2807
3337
|
|
|
3338
|
+
// src/engine/server-lifecycle.ts
|
|
3339
|
+
function lifecycleMeta(persistedLifecycle) {
|
|
3340
|
+
return {
|
|
3341
|
+
lastTransitionAt: persistedLifecycle?.lastTransitionAt ?? null,
|
|
3342
|
+
lastTransitionReason: persistedLifecycle?.lastTransitionReason ?? null,
|
|
3343
|
+
restartCount: persistedLifecycle?.restartCount ?? 0
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
function resolveBridgeLifecycleSnapshot(stateDir, instanceId, fallbackBridgeState, persistedLifecycle) {
|
|
3347
|
+
const persistedBridgeState = loadBridgeState(stateDir, instanceId) ?? fallbackBridgeState ?? null;
|
|
3348
|
+
const bridgeStatus = getBridgeStatus(stateDir, instanceId);
|
|
3349
|
+
const bridgeState = bridgeStatus === "running" ? loadBridgeState(stateDir, instanceId) ?? persistedBridgeState : persistedBridgeState;
|
|
3350
|
+
return deriveBridgeLifecycleState({
|
|
3351
|
+
bridgeStatus,
|
|
3352
|
+
bridgeState,
|
|
3353
|
+
runtimeHeartbeat: loadRuntimeBridgeHeartbeat(bridgeState),
|
|
3354
|
+
savedThread: loadRuntimeBridgeThreadState(bridgeState),
|
|
3355
|
+
persistedLifecycle
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
function deriveBridgeLifecycleState(options) {
|
|
3359
|
+
const runtimeHeartbeat = options.runtimeHeartbeat ?? null;
|
|
3360
|
+
const savedThread = options.savedThread ?? null;
|
|
3361
|
+
const meta = lifecycleMeta(
|
|
3362
|
+
options.persistedLifecycle ?? options.bridgeState?.lifecycle ?? null
|
|
3363
|
+
);
|
|
3364
|
+
if (options.bridgeStatus === "stopped") {
|
|
3365
|
+
return {
|
|
3366
|
+
presence: "stopped",
|
|
3367
|
+
status: "stopped",
|
|
3368
|
+
summary: "stopped",
|
|
3369
|
+
...meta,
|
|
3370
|
+
threadId: null,
|
|
3371
|
+
threadCwd: null,
|
|
3372
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3373
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3374
|
+
activeTurnId: null,
|
|
3375
|
+
connected: null,
|
|
3376
|
+
initialized: null,
|
|
3377
|
+
appServerHealthy: options.bridgeState?.appServer?.healthy ?? null
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
if (options.bridgeStatus === "stale") {
|
|
3381
|
+
return {
|
|
3382
|
+
presence: "bridge-stale",
|
|
3383
|
+
status: "bridge-stale",
|
|
3384
|
+
summary: "bridge-stale",
|
|
3385
|
+
...meta,
|
|
3386
|
+
threadId: runtimeHeartbeat?.threadId ?? null,
|
|
3387
|
+
threadCwd: runtimeHeartbeat?.threadCwd ?? null,
|
|
3388
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3389
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3390
|
+
activeTurnId: runtimeHeartbeat?.activeTurnId ?? null,
|
|
3391
|
+
connected: runtimeHeartbeat?.connected ?? null,
|
|
3392
|
+
initialized: runtimeHeartbeat?.initialized ?? null,
|
|
3393
|
+
appServerHealthy: options.bridgeState?.appServer?.healthy ?? null
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
const appServerHealthy = options.bridgeState?.appServer?.healthy ?? null;
|
|
3397
|
+
const threadId = runtimeHeartbeat?.threadId ?? null;
|
|
3398
|
+
const threadCwd = runtimeHeartbeat?.threadCwd ?? null;
|
|
3399
|
+
const connected = runtimeHeartbeat?.connected ?? null;
|
|
3400
|
+
const initialized = runtimeHeartbeat?.initialized ?? null;
|
|
3401
|
+
if (!runtimeHeartbeat) {
|
|
3402
|
+
return {
|
|
3403
|
+
presence: "bridge-live",
|
|
3404
|
+
status: "initializing",
|
|
3405
|
+
summary: "bridge-live, initializing",
|
|
3406
|
+
...meta,
|
|
3407
|
+
threadId: null,
|
|
3408
|
+
threadCwd: null,
|
|
3409
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3410
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3411
|
+
activeTurnId: null,
|
|
3412
|
+
connected: null,
|
|
3413
|
+
initialized: null,
|
|
3414
|
+
appServerHealthy
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
if (initialized === false) {
|
|
3418
|
+
return {
|
|
3419
|
+
presence: "bridge-live",
|
|
3420
|
+
status: "initializing",
|
|
3421
|
+
summary: "bridge-live, initializing",
|
|
3422
|
+
...meta,
|
|
3423
|
+
threadId,
|
|
3424
|
+
threadCwd,
|
|
3425
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3426
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3427
|
+
activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
|
|
3428
|
+
connected,
|
|
3429
|
+
initialized,
|
|
3430
|
+
appServerHealthy
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
if (threadId && connected !== false) {
|
|
3434
|
+
return {
|
|
3435
|
+
presence: "bridge-live",
|
|
3436
|
+
status: "ready",
|
|
3437
|
+
summary: "bridge-live, ready",
|
|
3438
|
+
...meta,
|
|
3439
|
+
threadId,
|
|
3440
|
+
threadCwd,
|
|
3441
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3442
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3443
|
+
activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
|
|
3444
|
+
connected,
|
|
3445
|
+
initialized,
|
|
3446
|
+
appServerHealthy
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
const degradedReason = savedThread?.threadId ? "saved thread only" : connected === false ? "disconnected" : "no active thread";
|
|
3450
|
+
return {
|
|
3451
|
+
presence: "bridge-live",
|
|
3452
|
+
status: "degraded-no-thread",
|
|
3453
|
+
summary: `bridge-live, degraded-no-thread (${degradedReason})`,
|
|
3454
|
+
...meta,
|
|
3455
|
+
threadId,
|
|
3456
|
+
threadCwd,
|
|
3457
|
+
savedThreadId: savedThread?.threadId ?? null,
|
|
3458
|
+
savedThreadCwd: savedThread?.cwd ?? null,
|
|
3459
|
+
activeTurnId: runtimeHeartbeat.activeTurnId ?? null,
|
|
3460
|
+
connected,
|
|
3461
|
+
initialized,
|
|
3462
|
+
appServerHealthy
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// src/engine/codex-session-state.ts
|
|
3467
|
+
import * as fs21 from "fs";
|
|
3468
|
+
import * as path18 from "path";
|
|
3469
|
+
function readLastDispatchAt(runtimeStateDir) {
|
|
3470
|
+
if (!runtimeStateDir) return null;
|
|
3471
|
+
const filePath = path18.join(runtimeStateDir, "last-dispatch.json");
|
|
3472
|
+
if (!fs21.existsSync(filePath)) return null;
|
|
3473
|
+
try {
|
|
3474
|
+
const parsed = JSON.parse(
|
|
3475
|
+
fs21.readFileSync(filePath, "utf-8")
|
|
3476
|
+
);
|
|
3477
|
+
return typeof parsed.dispatchedAt === "string" ? parsed.dispatchedAt : null;
|
|
3478
|
+
} catch {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
function formatIdleSummary(idleSince) {
|
|
3483
|
+
if (!idleSince) return "idle";
|
|
3484
|
+
return `idle since ${idleSince}`;
|
|
3485
|
+
}
|
|
3486
|
+
function deriveCodexSessionState(options) {
|
|
3487
|
+
const runtimeHeartbeat = options.runtimeHeartbeat ?? null;
|
|
3488
|
+
if (!runtimeHeartbeat) {
|
|
3489
|
+
return {
|
|
3490
|
+
status: "initializing",
|
|
3491
|
+
turnState: null,
|
|
3492
|
+
summary: "initializing",
|
|
3493
|
+
activeTurnId: null,
|
|
3494
|
+
lastTurnAt: null,
|
|
3495
|
+
lastDispatchAt: null,
|
|
3496
|
+
idleSince: null,
|
|
3497
|
+
connected: null,
|
|
3498
|
+
initialized: null
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
const turnState = runtimeHeartbeat.turnState ?? null;
|
|
3502
|
+
const activeTurnId = runtimeHeartbeat.activeTurnId ?? null;
|
|
3503
|
+
const lastTurnAt = runtimeHeartbeat.lastTurnAt ?? null;
|
|
3504
|
+
const lastDispatchAt = runtimeHeartbeat.lastDispatchAt ?? readLastDispatchAt(options.runtimeStateDir) ?? null;
|
|
3505
|
+
const idleSince = runtimeHeartbeat.idleSince ?? null;
|
|
3506
|
+
const connected = runtimeHeartbeat.connected ?? null;
|
|
3507
|
+
const initialized = runtimeHeartbeat.initialized ?? null;
|
|
3508
|
+
if (initialized === false) {
|
|
3509
|
+
return {
|
|
3510
|
+
status: "initializing",
|
|
3511
|
+
turnState,
|
|
3512
|
+
summary: "initializing",
|
|
3513
|
+
activeTurnId,
|
|
3514
|
+
lastTurnAt,
|
|
3515
|
+
lastDispatchAt,
|
|
3516
|
+
idleSince,
|
|
3517
|
+
connected,
|
|
3518
|
+
initialized
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
if (turnState === "active" || activeTurnId) {
|
|
3522
|
+
return {
|
|
3523
|
+
status: "active",
|
|
3524
|
+
turnState: "active",
|
|
3525
|
+
summary: activeTurnId ? `active turn ${activeTurnId}` : "active",
|
|
3526
|
+
activeTurnId,
|
|
3527
|
+
lastTurnAt,
|
|
3528
|
+
lastDispatchAt,
|
|
3529
|
+
idleSince: null,
|
|
3530
|
+
connected,
|
|
3531
|
+
initialized
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
if (turnState === "waiting-approval") {
|
|
3535
|
+
return {
|
|
3536
|
+
status: "waiting-approval",
|
|
3537
|
+
turnState,
|
|
3538
|
+
summary: `waiting-approval (${formatIdleSummary(idleSince)})`,
|
|
3539
|
+
activeTurnId,
|
|
3540
|
+
lastTurnAt,
|
|
3541
|
+
lastDispatchAt,
|
|
3542
|
+
idleSince,
|
|
3543
|
+
connected,
|
|
3544
|
+
initialized
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
if (turnState === "disconnected" || connected === false) {
|
|
3548
|
+
return {
|
|
3549
|
+
status: "disconnected",
|
|
3550
|
+
turnState: "disconnected",
|
|
3551
|
+
summary: "disconnected",
|
|
3552
|
+
activeTurnId,
|
|
3553
|
+
lastTurnAt,
|
|
3554
|
+
lastDispatchAt,
|
|
3555
|
+
idleSince: null,
|
|
3556
|
+
connected,
|
|
3557
|
+
initialized
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
return {
|
|
3561
|
+
status: "idle",
|
|
3562
|
+
turnState: turnState === "idle" ? turnState : "idle",
|
|
3563
|
+
summary: formatIdleSummary(idleSince),
|
|
3564
|
+
activeTurnId,
|
|
3565
|
+
lastTurnAt,
|
|
3566
|
+
lastDispatchAt,
|
|
3567
|
+
idleSince,
|
|
3568
|
+
connected,
|
|
3569
|
+
initialized
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
|
|
2808
3573
|
// src/engine/bridge-app-server-health.ts
|
|
2809
3574
|
import * as net2 from "net";
|
|
2810
3575
|
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
@@ -2816,7 +3581,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
2816
3581
|
if (!WebSocket) {
|
|
2817
3582
|
return false;
|
|
2818
3583
|
}
|
|
2819
|
-
return new Promise((
|
|
3584
|
+
return new Promise((resolve15) => {
|
|
2820
3585
|
let settled = false;
|
|
2821
3586
|
let socket = null;
|
|
2822
3587
|
const finish = (healthy) => {
|
|
@@ -2829,7 +3594,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
2829
3594
|
socket?.close();
|
|
2830
3595
|
} catch {
|
|
2831
3596
|
}
|
|
2832
|
-
|
|
3597
|
+
resolve15(healthy);
|
|
2833
3598
|
};
|
|
2834
3599
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2835
3600
|
try {
|
|
@@ -2901,21 +3666,21 @@ async function checkTcpPortListening(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_
|
|
|
2901
3666
|
return false;
|
|
2902
3667
|
}
|
|
2903
3668
|
if (!port || !Number.isFinite(port)) return false;
|
|
2904
|
-
return new Promise((
|
|
3669
|
+
return new Promise((resolve15) => {
|
|
2905
3670
|
const socket = net2.createConnection({ host: hostname, port });
|
|
2906
3671
|
const timer = setTimeout(() => {
|
|
2907
3672
|
socket.destroy();
|
|
2908
|
-
|
|
3673
|
+
resolve15(false);
|
|
2909
3674
|
}, timeoutMs);
|
|
2910
3675
|
socket.once("connect", () => {
|
|
2911
3676
|
clearTimeout(timer);
|
|
2912
3677
|
socket.destroy();
|
|
2913
|
-
|
|
3678
|
+
resolve15(true);
|
|
2914
3679
|
});
|
|
2915
3680
|
socket.once("error", () => {
|
|
2916
3681
|
clearTimeout(timer);
|
|
2917
3682
|
socket.destroy();
|
|
2918
|
-
|
|
3683
|
+
resolve15(false);
|
|
2919
3684
|
});
|
|
2920
3685
|
});
|
|
2921
3686
|
}
|
|
@@ -2954,19 +3719,19 @@ function markAppServerHealthy(appServer) {
|
|
|
2954
3719
|
}
|
|
2955
3720
|
|
|
2956
3721
|
// src/engine/bridge-app-server-auth.ts
|
|
2957
|
-
import * as
|
|
2958
|
-
import * as
|
|
3722
|
+
import * as fs23 from "fs";
|
|
3723
|
+
import * as path20 from "path";
|
|
2959
3724
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2960
3725
|
|
|
2961
3726
|
// src/runtime/resolve-node.ts
|
|
2962
|
-
import * as
|
|
2963
|
-
import * as
|
|
3727
|
+
import * as fs22 from "fs";
|
|
3728
|
+
import * as path19 from "path";
|
|
2964
3729
|
import { execSync as execSync3 } from "child_process";
|
|
2965
3730
|
function readNodeVersion(repoRoot) {
|
|
2966
|
-
const nvFile =
|
|
2967
|
-
if (!
|
|
3731
|
+
const nvFile = path19.join(repoRoot, ".node-version");
|
|
3732
|
+
if (!fs22.existsSync(nvFile)) return null;
|
|
2968
3733
|
try {
|
|
2969
|
-
const raw =
|
|
3734
|
+
const raw = fs22.readFileSync(nvFile, "utf-8").trim();
|
|
2970
3735
|
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
2971
3736
|
} catch {
|
|
2972
3737
|
return null;
|
|
@@ -2976,16 +3741,16 @@ function fnmCandidateDirs() {
|
|
|
2976
3741
|
if (process.platform === "win32") {
|
|
2977
3742
|
return [
|
|
2978
3743
|
process.env.FNM_DIR,
|
|
2979
|
-
process.env.APPDATA ?
|
|
2980
|
-
process.env.LOCALAPPDATA ?
|
|
2981
|
-
process.env.USERPROFILE ?
|
|
3744
|
+
process.env.APPDATA ? path19.join(process.env.APPDATA, "fnm") : null,
|
|
3745
|
+
process.env.LOCALAPPDATA ? path19.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
3746
|
+
process.env.USERPROFILE ? path19.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
2982
3747
|
].filter(Boolean);
|
|
2983
3748
|
}
|
|
2984
3749
|
return [
|
|
2985
3750
|
process.env.FNM_DIR,
|
|
2986
|
-
process.env.HOME ?
|
|
2987
|
-
process.env.HOME ?
|
|
2988
|
-
process.env.XDG_DATA_HOME ?
|
|
3751
|
+
process.env.HOME ? path19.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
3752
|
+
process.env.HOME ? path19.join(process.env.HOME, ".fnm") : null,
|
|
3753
|
+
process.env.XDG_DATA_HOME ? path19.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
2989
3754
|
].filter(Boolean);
|
|
2990
3755
|
}
|
|
2991
3756
|
function nodeExecutableName() {
|
|
@@ -2995,14 +3760,14 @@ function probeFnmNode(desiredVersion) {
|
|
|
2995
3760
|
const dirs = fnmCandidateDirs();
|
|
2996
3761
|
const exe = nodeExecutableName();
|
|
2997
3762
|
for (const baseDir of dirs) {
|
|
2998
|
-
const candidate =
|
|
3763
|
+
const candidate = path19.join(
|
|
2999
3764
|
baseDir,
|
|
3000
3765
|
"node-versions",
|
|
3001
3766
|
`v${desiredVersion}`,
|
|
3002
3767
|
"installation",
|
|
3003
3768
|
exe
|
|
3004
3769
|
);
|
|
3005
|
-
if (!
|
|
3770
|
+
if (!fs22.existsSync(candidate)) continue;
|
|
3006
3771
|
try {
|
|
3007
3772
|
const v = execSync3(`"${candidate}" --version`, {
|
|
3008
3773
|
encoding: "utf-8",
|
|
@@ -3043,12 +3808,12 @@ function checkStripTypesSupport(command) {
|
|
|
3043
3808
|
}
|
|
3044
3809
|
function findTsxFallback(repoRoot) {
|
|
3045
3810
|
const candidates = [
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3811
|
+
path19.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
3812
|
+
path19.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
3813
|
+
path19.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
3049
3814
|
];
|
|
3050
3815
|
for (const c of candidates) {
|
|
3051
|
-
if (
|
|
3816
|
+
if (fs22.existsSync(c)) return c;
|
|
3052
3817
|
}
|
|
3053
3818
|
return null;
|
|
3054
3819
|
}
|
|
@@ -3057,7 +3822,7 @@ function getFnmBinDir(repoRoot) {
|
|
|
3057
3822
|
if (!desiredVersion) return null;
|
|
3058
3823
|
const nodePath = probeFnmNode(desiredVersion);
|
|
3059
3824
|
if (!nodePath) return null;
|
|
3060
|
-
return
|
|
3825
|
+
return path19.dirname(nodePath);
|
|
3061
3826
|
}
|
|
3062
3827
|
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
3063
3828
|
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
@@ -3113,7 +3878,7 @@ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
|
3113
3878
|
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
3114
3879
|
return {
|
|
3115
3880
|
...baseEnv,
|
|
3116
|
-
[pathKey]: `${fnmBin}${
|
|
3881
|
+
[pathKey]: `${fnmBin}${path19.delimiter}${currentPath}`
|
|
3117
3882
|
};
|
|
3118
3883
|
}
|
|
3119
3884
|
|
|
@@ -3122,7 +3887,7 @@ function buildProtectedAppServerUrl(publicUrl, _token) {
|
|
|
3122
3887
|
return publicUrl;
|
|
3123
3888
|
}
|
|
3124
3889
|
function readGatewayTokenFromPath(tokenPath) {
|
|
3125
|
-
return
|
|
3890
|
+
return fs23.readFileSync(tokenPath, "utf8").trim();
|
|
3126
3891
|
}
|
|
3127
3892
|
function readGatewayToken(auth) {
|
|
3128
3893
|
if (!auth) {
|
|
@@ -3132,14 +3897,14 @@ function readGatewayToken(auth) {
|
|
|
3132
3897
|
if (legacyToken?.trim()) {
|
|
3133
3898
|
return legacyToken.trim();
|
|
3134
3899
|
}
|
|
3135
|
-
if (!auth.tokenPath || !
|
|
3900
|
+
if (!auth.tokenPath || !fs23.existsSync(auth.tokenPath)) {
|
|
3136
3901
|
return null;
|
|
3137
3902
|
}
|
|
3138
3903
|
const fileToken = readGatewayTokenFromPath(auth.tokenPath);
|
|
3139
3904
|
return fileToken || null;
|
|
3140
3905
|
}
|
|
3141
3906
|
function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
|
|
3142
|
-
if (auth.tokenPath &&
|
|
3907
|
+
if (auth.tokenPath && fs23.existsSync(auth.tokenPath)) {
|
|
3143
3908
|
return auth;
|
|
3144
3909
|
}
|
|
3145
3910
|
const token = readGatewayToken(auth);
|
|
@@ -3177,7 +3942,7 @@ async function createManagedAppServerAuth(options) {
|
|
|
3177
3942
|
options.stateDir,
|
|
3178
3943
|
options.instanceId
|
|
3179
3944
|
);
|
|
3180
|
-
|
|
3945
|
+
fs23.mkdirSync(path20.dirname(gatewayLogPath), { recursive: true });
|
|
3181
3946
|
rotateLog(gatewayLogPath);
|
|
3182
3947
|
const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
|
|
3183
3948
|
const gatewayArgs = [];
|
|
@@ -3252,20 +4017,20 @@ function canReuseManagedAppServer(appServer) {
|
|
|
3252
4017
|
}
|
|
3253
4018
|
|
|
3254
4019
|
// src/engine/bridge-app-server-lifecycle.ts
|
|
3255
|
-
import * as
|
|
3256
|
-
import * as
|
|
4020
|
+
import * as fs24 from "fs";
|
|
4021
|
+
import * as path21 from "path";
|
|
3257
4022
|
var DEFAULT_APP_SERVER_URL3 = "ws://127.0.0.1:4501";
|
|
3258
4023
|
var APP_SERVER_START_TIMEOUT_MS = 2e4;
|
|
3259
4024
|
var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
|
|
3260
4025
|
function isAppServerUsedByOtherBridge(stateDir, excludeInstanceId, appServer) {
|
|
3261
|
-
const pidDir =
|
|
3262
|
-
if (!
|
|
3263
|
-
for (const name of
|
|
4026
|
+
const pidDir = path21.join(stateDir, "pids");
|
|
4027
|
+
if (!fs24.existsSync(pidDir)) return false;
|
|
4028
|
+
for (const name of fs24.readdirSync(pidDir)) {
|
|
3264
4029
|
if (!name.startsWith("bridge-") || !name.endsWith(".json")) continue;
|
|
3265
4030
|
const otherId = name.slice("bridge-".length, -".json".length);
|
|
3266
4031
|
if (otherId === excludeInstanceId) continue;
|
|
3267
4032
|
try {
|
|
3268
|
-
const raw =
|
|
4033
|
+
const raw = fs24.readFileSync(path21.join(pidDir, name), "utf-8");
|
|
3269
4034
|
const state = JSON.parse(raw);
|
|
3270
4035
|
if (state.appServer?.url === appServer.url && state.appServer?.pid === appServer.pid && isProcessAlive(state.pid)) {
|
|
3271
4036
|
return true;
|
|
@@ -3277,16 +4042,16 @@ function isAppServerUsedByOtherBridge(stateDir, excludeInstanceId, appServer) {
|
|
|
3277
4042
|
return false;
|
|
3278
4043
|
}
|
|
3279
4044
|
function findReusableManagedAppServer(stateDir, publicUrl) {
|
|
3280
|
-
const pidDir =
|
|
3281
|
-
if (!
|
|
4045
|
+
const pidDir = path21.join(stateDir, "pids");
|
|
4046
|
+
if (!fs24.existsSync(pidDir)) {
|
|
3282
4047
|
return null;
|
|
3283
4048
|
}
|
|
3284
|
-
for (const name of
|
|
4049
|
+
for (const name of fs24.readdirSync(pidDir)) {
|
|
3285
4050
|
if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
|
|
3286
4051
|
continue;
|
|
3287
4052
|
}
|
|
3288
4053
|
try {
|
|
3289
|
-
const raw =
|
|
4054
|
+
const raw = fs24.readFileSync(path21.join(pidDir, name), "utf-8");
|
|
3290
4055
|
const parsed = JSON.parse(raw);
|
|
3291
4056
|
if (parsed.appServer?.url !== publicUrl) {
|
|
3292
4057
|
continue;
|
|
@@ -3358,7 +4123,7 @@ Start the app-server manually:
|
|
|
3358
4123
|
);
|
|
3359
4124
|
}
|
|
3360
4125
|
const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
|
|
3361
|
-
|
|
4126
|
+
fs24.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
3362
4127
|
rotateLog(logPath);
|
|
3363
4128
|
if (options.noAuth) {
|
|
3364
4129
|
const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
|
|
@@ -3553,10 +4318,82 @@ function formatCodexAppServerCommand(command, url) {
|
|
|
3553
4318
|
}
|
|
3554
4319
|
|
|
3555
4320
|
// src/engine/bridge-startup.ts
|
|
3556
|
-
import * as
|
|
3557
|
-
import * as
|
|
4321
|
+
import * as fs25 from "fs";
|
|
4322
|
+
import * as path22 from "path";
|
|
3558
4323
|
function getBridgeRuntimeStateDir(repoRoot, instanceId) {
|
|
3559
|
-
|
|
4324
|
+
const resolved = path22.resolve(
|
|
4325
|
+
path22.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`)
|
|
4326
|
+
);
|
|
4327
|
+
const expectedBase = path22.resolve(repoRoot, ".tmp") + path22.sep;
|
|
4328
|
+
if (!resolved.startsWith(expectedBase)) {
|
|
4329
|
+
throw new Error(
|
|
4330
|
+
`Path traversal blocked: runtime state dir escapes .tmp/ directory`
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
return resolved;
|
|
4334
|
+
}
|
|
4335
|
+
var STALE_DIRECT_HEARTBEAT_MS = 5 * 60 * 1e3;
|
|
4336
|
+
function warnHeartbeatCleanup(instanceId, message) {
|
|
4337
|
+
console.warn(
|
|
4338
|
+
`[tap] heartbeat cleanup skipped for ${instanceId}: ${message}`
|
|
4339
|
+
);
|
|
4340
|
+
}
|
|
4341
|
+
function getHeartbeatActivityMs(record) {
|
|
4342
|
+
const timestamp = new Date(record.lastActivity ?? record.timestamp ?? 0).getTime();
|
|
4343
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
4344
|
+
}
|
|
4345
|
+
function isSameInstanceHeartbeat(key, heartbeat, instanceId) {
|
|
4346
|
+
if (heartbeat.instanceId === instanceId) return true;
|
|
4347
|
+
if (heartbeat.connectHash === `instance:${instanceId}`) return true;
|
|
4348
|
+
return key === instanceId || key.replace(/_/g, "-") === instanceId || key.replace(/-/g, "_") === instanceId;
|
|
4349
|
+
}
|
|
4350
|
+
function cleanupStaleSameInstanceHeartbeats(commsDir, instanceId) {
|
|
4351
|
+
const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
|
|
4352
|
+
if (!fs25.existsSync(heartbeatsPath)) return;
|
|
4353
|
+
const lockPath = path22.join(commsDir, ".heartbeats.lock");
|
|
4354
|
+
try {
|
|
4355
|
+
fs25.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
4356
|
+
} catch {
|
|
4357
|
+
warnHeartbeatCleanup(instanceId, "heartbeat store busy");
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
try {
|
|
4361
|
+
let store = {};
|
|
4362
|
+
try {
|
|
4363
|
+
store = JSON.parse(
|
|
4364
|
+
fs25.readFileSync(heartbeatsPath, "utf-8")
|
|
4365
|
+
);
|
|
4366
|
+
} catch {
|
|
4367
|
+
warnHeartbeatCleanup(instanceId, "heartbeat store unreadable");
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
let changed = false;
|
|
4371
|
+
for (const [key, heartbeat] of Object.entries(store)) {
|
|
4372
|
+
if (!isSameInstanceHeartbeat(key, heartbeat, instanceId)) continue;
|
|
4373
|
+
const status = heartbeat.status ?? "active";
|
|
4374
|
+
const isDeadBridge = heartbeat.source === "bridge-dispatch" && heartbeat.bridgePid != null && !isProcessAlive(heartbeat.bridgePid);
|
|
4375
|
+
const activityMs = getHeartbeatActivityMs(heartbeat);
|
|
4376
|
+
const isStaleDirect = heartbeat.source !== "bridge-dispatch" && activityMs != null && Date.now() - activityMs > STALE_DIRECT_HEARTBEAT_MS;
|
|
4377
|
+
if (status === "signing-off" || isDeadBridge || isStaleDirect) {
|
|
4378
|
+
delete store[key];
|
|
4379
|
+
changed = true;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
if (!changed) return;
|
|
4383
|
+
const tmpPath = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
4384
|
+
fs25.writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
4385
|
+
fs25.renameSync(tmpPath, heartbeatsPath);
|
|
4386
|
+
} catch (error) {
|
|
4387
|
+
warnHeartbeatCleanup(
|
|
4388
|
+
instanceId,
|
|
4389
|
+
error instanceof Error ? error.message : String(error)
|
|
4390
|
+
);
|
|
4391
|
+
} finally {
|
|
4392
|
+
try {
|
|
4393
|
+
fs25.unlinkSync(lockPath);
|
|
4394
|
+
} catch {
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
3560
4397
|
}
|
|
3561
4398
|
async function startBridge(options) {
|
|
3562
4399
|
const {
|
|
@@ -3584,12 +4421,14 @@ async function startBridge(options) {
|
|
|
3584
4421
|
);
|
|
3585
4422
|
}
|
|
3586
4423
|
const previousBridgeState = loadBridgeState(stateDir, instanceId);
|
|
4424
|
+
const previousLifecycle = options.previousLifecycle ?? previousBridgeState?.lifecycle ?? null;
|
|
3587
4425
|
const previousAppServer = previousBridgeState?.appServer ?? null;
|
|
3588
4426
|
clearBridgeState(stateDir, instanceId);
|
|
4427
|
+
cleanupStaleSameInstanceHeartbeats(commsDir, instanceId);
|
|
3589
4428
|
const logPath = logFilePath(stateDir, instanceId);
|
|
3590
|
-
|
|
4429
|
+
fs25.mkdirSync(path22.dirname(logPath), { recursive: true });
|
|
3591
4430
|
rotateLog(logPath);
|
|
3592
|
-
const repoRoot = options.repoRoot ??
|
|
4431
|
+
const repoRoot = options.repoRoot ?? path22.resolve(stateDir, "..");
|
|
3593
4432
|
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3594
4433
|
const resolved = resolveNodeRuntime(
|
|
3595
4434
|
options.runtimeCommand ?? "node",
|
|
@@ -3600,6 +4439,7 @@ async function startBridge(options) {
|
|
|
3600
4439
|
const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
|
|
3601
4440
|
let appServer = null;
|
|
3602
4441
|
let bridgeAppServerUrl = effectiveAppServerUrl;
|
|
4442
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3603
4443
|
if (runtime === "codex" && options.manageAppServer) {
|
|
3604
4444
|
appServer = await ensureCodexAppServer({
|
|
3605
4445
|
instanceId,
|
|
@@ -3677,9 +4517,18 @@ async function startBridge(options) {
|
|
|
3677
4517
|
const state = {
|
|
3678
4518
|
pid: bridgePid,
|
|
3679
4519
|
statePath: pidFilePath(stateDir, instanceId),
|
|
3680
|
-
lastHeartbeat:
|
|
4520
|
+
lastHeartbeat: startedAt,
|
|
3681
4521
|
appServer,
|
|
3682
|
-
runtimeStateDir
|
|
4522
|
+
runtimeStateDir,
|
|
4523
|
+
lifecycle: transitionBridgeLifecycle(
|
|
4524
|
+
previousLifecycle,
|
|
4525
|
+
"initializing",
|
|
4526
|
+
previousLifecycle ? "bridge restart" : "bridge start",
|
|
4527
|
+
{
|
|
4528
|
+
at: startedAt,
|
|
4529
|
+
incrementRestart: previousLifecycle != null
|
|
4530
|
+
}
|
|
4531
|
+
)
|
|
3683
4532
|
};
|
|
3684
4533
|
saveBridgeState(stateDir, instanceId, state);
|
|
3685
4534
|
return state;
|
|
@@ -3699,55 +4548,82 @@ async function startBridge(options) {
|
|
|
3699
4548
|
}
|
|
3700
4549
|
|
|
3701
4550
|
// src/engine/bridge-orchestrator.ts
|
|
3702
|
-
import * as
|
|
3703
|
-
import * as
|
|
4551
|
+
import * as fs26 from "fs";
|
|
4552
|
+
import * as path23 from "path";
|
|
3704
4553
|
async function stopBridge(options) {
|
|
3705
4554
|
const { instanceId, stateDir, platform } = options;
|
|
3706
4555
|
const state = loadBridgeState(stateDir, instanceId);
|
|
3707
4556
|
if (!state) {
|
|
3708
|
-
return
|
|
4557
|
+
return {
|
|
4558
|
+
stopped: false,
|
|
4559
|
+
lifecycle: null
|
|
4560
|
+
};
|
|
3709
4561
|
}
|
|
4562
|
+
const currentLifecycle = state.lifecycle ?? null;
|
|
3710
4563
|
if (!isProcessAlive(state.pid)) {
|
|
3711
4564
|
clearBridgeState(stateDir, instanceId);
|
|
3712
|
-
return
|
|
4565
|
+
return {
|
|
4566
|
+
stopped: false,
|
|
4567
|
+
lifecycle: transitionBridgeLifecycle(
|
|
4568
|
+
currentLifecycle,
|
|
4569
|
+
"crashed",
|
|
4570
|
+
"bridge pid not alive"
|
|
4571
|
+
)
|
|
4572
|
+
};
|
|
3713
4573
|
}
|
|
4574
|
+
state.lifecycle = transitionBridgeLifecycle(
|
|
4575
|
+
currentLifecycle,
|
|
4576
|
+
"stopping",
|
|
4577
|
+
"bridge stop requested"
|
|
4578
|
+
);
|
|
4579
|
+
saveBridgeState(stateDir, instanceId, state);
|
|
3714
4580
|
try {
|
|
3715
4581
|
await terminateProcess(state.pid, platform);
|
|
3716
4582
|
} catch {
|
|
3717
4583
|
}
|
|
3718
4584
|
clearBridgeState(stateDir, instanceId);
|
|
3719
|
-
return
|
|
4585
|
+
return {
|
|
4586
|
+
stopped: true,
|
|
4587
|
+
lifecycle: transitionBridgeLifecycle(
|
|
4588
|
+
state.lifecycle ?? currentLifecycle,
|
|
4589
|
+
"stopped",
|
|
4590
|
+
"bridge stopped"
|
|
4591
|
+
)
|
|
4592
|
+
};
|
|
3720
4593
|
}
|
|
3721
4594
|
async function restartBridge(options) {
|
|
3722
4595
|
const { instanceId, stateDir, platform } = options;
|
|
3723
4596
|
const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
|
|
3724
4597
|
const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
|
|
3725
4598
|
const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
|
|
3726
|
-
const heartbeatPath =
|
|
3727
|
-
if (
|
|
4599
|
+
const heartbeatPath = path23.join(runtimeStateDir, "heartbeat.json");
|
|
4600
|
+
if (fs26.existsSync(heartbeatPath)) {
|
|
3728
4601
|
const startWait = Date.now();
|
|
3729
4602
|
while (Date.now() - startWait < drainTimeout) {
|
|
3730
4603
|
try {
|
|
3731
|
-
const hb = JSON.parse(
|
|
4604
|
+
const hb = JSON.parse(fs26.readFileSync(heartbeatPath, "utf-8"));
|
|
3732
4605
|
if (!hb.activeTurnId) break;
|
|
3733
4606
|
} catch {
|
|
3734
4607
|
break;
|
|
3735
4608
|
}
|
|
3736
|
-
await new Promise((
|
|
4609
|
+
await new Promise((resolve15) => setTimeout(resolve15, 1e3));
|
|
3737
4610
|
}
|
|
3738
4611
|
}
|
|
3739
4612
|
if (options.headless?.enabled && options.commsDir) {
|
|
3740
4613
|
const agentName = options.agentName ?? instanceId;
|
|
3741
|
-
cleanupHeadlessDispatch(
|
|
4614
|
+
cleanupHeadlessDispatch(path23.join(options.commsDir, "inbox"), agentName);
|
|
3742
4615
|
}
|
|
3743
|
-
await stopBridge({ instanceId, stateDir, platform });
|
|
4616
|
+
const stopResult = await stopBridge({ instanceId, stateDir, platform });
|
|
3744
4617
|
return startBridge({
|
|
3745
4618
|
...options,
|
|
3746
|
-
processExistingMessages: true
|
|
4619
|
+
processExistingMessages: true,
|
|
4620
|
+
previousLifecycle: stopResult.lifecycle ?? options.previousLifecycle ?? null
|
|
3747
4621
|
});
|
|
3748
4622
|
}
|
|
3749
4623
|
|
|
3750
4624
|
// src/commands/add.ts
|
|
4625
|
+
init_config();
|
|
4626
|
+
init_instance_config();
|
|
3751
4627
|
var ADD_HELP = `
|
|
3752
4628
|
Usage:
|
|
3753
4629
|
tap add <claude|codex|gemini> [options]
|
|
@@ -3900,6 +4776,22 @@ async function addCommand(args) {
|
|
|
3900
4776
|
agentName: resolvedAgentName
|
|
3901
4777
|
});
|
|
3902
4778
|
saveState(repoRoot, updatedState);
|
|
4779
|
+
const { config: cfg } = resolveConfig({}, repoRoot);
|
|
4780
|
+
try {
|
|
4781
|
+
const {
|
|
4782
|
+
loadInstanceConfig: loadInstCfg,
|
|
4783
|
+
updateInstanceConfig: updateInstCfg,
|
|
4784
|
+
saveInstanceConfig: saveInstCfg
|
|
4785
|
+
} = await Promise.resolve().then(() => (init_instance_config(), instance_config_exports));
|
|
4786
|
+
const existing = loadInstCfg(cfg.stateDir, instanceId);
|
|
4787
|
+
if (existing) {
|
|
4788
|
+
const updated = updateInstCfg(existing, {
|
|
4789
|
+
agentName: resolvedAgentName
|
|
4790
|
+
});
|
|
4791
|
+
saveInstCfg(cfg.stateDir, updated);
|
|
4792
|
+
}
|
|
4793
|
+
} catch {
|
|
4794
|
+
}
|
|
3903
4795
|
return {
|
|
3904
4796
|
ok: true,
|
|
3905
4797
|
command: "add",
|
|
@@ -4027,6 +4919,7 @@ async function addCommand(args) {
|
|
|
4027
4919
|
"Verification had failures. Runtime may need manual configuration."
|
|
4028
4920
|
);
|
|
4029
4921
|
}
|
|
4922
|
+
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
4030
4923
|
let bridge = null;
|
|
4031
4924
|
let effectivePort = port;
|
|
4032
4925
|
if (mode === "app-server") {
|
|
@@ -4035,9 +4928,8 @@ async function addCommand(args) {
|
|
|
4035
4928
|
logWarn("Bridge script not found. Bridge not started.");
|
|
4036
4929
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
4037
4930
|
} else {
|
|
4038
|
-
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
4039
4931
|
if (effectivePort == null && runtime === "codex") {
|
|
4040
|
-
const currentState = loadState(repoRoot);
|
|
4932
|
+
const currentState = loadState(repoRoot) ?? state;
|
|
4041
4933
|
effectivePort = await findNextAvailableAppServerPort(
|
|
4042
4934
|
currentState,
|
|
4043
4935
|
resolvedCfg.appServerUrl,
|
|
@@ -4072,6 +4964,41 @@ async function addCommand(args) {
|
|
|
4072
4964
|
}
|
|
4073
4965
|
}
|
|
4074
4966
|
}
|
|
4967
|
+
let effectiveAppServerUrl = resolvedCfg.appServerUrl;
|
|
4968
|
+
if (effectivePort != null) {
|
|
4969
|
+
try {
|
|
4970
|
+
const parsed = new URL(resolvedCfg.appServerUrl);
|
|
4971
|
+
parsed.port = String(effectivePort);
|
|
4972
|
+
effectiveAppServerUrl = parsed.toString().replace(/\/$/, "");
|
|
4973
|
+
} catch {
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
const permRole = roleArg === "reviewer" ? "reviewer" : headlessFlag ? "implementer" : void 0;
|
|
4977
|
+
const instConfig = createInstanceConfig({
|
|
4978
|
+
instanceId,
|
|
4979
|
+
runtime,
|
|
4980
|
+
agentName: resolvedAgentName,
|
|
4981
|
+
agentId: null,
|
|
4982
|
+
port: effectivePort,
|
|
4983
|
+
appServerUrl: effectiveAppServerUrl,
|
|
4984
|
+
commsDir: ctx.commsDir,
|
|
4985
|
+
stateDir: ctx.stateDir,
|
|
4986
|
+
repoRoot,
|
|
4987
|
+
role: permRole
|
|
4988
|
+
});
|
|
4989
|
+
if (probe.configPath) {
|
|
4990
|
+
try {
|
|
4991
|
+
const { computeFileHash: computeFileHash2 } = await Promise.resolve().then(() => (init_drift_detector(), drift_detector_exports));
|
|
4992
|
+
const runtimeHash = computeFileHash2(probe.configPath);
|
|
4993
|
+
if (runtimeHash) {
|
|
4994
|
+
instConfig.runtimeConfigHash = runtimeHash;
|
|
4995
|
+
instConfig.lastSyncedToRuntime = (/* @__PURE__ */ new Date()).toISOString();
|
|
4996
|
+
}
|
|
4997
|
+
} catch {
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
const instConfigPath = saveInstanceConfig(ctx.stateDir, instConfig);
|
|
5001
|
+
logSuccess(`Instance config: ${instConfigPath}`);
|
|
4075
5002
|
const instanceState = {
|
|
4076
5003
|
instanceId,
|
|
4077
5004
|
runtime,
|
|
@@ -4089,6 +5016,8 @@ async function addCommand(args) {
|
|
|
4089
5016
|
manageAppServer: runtime === "codex",
|
|
4090
5017
|
noAuth: false,
|
|
4091
5018
|
headless,
|
|
5019
|
+
configHash: instConfig.configHash,
|
|
5020
|
+
configSourceFile: instConfigPath,
|
|
4092
5021
|
warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
|
|
4093
5022
|
};
|
|
4094
5023
|
const newState = updateInstanceState(state, instanceId, instanceState);
|
|
@@ -4123,6 +5052,8 @@ async function addCommand(args) {
|
|
|
4123
5052
|
}
|
|
4124
5053
|
|
|
4125
5054
|
// src/commands/status.ts
|
|
5055
|
+
init_config();
|
|
5056
|
+
init_utils();
|
|
4126
5057
|
var STATUS_HELP = `
|
|
4127
5058
|
Usage:
|
|
4128
5059
|
tap status
|
|
@@ -4134,30 +5065,71 @@ Examples:
|
|
|
4134
5065
|
npx @hua-labs/tap status
|
|
4135
5066
|
`.trim();
|
|
4136
5067
|
function resolveStatus(inst, stateDir) {
|
|
4137
|
-
if (!inst.installed)
|
|
5068
|
+
if (!inst.installed) {
|
|
5069
|
+
return {
|
|
5070
|
+
status: "not installed",
|
|
5071
|
+
lifecycle: null,
|
|
5072
|
+
session: null
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
4138
5075
|
switch (inst.bridgeMode) {
|
|
4139
5076
|
case "native-push":
|
|
4140
5077
|
case "polling":
|
|
4141
|
-
return
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
}
|
|
5078
|
+
return {
|
|
5079
|
+
status: inst.lastVerifiedAt ? "active" : "configured",
|
|
5080
|
+
lifecycle: null,
|
|
5081
|
+
session: null
|
|
5082
|
+
};
|
|
5083
|
+
case "app-server": {
|
|
4146
5084
|
if (inst.bridge) {
|
|
4147
|
-
|
|
5085
|
+
const lifecycle = resolveBridgeLifecycleSnapshot(
|
|
5086
|
+
stateDir,
|
|
5087
|
+
inst.instanceId,
|
|
5088
|
+
inst.bridge
|
|
5089
|
+
);
|
|
5090
|
+
if (lifecycle.status === "bridge-stale") {
|
|
5091
|
+
inst.bridge = null;
|
|
5092
|
+
return {
|
|
5093
|
+
status: inst.lastVerifiedAt ? "configured" : "installed",
|
|
5094
|
+
lifecycle,
|
|
5095
|
+
session: null
|
|
5096
|
+
};
|
|
5097
|
+
}
|
|
5098
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(inst.bridge);
|
|
5099
|
+
return {
|
|
5100
|
+
status: "active",
|
|
5101
|
+
lifecycle,
|
|
5102
|
+
session: deriveCodexSessionState({
|
|
5103
|
+
runtimeHeartbeat,
|
|
5104
|
+
runtimeStateDir: inst.bridge.runtimeStateDir ?? null
|
|
5105
|
+
})
|
|
5106
|
+
};
|
|
4148
5107
|
}
|
|
4149
|
-
return
|
|
5108
|
+
return {
|
|
5109
|
+
status: inst.lastVerifiedAt ? "configured" : "installed",
|
|
5110
|
+
lifecycle: deriveBridgeLifecycleState({
|
|
5111
|
+
bridgeStatus: "stopped"
|
|
5112
|
+
}),
|
|
5113
|
+
session: deriveCodexSessionState({ runtimeHeartbeat: null })
|
|
5114
|
+
};
|
|
5115
|
+
}
|
|
4150
5116
|
default:
|
|
4151
|
-
return
|
|
5117
|
+
return {
|
|
5118
|
+
status: "installed",
|
|
5119
|
+
lifecycle: null,
|
|
5120
|
+
session: null
|
|
5121
|
+
};
|
|
4152
5122
|
}
|
|
4153
5123
|
}
|
|
4154
|
-
function instanceStatusLine(inst, status) {
|
|
5124
|
+
function instanceStatusLine(inst, status, lifecycle, session) {
|
|
4155
5125
|
const bridgeInfo = inst.bridge ? ` (pid: ${inst.bridge.pid})` : "";
|
|
5126
|
+
const lifecycleStr = lifecycle?.status ?? "-";
|
|
5127
|
+
const sessionStr = session?.status ?? "-";
|
|
4156
5128
|
const mode = inst.bridgeMode;
|
|
4157
5129
|
const portStr = inst.port ? ` port:${inst.port}` : "";
|
|
4158
5130
|
const restart = inst.restartRequired ? " [restart required]" : "";
|
|
4159
5131
|
const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
|
|
4160
|
-
return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
|
|
5132
|
+
return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${lifecycleStr.padEnd(20)} ${sessionStr.padEnd(18)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
|
|
4161
5133
|
}
|
|
4162
5134
|
async function statusCommand(args) {
|
|
4163
5135
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -4201,16 +5173,16 @@ async function statusCommand(args) {
|
|
|
4201
5173
|
} else {
|
|
4202
5174
|
log("");
|
|
4203
5175
|
log(
|
|
4204
|
-
`${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(14)} ${"Bridge Mode".padEnd(14)} Details`
|
|
5176
|
+
`${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(14)} ${"Lifecycle".padEnd(20)} ${"Session".padEnd(18)} ${"Bridge Mode".padEnd(14)} Details`
|
|
4205
5177
|
);
|
|
4206
5178
|
log(
|
|
4207
|
-
`${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(14)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)}`
|
|
5179
|
+
`${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(14)} ${"\u2500".repeat(20)}`
|
|
4208
5180
|
);
|
|
4209
5181
|
for (const id of installed) {
|
|
4210
5182
|
const inst = state.instances[id];
|
|
4211
5183
|
if (inst) {
|
|
4212
|
-
const status = resolveStatus(inst, stateDir);
|
|
4213
|
-
log(instanceStatusLine(inst, status));
|
|
5184
|
+
const { status, lifecycle, session } = resolveStatus(inst, stateDir);
|
|
5185
|
+
log(instanceStatusLine(inst, status, lifecycle, session));
|
|
4214
5186
|
if (inst.warnings.length > 0) {
|
|
4215
5187
|
for (const w of inst.warnings) {
|
|
4216
5188
|
logWarn(` ${w}`);
|
|
@@ -4218,6 +5190,8 @@ async function statusCommand(args) {
|
|
|
4218
5190
|
}
|
|
4219
5191
|
instances[id] = {
|
|
4220
5192
|
status,
|
|
5193
|
+
lifecycle,
|
|
5194
|
+
session,
|
|
4221
5195
|
runtime: inst.runtime,
|
|
4222
5196
|
bridgeMode: inst.bridgeMode,
|
|
4223
5197
|
bridge: inst.bridge,
|
|
@@ -4249,8 +5223,11 @@ async function statusCommand(args) {
|
|
|
4249
5223
|
};
|
|
4250
5224
|
}
|
|
4251
5225
|
|
|
5226
|
+
// src/commands/remove.ts
|
|
5227
|
+
init_utils();
|
|
5228
|
+
|
|
4252
5229
|
// src/engine/rollback.ts
|
|
4253
|
-
import * as
|
|
5230
|
+
import * as fs27 from "fs";
|
|
4254
5231
|
async function rollbackRuntime(_instanceId, runtimeState) {
|
|
4255
5232
|
const errors = [];
|
|
4256
5233
|
const restoredFiles = [];
|
|
@@ -4279,7 +5256,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
|
|
|
4279
5256
|
};
|
|
4280
5257
|
}
|
|
4281
5258
|
function rollbackArtifact(artifact) {
|
|
4282
|
-
if (!
|
|
5259
|
+
if (!fs27.existsSync(artifact.path)) {
|
|
4283
5260
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
4284
5261
|
}
|
|
4285
5262
|
switch (artifact.kind) {
|
|
@@ -4297,7 +5274,7 @@ function rollbackArtifact(artifact) {
|
|
|
4297
5274
|
}
|
|
4298
5275
|
}
|
|
4299
5276
|
function rollbackJsonPath(artifact) {
|
|
4300
|
-
const raw =
|
|
5277
|
+
const raw = fs27.readFileSync(artifact.path, "utf-8");
|
|
4301
5278
|
let config;
|
|
4302
5279
|
try {
|
|
4303
5280
|
config = JSON.parse(raw);
|
|
@@ -4323,18 +5300,18 @@ function rollbackJsonPath(artifact) {
|
|
|
4323
5300
|
cleanEmptyParents(config, artifact.selector);
|
|
4324
5301
|
}
|
|
4325
5302
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
4326
|
-
|
|
4327
|
-
|
|
5303
|
+
fs27.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5304
|
+
fs27.renameSync(tmp, artifact.path);
|
|
4328
5305
|
return { restored: true };
|
|
4329
5306
|
}
|
|
4330
5307
|
function rollbackTomlTable(artifact) {
|
|
4331
|
-
const content =
|
|
5308
|
+
const content = fs27.readFileSync(artifact.path, "utf-8");
|
|
4332
5309
|
const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
|
|
4333
5310
|
if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
|
|
4334
5311
|
const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
|
|
4335
5312
|
const tmp2 = `${artifact.path}.tmp.${process.pid}`;
|
|
4336
|
-
|
|
4337
|
-
|
|
5313
|
+
fs27.writeFileSync(tmp2, nextContent, "utf-8");
|
|
5314
|
+
fs27.renameSync(tmp2, artifact.path);
|
|
4338
5315
|
return { restored: true };
|
|
4339
5316
|
}
|
|
4340
5317
|
if (!extractTomlTable(content, artifact.selector)) {
|
|
@@ -4344,13 +5321,13 @@ function rollbackTomlTable(artifact) {
|
|
|
4344
5321
|
};
|
|
4345
5322
|
}
|
|
4346
5323
|
const tmp = `${artifact.path}.tmp.${process.pid}`;
|
|
4347
|
-
|
|
4348
|
-
|
|
5324
|
+
fs27.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
|
|
5325
|
+
fs27.renameSync(tmp, artifact.path);
|
|
4349
5326
|
return { restored: true };
|
|
4350
5327
|
}
|
|
4351
5328
|
function rollbackFile(artifact) {
|
|
4352
|
-
if (
|
|
4353
|
-
|
|
5329
|
+
if (fs27.existsSync(artifact.path)) {
|
|
5330
|
+
fs27.unlinkSync(artifact.path);
|
|
4354
5331
|
return { restored: true };
|
|
4355
5332
|
}
|
|
4356
5333
|
return { restored: false, error: `File not found: ${artifact.path}` };
|
|
@@ -4475,12 +5452,12 @@ async function removeCommand(args) {
|
|
|
4475
5452
|
logHeader(`@hua-labs/tap remove ${instanceId}`);
|
|
4476
5453
|
if (instance.bridge) {
|
|
4477
5454
|
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4478
|
-
const
|
|
5455
|
+
const stopResult = await stopBridge({
|
|
4479
5456
|
instanceId,
|
|
4480
5457
|
stateDir: ctx.stateDir,
|
|
4481
5458
|
platform: ctx.platform
|
|
4482
5459
|
});
|
|
4483
|
-
if (stopped) {
|
|
5460
|
+
if (stopResult.stopped) {
|
|
4484
5461
|
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
4485
5462
|
} else {
|
|
4486
5463
|
log(`No running bridge for ${instanceId}`);
|
|
@@ -4522,118 +5499,21 @@ async function removeCommand(args) {
|
|
|
4522
5499
|
}
|
|
4523
5500
|
|
|
4524
5501
|
// src/commands/bridge.ts
|
|
4525
|
-
|
|
4526
|
-
import * as path22 from "path";
|
|
4527
|
-
function formatAge(seconds) {
|
|
4528
|
-
if (seconds < 60) return `${seconds}s ago`;
|
|
4529
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
4530
|
-
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
|
|
4531
|
-
}
|
|
4532
|
-
var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
|
|
4533
|
-
var BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
4534
|
-
var BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
|
|
4535
|
-
function loadBridgeHeartbeatStore(commsDir) {
|
|
4536
|
-
const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
|
|
4537
|
-
if (!existsSync21(heartbeatsPath)) return {};
|
|
4538
|
-
try {
|
|
4539
|
-
return JSON.parse(readFileSync17(heartbeatsPath, "utf-8"));
|
|
4540
|
-
} catch {
|
|
4541
|
-
return null;
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
function saveBridgeHeartbeatStore(commsDir, store) {
|
|
4545
|
-
const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
|
|
4546
|
-
const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
4547
|
-
writeFileSync12(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
4548
|
-
renameSync11(tmp, heartbeatsPath);
|
|
4549
|
-
}
|
|
4550
|
-
function parseBridgeHeartbeatAgeMs(record, now) {
|
|
4551
|
-
const raw = record.lastActivity ?? record.timestamp;
|
|
4552
|
-
if (!raw) return Number.POSITIVE_INFINITY;
|
|
4553
|
-
const parsed = new Date(raw).getTime();
|
|
4554
|
-
if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
|
|
4555
|
-
return Math.max(0, now - parsed);
|
|
4556
|
-
}
|
|
4557
|
-
function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
|
|
4558
|
-
if (state.instances[heartbeatId]) return heartbeatId;
|
|
4559
|
-
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
4560
|
-
if (state.instances[hyphenated]) return hyphenated;
|
|
4561
|
-
const underscored = heartbeatId.replace(/-/g, "_");
|
|
4562
|
-
if (state.instances[underscored]) return underscored;
|
|
4563
|
-
return null;
|
|
4564
|
-
}
|
|
4565
|
-
function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
|
|
4566
|
-
const store = loadBridgeHeartbeatStore(commsDir);
|
|
4567
|
-
if (store === null) {
|
|
4568
|
-
return {
|
|
4569
|
-
removed: 0,
|
|
4570
|
-
warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
|
|
4571
|
-
};
|
|
4572
|
-
}
|
|
4573
|
-
const now = Date.now();
|
|
4574
|
-
let removed = 0;
|
|
4575
|
-
for (const [heartbeatId, heartbeat] of Object.entries(store)) {
|
|
4576
|
-
const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
|
|
4577
|
-
const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
|
|
4578
|
-
const instance = instanceId ? state.instances[instanceId] : null;
|
|
4579
|
-
const bridgeBacked = instance?.bridgeMode === "app-server";
|
|
4580
|
-
const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
|
|
4581
|
-
const status = heartbeat.status ?? "active";
|
|
4582
|
-
const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
|
|
4583
|
-
const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
|
|
4584
|
-
const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
|
|
4585
|
-
if (staleByStatus || staleByDeadBridge || staleByAge) {
|
|
4586
|
-
delete store[heartbeatId];
|
|
4587
|
-
removed += 1;
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4590
|
-
if (removed > 0) {
|
|
4591
|
-
saveBridgeHeartbeatStore(commsDir, store);
|
|
4592
|
-
}
|
|
4593
|
-
return { removed };
|
|
4594
|
-
}
|
|
4595
|
-
var BRIDGE_HELP = `
|
|
4596
|
-
Usage:
|
|
4597
|
-
tap bridge <subcommand> [instance] [options]
|
|
4598
|
-
|
|
4599
|
-
Subcommands:
|
|
4600
|
-
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
4601
|
-
start --all Start all registered app-server instances
|
|
4602
|
-
stop <instance> Stop bridge for an instance
|
|
4603
|
-
stop Stop all running bridges
|
|
4604
|
-
status Show bridge status for all instances
|
|
4605
|
-
status <instance> Show bridge status for a specific instance
|
|
4606
|
-
tui <instance> Show the safe Codex TUI attach command for a running bridge
|
|
4607
|
-
watch Monitor bridges and auto-restart stuck/stale ones
|
|
5502
|
+
init_utils();
|
|
4608
5503
|
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
4615
|
-
--reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
|
|
4616
|
-
--message-lookback-minutes <n> Process messages from last N minutes (default: 10)
|
|
4617
|
-
--thread-id <id> Resume specific thread
|
|
4618
|
-
--ephemeral Use ephemeral thread (no persistence)
|
|
4619
|
-
--process-existing-messages Process all existing inbox messages
|
|
4620
|
-
--no-server Skip app-server auto-start and connect only
|
|
4621
|
-
--no-auth Skip auth gateway (app-server listens directly, localhost only)
|
|
4622
|
-
|
|
4623
|
-
Port Assignment:
|
|
4624
|
-
Ports are auto-assigned from 4501 on first bridge start if not set via --port
|
|
4625
|
-
during 'tap add'. Auto-assigned ports are saved to state for future starts.
|
|
5504
|
+
// src/commands/bridge-start.ts
|
|
5505
|
+
import * as path26 from "path";
|
|
5506
|
+
init_instance_config();
|
|
5507
|
+
init_config();
|
|
5508
|
+
init_utils();
|
|
4626
5509
|
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
npx @hua-labs/tap bridge status
|
|
4635
|
-
npx @hua-labs/tap bridge tui codex
|
|
4636
|
-
`.trim();
|
|
5510
|
+
// src/commands/bridge-helpers.ts
|
|
5511
|
+
import * as path24 from "path";
|
|
5512
|
+
function formatAge(seconds) {
|
|
5513
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
5514
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
5515
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
|
|
5516
|
+
}
|
|
4637
5517
|
function formatAppServerState(appServer) {
|
|
4638
5518
|
const ownership = appServer.managed ? "managed" : "external";
|
|
4639
5519
|
const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
|
|
@@ -4674,7 +5554,7 @@ function formatThreadSummary(threadId, cwd) {
|
|
|
4674
5554
|
return cwd ? `${threadId} (${cwd})` : threadId;
|
|
4675
5555
|
}
|
|
4676
5556
|
function normalizeComparablePath(value) {
|
|
4677
|
-
return
|
|
5557
|
+
return path24.resolve(value).replace(/\\/g, "/").toLowerCase();
|
|
4678
5558
|
}
|
|
4679
5559
|
function sameOptionalPath(left, right) {
|
|
4680
5560
|
if (!left || !right) {
|
|
@@ -4682,6 +5562,16 @@ function sameOptionalPath(left, right) {
|
|
|
4682
5562
|
}
|
|
4683
5563
|
return normalizeComparablePath(left) === normalizeComparablePath(right);
|
|
4684
5564
|
}
|
|
5565
|
+
function resolveRecoveredAgentName(instanceId, explicitAgentName, repoRoot, stateDir) {
|
|
5566
|
+
return resolveAgentName(instanceId, explicitAgentName, { repoRoot, stateDir }) ?? void 0;
|
|
5567
|
+
}
|
|
5568
|
+
function formatLifecycleTransition(lifecycle) {
|
|
5569
|
+
if (!lifecycle?.lastTransitionAt) {
|
|
5570
|
+
return null;
|
|
5571
|
+
}
|
|
5572
|
+
const reason = lifecycle.lastTransitionReason ? ` (${lifecycle.lastTransitionReason})` : "";
|
|
5573
|
+
return `${lifecycle.lastTransitionAt}${reason}, restarts=${lifecycle.restartCount}`;
|
|
5574
|
+
}
|
|
4685
5575
|
function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
|
|
4686
5576
|
const shared = [];
|
|
4687
5577
|
for (const [id, inst] of Object.entries(state.instances)) {
|
|
@@ -4734,6 +5624,75 @@ function transferManagedAppServerOwnership(state, stateDir, recipientId, appServ
|
|
|
4734
5624
|
};
|
|
4735
5625
|
return true;
|
|
4736
5626
|
}
|
|
5627
|
+
|
|
5628
|
+
// src/commands/bridge-heartbeat.ts
|
|
5629
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21, renameSync as renameSync13, writeFileSync as writeFileSync14 } from "fs";
|
|
5630
|
+
import * as path25 from "path";
|
|
5631
|
+
var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
|
|
5632
|
+
var BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
5633
|
+
var BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
|
|
5634
|
+
function loadBridgeHeartbeatStore(commsDir) {
|
|
5635
|
+
const heartbeatsPath = path25.join(commsDir, "heartbeats.json");
|
|
5636
|
+
if (!existsSync25(heartbeatsPath)) return {};
|
|
5637
|
+
try {
|
|
5638
|
+
return JSON.parse(readFileSync21(heartbeatsPath, "utf-8"));
|
|
5639
|
+
} catch {
|
|
5640
|
+
return null;
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
function saveBridgeHeartbeatStore(commsDir, store) {
|
|
5644
|
+
const heartbeatsPath = path25.join(commsDir, "heartbeats.json");
|
|
5645
|
+
const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
5646
|
+
writeFileSync14(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
5647
|
+
renameSync13(tmp, heartbeatsPath);
|
|
5648
|
+
}
|
|
5649
|
+
function parseBridgeHeartbeatAgeMs(record, now) {
|
|
5650
|
+
const raw = record.lastActivity ?? record.timestamp;
|
|
5651
|
+
if (!raw) return Number.POSITIVE_INFINITY;
|
|
5652
|
+
const parsed = new Date(raw).getTime();
|
|
5653
|
+
if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
|
|
5654
|
+
return Math.max(0, now - parsed);
|
|
5655
|
+
}
|
|
5656
|
+
function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
|
|
5657
|
+
if (state.instances[heartbeatId]) return heartbeatId;
|
|
5658
|
+
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
5659
|
+
if (state.instances[hyphenated]) return hyphenated;
|
|
5660
|
+
const underscored = heartbeatId.replace(/-/g, "_");
|
|
5661
|
+
if (state.instances[underscored]) return underscored;
|
|
5662
|
+
return null;
|
|
5663
|
+
}
|
|
5664
|
+
function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
|
|
5665
|
+
const store = loadBridgeHeartbeatStore(commsDir);
|
|
5666
|
+
if (store === null) {
|
|
5667
|
+
return {
|
|
5668
|
+
removed: 0,
|
|
5669
|
+
warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
const now = Date.now();
|
|
5673
|
+
let removed = 0;
|
|
5674
|
+
for (const [heartbeatId, heartbeat] of Object.entries(store)) {
|
|
5675
|
+
const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
|
|
5676
|
+
const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
|
|
5677
|
+
const instance = instanceId ? state.instances[instanceId] : null;
|
|
5678
|
+
const bridgeBacked = instance?.bridgeMode === "app-server";
|
|
5679
|
+
const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
|
|
5680
|
+
const status = heartbeat.status ?? "active";
|
|
5681
|
+
const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
|
|
5682
|
+
const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
|
|
5683
|
+
const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
|
|
5684
|
+
if (staleByStatus || staleByDeadBridge || staleByAge) {
|
|
5685
|
+
delete store[heartbeatId];
|
|
5686
|
+
removed += 1;
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
if (removed > 0) {
|
|
5690
|
+
saveBridgeHeartbeatStore(commsDir, store);
|
|
5691
|
+
}
|
|
5692
|
+
return { removed };
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
// src/commands/bridge-start.ts
|
|
4737
5696
|
async function bridgeStart(identifier, agentName, flags = {}) {
|
|
4738
5697
|
const repoRoot = findRepoRoot();
|
|
4739
5698
|
let state = loadState(repoRoot);
|
|
@@ -4774,6 +5733,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4774
5733
|
}
|
|
4775
5734
|
const adapter = getAdapter(instance.runtime);
|
|
4776
5735
|
const mode = adapter.bridgeMode();
|
|
5736
|
+
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
5737
|
+
if (instance.runtime === "codex") {
|
|
5738
|
+
const patched = patchCodexApprovalMode();
|
|
5739
|
+
if (patched) {
|
|
5740
|
+
log(`patched approval_mode \u2192 auto in ${patched}`);
|
|
5741
|
+
const instConfig = loadInstanceConfig(ctx.stateDir, instanceId);
|
|
5742
|
+
if (instConfig) {
|
|
5743
|
+
instConfig.runtimeConfigHash = fileHash(patched);
|
|
5744
|
+
instConfig.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5745
|
+
saveInstanceConfig(ctx.stateDir, instConfig);
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
4777
5749
|
if (mode !== "app-server") {
|
|
4778
5750
|
return {
|
|
4779
5751
|
ok: true,
|
|
@@ -4786,14 +5758,18 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4786
5758
|
data: { bridgeMode: mode }
|
|
4787
5759
|
};
|
|
4788
5760
|
}
|
|
4789
|
-
const resolvedAgentName =
|
|
4790
|
-
|
|
4791
|
-
|
|
5761
|
+
const resolvedAgentName = resolveRecoveredAgentName(
|
|
5762
|
+
instanceId,
|
|
5763
|
+
agentName,
|
|
5764
|
+
repoRoot,
|
|
5765
|
+
ctx.stateDir
|
|
5766
|
+
);
|
|
5767
|
+
if ((resolvedAgentName ?? null) !== instance.agentName) {
|
|
5768
|
+
instance = { ...instance, agentName: resolvedAgentName ?? null };
|
|
4792
5769
|
const updatedState = updateInstanceState(state, instanceId, instance);
|
|
4793
5770
|
saveState(repoRoot, updatedState);
|
|
4794
5771
|
state = updatedState;
|
|
4795
5772
|
}
|
|
4796
|
-
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
4797
5773
|
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
4798
5774
|
if (!bridgeScript) {
|
|
4799
5775
|
return {
|
|
@@ -4961,7 +5937,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4961
5937
|
messageLookbackMinutes,
|
|
4962
5938
|
threadId,
|
|
4963
5939
|
ephemeral,
|
|
4964
|
-
processExistingMessages
|
|
5940
|
+
processExistingMessages,
|
|
5941
|
+
previousLifecycle: instance.bridgeLifecycle ?? instance.bridge?.lifecycle ?? null
|
|
4965
5942
|
});
|
|
4966
5943
|
} finally {
|
|
4967
5944
|
if (previousWarmup === void 0) {
|
|
@@ -4971,7 +5948,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4971
5948
|
}
|
|
4972
5949
|
}
|
|
4973
5950
|
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
4974
|
-
log(`Log: ${
|
|
5951
|
+
log(`Log: ${path26.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
|
|
4975
5952
|
if (bridge.appServer) {
|
|
4976
5953
|
log(`App server: ${formatAppServerState(bridge.appServer)}`);
|
|
4977
5954
|
if (bridge.appServer.logPath) {
|
|
@@ -4990,7 +5967,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
|
|
|
4990
5967
|
log(`TUI connect: ${bridge.appServer.url}`);
|
|
4991
5968
|
}
|
|
4992
5969
|
}
|
|
4993
|
-
const updated = {
|
|
5970
|
+
const updated = {
|
|
5971
|
+
...instance,
|
|
5972
|
+
bridge,
|
|
5973
|
+
bridgeLifecycle: bridge.lifecycle ?? instance.bridgeLifecycle ?? null,
|
|
5974
|
+
manageAppServer,
|
|
5975
|
+
noAuth
|
|
5976
|
+
};
|
|
4994
5977
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
4995
5978
|
saveState(repoRoot, newState);
|
|
4996
5979
|
return {
|
|
@@ -5078,14 +6061,19 @@ async function bridgeStartAll(flags = {}) {
|
|
|
5078
6061
|
const failed = [];
|
|
5079
6062
|
for (const instanceId of appServerInstances) {
|
|
5080
6063
|
const inst = state.instances[instanceId];
|
|
5081
|
-
const storedName =
|
|
6064
|
+
const storedName = resolveRecoveredAgentName(
|
|
6065
|
+
instanceId,
|
|
6066
|
+
inst?.agentName ?? void 0,
|
|
6067
|
+
repoRoot,
|
|
6068
|
+
ctx.stateDir
|
|
6069
|
+
);
|
|
5082
6070
|
if (!storedName) {
|
|
5083
6071
|
const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
|
|
5084
6072
|
log(msg);
|
|
5085
6073
|
warnings.push(msg);
|
|
5086
6074
|
continue;
|
|
5087
6075
|
}
|
|
5088
|
-
const stateDir =
|
|
6076
|
+
const stateDir = path26.join(repoRoot, ".tap-comms");
|
|
5089
6077
|
const currentBridgeState = loadBridgeState(stateDir, instanceId);
|
|
5090
6078
|
const { manageAppServer, noAuth } = inferRestartMode(
|
|
5091
6079
|
currentBridgeState,
|
|
@@ -5125,6 +6113,9 @@ async function bridgeStartAll(flags = {}) {
|
|
|
5125
6113
|
data: { started, failed, prunedHeartbeats }
|
|
5126
6114
|
};
|
|
5127
6115
|
}
|
|
6116
|
+
|
|
6117
|
+
// src/commands/bridge-stop.ts
|
|
6118
|
+
init_utils();
|
|
5128
6119
|
async function bridgeStopOne(identifier) {
|
|
5129
6120
|
const repoRoot = findRepoRoot();
|
|
5130
6121
|
const state = loadState(repoRoot);
|
|
@@ -5159,14 +6150,14 @@ async function bridgeStopOne(identifier) {
|
|
|
5159
6150
|
);
|
|
5160
6151
|
const appServer = bridgeState?.appServer ?? null;
|
|
5161
6152
|
logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
|
|
5162
|
-
const
|
|
6153
|
+
const stopResult = await stopBridge({
|
|
5163
6154
|
instanceId,
|
|
5164
6155
|
stateDir: ctx.stateDir,
|
|
5165
6156
|
platform: ctx.platform
|
|
5166
6157
|
});
|
|
5167
6158
|
let appServerStopped = false;
|
|
5168
6159
|
let appServerTransferredTo = null;
|
|
5169
|
-
if (stopped) {
|
|
6160
|
+
if (stopResult.stopped) {
|
|
5170
6161
|
logSuccess(`Bridge for ${instanceId} stopped`);
|
|
5171
6162
|
} else {
|
|
5172
6163
|
log(`No running bridge for ${instanceId}`);
|
|
@@ -5210,11 +6201,15 @@ async function bridgeStopOne(identifier) {
|
|
|
5210
6201
|
}
|
|
5211
6202
|
}
|
|
5212
6203
|
if (instance) {
|
|
5213
|
-
const updated = {
|
|
6204
|
+
const updated = {
|
|
6205
|
+
...instance,
|
|
6206
|
+
bridge: null,
|
|
6207
|
+
bridgeLifecycle: stopResult.lifecycle ?? instance.bridgeLifecycle ?? null
|
|
6208
|
+
};
|
|
5214
6209
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
5215
6210
|
saveState(repoRoot, newState);
|
|
5216
6211
|
}
|
|
5217
|
-
if (stopped) {
|
|
6212
|
+
if (stopResult.stopped) {
|
|
5218
6213
|
return {
|
|
5219
6214
|
ok: true,
|
|
5220
6215
|
command: "bridge",
|
|
@@ -5273,18 +6268,22 @@ async function bridgeStopAll() {
|
|
|
5273
6268
|
appServer
|
|
5274
6269
|
);
|
|
5275
6270
|
}
|
|
5276
|
-
const
|
|
6271
|
+
const stopResult = await stopBridge({
|
|
5277
6272
|
instanceId,
|
|
5278
6273
|
stateDir: ctx.stateDir,
|
|
5279
6274
|
platform: ctx.platform
|
|
5280
6275
|
});
|
|
5281
|
-
if (
|
|
6276
|
+
if (stopResult.stopped) {
|
|
5282
6277
|
logSuccess(`Stopped bridge for ${instanceId}`);
|
|
5283
6278
|
stopped.push(instanceId);
|
|
5284
6279
|
}
|
|
5285
6280
|
const instance = state.instances[instanceId];
|
|
5286
|
-
if (instance?.bridge) {
|
|
5287
|
-
state.instances[instanceId] = {
|
|
6281
|
+
if (instance?.bridge || stopResult.lifecycle) {
|
|
6282
|
+
state.instances[instanceId] = {
|
|
6283
|
+
...instance,
|
|
6284
|
+
bridge: null,
|
|
6285
|
+
bridgeLifecycle: stopResult.lifecycle ?? instance.bridgeLifecycle ?? null
|
|
6286
|
+
};
|
|
5288
6287
|
stateChanged = true;
|
|
5289
6288
|
}
|
|
5290
6289
|
}
|
|
@@ -5320,9 +6319,13 @@ async function bridgeStopAll() {
|
|
|
5320
6319
|
data: { stopped, stoppedAppServers }
|
|
5321
6320
|
};
|
|
5322
6321
|
}
|
|
6322
|
+
|
|
6323
|
+
// src/commands/bridge-watch.ts
|
|
6324
|
+
init_config();
|
|
6325
|
+
init_utils();
|
|
5323
6326
|
async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
5324
6327
|
const repoRoot = findRepoRoot();
|
|
5325
|
-
|
|
6328
|
+
let state = loadState(repoRoot);
|
|
5326
6329
|
if (!state) {
|
|
5327
6330
|
return {
|
|
5328
6331
|
ok: false,
|
|
@@ -5342,14 +6345,27 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5342
6345
|
);
|
|
5343
6346
|
const restarted = [];
|
|
5344
6347
|
const cleaned = [];
|
|
6348
|
+
const initializing = [];
|
|
6349
|
+
const degraded = [];
|
|
5345
6350
|
const healthy = [];
|
|
5346
6351
|
const warnings = [];
|
|
6352
|
+
let stateChanged = false;
|
|
5347
6353
|
for (const instanceId of instanceIds) {
|
|
5348
6354
|
const inst = state.instances[instanceId];
|
|
5349
6355
|
if (!inst?.installed || inst.bridgeMode !== "app-server") continue;
|
|
5350
6356
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
5351
6357
|
if (status === "stale") {
|
|
5352
6358
|
log(`${instanceId}: stale (process dead) \u2014 cleaning up`);
|
|
6359
|
+
state.instances[instanceId] = {
|
|
6360
|
+
...inst,
|
|
6361
|
+
bridge: null,
|
|
6362
|
+
bridgeLifecycle: transitionBridgeLifecycle(
|
|
6363
|
+
inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null,
|
|
6364
|
+
"crashed",
|
|
6365
|
+
"bridge pid not alive"
|
|
6366
|
+
)
|
|
6367
|
+
};
|
|
6368
|
+
stateChanged = true;
|
|
5353
6369
|
cleaned.push(instanceId);
|
|
5354
6370
|
continue;
|
|
5355
6371
|
}
|
|
@@ -5357,6 +6373,24 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5357
6373
|
log(`${instanceId}: stopped`);
|
|
5358
6374
|
continue;
|
|
5359
6375
|
}
|
|
6376
|
+
const lifecycle = resolveBridgeLifecycleSnapshot(
|
|
6377
|
+
stateDir,
|
|
6378
|
+
instanceId,
|
|
6379
|
+
inst.bridge,
|
|
6380
|
+
inst.bridgeLifecycle ?? null
|
|
6381
|
+
);
|
|
6382
|
+
if (lifecycle.status === "initializing") {
|
|
6383
|
+
initializing.push(instanceId);
|
|
6384
|
+
log(`${instanceId}: initializing`);
|
|
6385
|
+
continue;
|
|
6386
|
+
}
|
|
6387
|
+
if (lifecycle.status === "degraded-no-thread") {
|
|
6388
|
+
degraded.push(instanceId);
|
|
6389
|
+
log(
|
|
6390
|
+
`${instanceId}: degraded-no-thread${lifecycle.savedThreadId ? ` (saved thread ${lifecycle.savedThreadId})` : ""}`
|
|
6391
|
+
);
|
|
6392
|
+
continue;
|
|
6393
|
+
}
|
|
5360
6394
|
if (isTurnStuck(stateDir, instanceId, stuckThresholdSeconds)) {
|
|
5361
6395
|
const turnInfo = getTurnInfo(stateDir, instanceId, stuckThresholdSeconds);
|
|
5362
6396
|
const ageStr = turnInfo?.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
|
|
@@ -5380,6 +6414,12 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5380
6414
|
const previousWarmup = process.env.TAP_COLD_START_WARMUP;
|
|
5381
6415
|
process.env.TAP_COLD_START_WARMUP = "true";
|
|
5382
6416
|
try {
|
|
6417
|
+
const recoveredAgentName = resolveRecoveredAgentName(
|
|
6418
|
+
instanceId,
|
|
6419
|
+
void 0,
|
|
6420
|
+
repoRoot,
|
|
6421
|
+
ctx.stateDir
|
|
6422
|
+
);
|
|
5383
6423
|
const newBridgeState = await restartBridge({
|
|
5384
6424
|
instanceId,
|
|
5385
6425
|
runtime: inst.runtime,
|
|
@@ -5387,7 +6427,7 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5387
6427
|
commsDir: ctx.commsDir,
|
|
5388
6428
|
bridgeScript,
|
|
5389
6429
|
platform: ctx.platform,
|
|
5390
|
-
agentName:
|
|
6430
|
+
agentName: recoveredAgentName,
|
|
5391
6431
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
5392
6432
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
5393
6433
|
repoRoot,
|
|
@@ -5395,15 +6435,22 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5395
6435
|
headless: inst.headless,
|
|
5396
6436
|
drainTimeoutSeconds: 30,
|
|
5397
6437
|
manageAppServer,
|
|
5398
|
-
noAuth
|
|
6438
|
+
noAuth,
|
|
6439
|
+
previousLifecycle: inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null
|
|
5399
6440
|
});
|
|
5400
|
-
const updatedInst = {
|
|
6441
|
+
const updatedInst = {
|
|
6442
|
+
...inst,
|
|
6443
|
+
agentName: recoveredAgentName ?? inst.agentName ?? null,
|
|
6444
|
+
bridge: newBridgeState,
|
|
6445
|
+
bridgeLifecycle: newBridgeState.lifecycle ?? inst.bridgeLifecycle ?? null
|
|
6446
|
+
};
|
|
5401
6447
|
const updatedState = updateInstanceState(
|
|
5402
6448
|
state,
|
|
5403
6449
|
instanceId,
|
|
5404
6450
|
updatedInst
|
|
5405
6451
|
);
|
|
5406
6452
|
saveState(repoRoot, updatedState);
|
|
6453
|
+
state = updatedState;
|
|
5407
6454
|
restarted.push(instanceId);
|
|
5408
6455
|
logSuccess(`${instanceId}: restarted`);
|
|
5409
6456
|
} catch (err) {
|
|
@@ -5425,19 +6472,29 @@ async function bridgeWatch(_intervalSeconds, stuckThresholdSeconds) {
|
|
|
5425
6472
|
const message = [
|
|
5426
6473
|
restarted.length > 0 ? `Restarted: ${restarted.join(", ")}` : null,
|
|
5427
6474
|
cleaned.length > 0 ? `Cleaned stale: ${cleaned.join(", ")}` : null,
|
|
6475
|
+
initializing.length > 0 ? `Initializing: ${initializing.join(", ")}` : null,
|
|
6476
|
+
degraded.length > 0 ? `Degraded: ${degraded.join(", ")}` : null,
|
|
5428
6477
|
healthy.length > 0 ? `Healthy: ${healthy.join(", ")}` : null
|
|
5429
6478
|
].filter(Boolean).join(". ") || "No app-server bridges found";
|
|
5430
6479
|
log("");
|
|
5431
6480
|
log(message);
|
|
6481
|
+
if (stateChanged) {
|
|
6482
|
+
saveState(repoRoot, state);
|
|
6483
|
+
}
|
|
5432
6484
|
return {
|
|
5433
6485
|
ok: true,
|
|
5434
6486
|
command: "bridge",
|
|
5435
6487
|
code: restarted.length > 0 ? "TAP_BRIDGE_WATCH_RESTARTED" : "TAP_BRIDGE_WATCH_OK",
|
|
5436
6488
|
message,
|
|
5437
6489
|
warnings,
|
|
5438
|
-
data: { restarted, cleaned, healthy }
|
|
6490
|
+
data: { restarted, cleaned, initializing, degraded, healthy }
|
|
5439
6491
|
};
|
|
5440
6492
|
}
|
|
6493
|
+
|
|
6494
|
+
// src/commands/bridge-status.ts
|
|
6495
|
+
import * as path27 from "path";
|
|
6496
|
+
init_config();
|
|
6497
|
+
init_utils();
|
|
5441
6498
|
function bridgeStatusAll() {
|
|
5442
6499
|
const repoRoot = findRepoRoot();
|
|
5443
6500
|
const state = loadState(repoRoot);
|
|
@@ -5457,11 +6514,12 @@ function bridgeStatusAll() {
|
|
|
5457
6514
|
const bridges = {};
|
|
5458
6515
|
logHeader("@hua-labs/tap bridge status");
|
|
5459
6516
|
log(
|
|
5460
|
-
`${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
|
|
6517
|
+
`${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"Lifecycle".padEnd(20)} ${"Session".padEnd(18)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
|
|
5461
6518
|
);
|
|
5462
6519
|
log(
|
|
5463
|
-
`${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
|
|
6520
|
+
`${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
|
|
5464
6521
|
);
|
|
6522
|
+
let stateChanged = false;
|
|
5465
6523
|
for (const instanceId of instanceIds) {
|
|
5466
6524
|
const inst = state.instances[instanceId];
|
|
5467
6525
|
if (!inst?.installed) continue;
|
|
@@ -5471,6 +6529,8 @@ function bridgeStatusAll() {
|
|
|
5471
6529
|
);
|
|
5472
6530
|
bridges[instanceId] = {
|
|
5473
6531
|
status: "n/a",
|
|
6532
|
+
lifecycle: null,
|
|
6533
|
+
session: null,
|
|
5474
6534
|
runtime: inst.runtime,
|
|
5475
6535
|
pid: null,
|
|
5476
6536
|
port: inst.port,
|
|
@@ -5484,18 +6544,40 @@ function bridgeStatusAll() {
|
|
|
5484
6544
|
continue;
|
|
5485
6545
|
}
|
|
5486
6546
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
5487
|
-
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
6547
|
+
const bridgeState = loadBridgeState(stateDir, instanceId) ?? inst.bridge;
|
|
5488
6548
|
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
5489
6549
|
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
6550
|
+
const lifecycle = deriveBridgeLifecycleState({
|
|
6551
|
+
bridgeStatus: status,
|
|
6552
|
+
bridgeState,
|
|
6553
|
+
runtimeHeartbeat,
|
|
6554
|
+
savedThread,
|
|
6555
|
+
persistedLifecycle: inst.bridgeLifecycle ?? bridgeState?.lifecycle ?? null
|
|
6556
|
+
});
|
|
6557
|
+
const session = status === "running" ? deriveCodexSessionState({
|
|
6558
|
+
runtimeHeartbeat,
|
|
6559
|
+
runtimeStateDir: bridgeState?.runtimeStateDir ?? null
|
|
6560
|
+
}) : null;
|
|
5490
6561
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
6562
|
+
if (lifecycle.status === "bridge-stale" && inst.bridge) {
|
|
6563
|
+
state.instances[instanceId] = {
|
|
6564
|
+
...inst,
|
|
6565
|
+
bridge: null,
|
|
6566
|
+
bridgeLifecycle: transitionBridgeLifecycle(
|
|
6567
|
+
inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null,
|
|
6568
|
+
"crashed",
|
|
6569
|
+
"bridge pid not alive"
|
|
6570
|
+
)
|
|
6571
|
+
};
|
|
6572
|
+
stateChanged = true;
|
|
6573
|
+
}
|
|
5491
6574
|
const pid = bridgeState?.pid ?? null;
|
|
5492
6575
|
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
5493
6576
|
const pidStr = pid ? String(pid) : "-";
|
|
5494
6577
|
const portStr = inst.port ? String(inst.port) : "-";
|
|
5495
6578
|
const ageStr = age !== null ? formatAge(age) : "-";
|
|
5496
|
-
const statusColor = status === "running" ? "running" : status === "stale" ? "stale!" : "stopped";
|
|
5497
6579
|
log(
|
|
5498
|
-
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${
|
|
6580
|
+
`${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(10)} ${lifecycle.status.padEnd(20)} ${(session?.status ?? "-").padEnd(18)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
|
|
5499
6581
|
);
|
|
5500
6582
|
if (bridgeState?.appServer) {
|
|
5501
6583
|
log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
|
|
@@ -5518,6 +6600,10 @@ function bridgeStatusAll() {
|
|
|
5518
6600
|
` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
|
|
5519
6601
|
);
|
|
5520
6602
|
}
|
|
6603
|
+
const transition = formatLifecycleTransition(lifecycle);
|
|
6604
|
+
if (transition) {
|
|
6605
|
+
log(` Transition: ${transition}`);
|
|
6606
|
+
}
|
|
5521
6607
|
const turnInfo = getTurnInfo(stateDir, instanceId);
|
|
5522
6608
|
if (turnInfo?.activeTurnId) {
|
|
5523
6609
|
const ageStr2 = turnInfo.ageSeconds != null ? formatAge(turnInfo.ageSeconds) : "?";
|
|
@@ -5533,6 +6619,8 @@ function bridgeStatusAll() {
|
|
|
5533
6619
|
}
|
|
5534
6620
|
bridges[instanceId] = {
|
|
5535
6621
|
status,
|
|
6622
|
+
lifecycle,
|
|
6623
|
+
session,
|
|
5536
6624
|
runtime: inst.runtime,
|
|
5537
6625
|
pid,
|
|
5538
6626
|
port: inst.port,
|
|
@@ -5547,6 +6635,9 @@ function bridgeStatusAll() {
|
|
|
5547
6635
|
if (instanceIds.length === 0) {
|
|
5548
6636
|
log("No instances installed.");
|
|
5549
6637
|
}
|
|
6638
|
+
if (stateChanged) {
|
|
6639
|
+
saveState(repoRoot, state);
|
|
6640
|
+
}
|
|
5550
6641
|
log("");
|
|
5551
6642
|
return {
|
|
5552
6643
|
ok: true,
|
|
@@ -5612,6 +6703,15 @@ function bridgeStatusOne(identifier) {
|
|
|
5612
6703
|
warnings: [],
|
|
5613
6704
|
data: {
|
|
5614
6705
|
status: "n/a",
|
|
6706
|
+
lifecycle: {
|
|
6707
|
+
presence: "stopped",
|
|
6708
|
+
status: "stopped",
|
|
6709
|
+
summary: "stopped",
|
|
6710
|
+
lastTransitionAt: null,
|
|
6711
|
+
lastTransitionReason: null,
|
|
6712
|
+
restartCount: 0
|
|
6713
|
+
},
|
|
6714
|
+
session: null,
|
|
5615
6715
|
bridgeMode: inst.bridgeMode,
|
|
5616
6716
|
pid: null,
|
|
5617
6717
|
port: inst.port,
|
|
@@ -5627,12 +6727,25 @@ function bridgeStatusOne(identifier) {
|
|
|
5627
6727
|
const { config: resolvedCfg2 } = resolveConfig({}, repoRoot);
|
|
5628
6728
|
const stateDir = resolvedCfg2.stateDir;
|
|
5629
6729
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
5630
|
-
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
6730
|
+
const bridgeState = loadBridgeState(stateDir, instanceId) ?? inst.bridge;
|
|
5631
6731
|
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
5632
6732
|
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
5633
6733
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
5634
6734
|
const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
|
|
6735
|
+
const lifecycle = deriveBridgeLifecycleState({
|
|
6736
|
+
bridgeStatus: status,
|
|
6737
|
+
bridgeState,
|
|
6738
|
+
runtimeHeartbeat,
|
|
6739
|
+
savedThread,
|
|
6740
|
+
persistedLifecycle: inst.bridgeLifecycle ?? bridgeState?.lifecycle ?? null
|
|
6741
|
+
});
|
|
6742
|
+
const session = deriveCodexSessionState({
|
|
6743
|
+
runtimeHeartbeat,
|
|
6744
|
+
runtimeStateDir: bridgeState?.runtimeStateDir ?? null
|
|
6745
|
+
});
|
|
5635
6746
|
log(`Status: ${status}`);
|
|
6747
|
+
log(`Lifecycle: ${lifecycle.summary}`);
|
|
6748
|
+
log(`Session: ${session.summary}`);
|
|
5636
6749
|
if (bridgeState) {
|
|
5637
6750
|
log(`PID: ${bridgeState.pid}`);
|
|
5638
6751
|
log(
|
|
@@ -5649,7 +6762,7 @@ function bridgeStatusOne(identifier) {
|
|
|
5649
6762
|
);
|
|
5650
6763
|
}
|
|
5651
6764
|
log(
|
|
5652
|
-
`Log: ${
|
|
6765
|
+
`Log: ${path27.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
|
|
5653
6766
|
);
|
|
5654
6767
|
if (bridgeState.appServer) {
|
|
5655
6768
|
log(`App server: ${bridgeState.appServer.url}`);
|
|
@@ -5681,6 +6794,10 @@ function bridgeStatusOne(identifier) {
|
|
|
5681
6794
|
}
|
|
5682
6795
|
}
|
|
5683
6796
|
}
|
|
6797
|
+
const transition = formatLifecycleTransition(lifecycle);
|
|
6798
|
+
if (transition) {
|
|
6799
|
+
log(`Transition: ${transition}`);
|
|
6800
|
+
}
|
|
5684
6801
|
log("");
|
|
5685
6802
|
return {
|
|
5686
6803
|
ok: true,
|
|
@@ -5692,6 +6809,23 @@ function bridgeStatusOne(identifier) {
|
|
|
5692
6809
|
warnings: [],
|
|
5693
6810
|
data: {
|
|
5694
6811
|
status,
|
|
6812
|
+
lifecycle: {
|
|
6813
|
+
presence: lifecycle.presence,
|
|
6814
|
+
status: lifecycle.status,
|
|
6815
|
+
summary: lifecycle.summary,
|
|
6816
|
+
lastTransitionAt: lifecycle.lastTransitionAt,
|
|
6817
|
+
lastTransitionReason: lifecycle.lastTransitionReason,
|
|
6818
|
+
restartCount: lifecycle.restartCount
|
|
6819
|
+
},
|
|
6820
|
+
session: {
|
|
6821
|
+
status: session.status,
|
|
6822
|
+
turnState: session.turnState,
|
|
6823
|
+
summary: session.summary,
|
|
6824
|
+
activeTurnId: session.activeTurnId,
|
|
6825
|
+
idleSince: session.idleSince,
|
|
6826
|
+
lastTurnAt: session.lastTurnAt,
|
|
6827
|
+
lastDispatchAt: session.lastDispatchAt
|
|
6828
|
+
},
|
|
5695
6829
|
bridgeMode: inst.bridgeMode,
|
|
5696
6830
|
pid: bridgeState?.pid ?? null,
|
|
5697
6831
|
port: inst.port,
|
|
@@ -5704,6 +6838,10 @@ function bridgeStatusOne(identifier) {
|
|
|
5704
6838
|
}
|
|
5705
6839
|
};
|
|
5706
6840
|
}
|
|
6841
|
+
|
|
6842
|
+
// src/commands/bridge-tui.ts
|
|
6843
|
+
init_config();
|
|
6844
|
+
init_utils();
|
|
5707
6845
|
function bridgeTuiOne(identifier) {
|
|
5708
6846
|
const repoRoot = findRepoRoot();
|
|
5709
6847
|
const state = loadState(repoRoot);
|
|
@@ -5820,7 +6958,11 @@ function bridgeTuiOne(identifier) {
|
|
|
5820
6958
|
}
|
|
5821
6959
|
};
|
|
5822
6960
|
}
|
|
5823
|
-
|
|
6961
|
+
|
|
6962
|
+
// src/commands/bridge-restart.ts
|
|
6963
|
+
init_config();
|
|
6964
|
+
init_utils();
|
|
6965
|
+
async function bridgeRestart(identifier, flags, explicitAgentName) {
|
|
5824
6966
|
const repoRoot = findRepoRoot();
|
|
5825
6967
|
const state = loadState(repoRoot);
|
|
5826
6968
|
if (!state) {
|
|
@@ -5893,6 +7035,12 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5893
7035
|
logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
|
|
5894
7036
|
log(`Drain timeout: ${drainTimeout}s`);
|
|
5895
7037
|
try {
|
|
7038
|
+
const resolvedAgentName = resolveRecoveredAgentName(
|
|
7039
|
+
instanceId,
|
|
7040
|
+
explicitAgentName,
|
|
7041
|
+
repoRoot,
|
|
7042
|
+
ctx.stateDir
|
|
7043
|
+
);
|
|
5896
7044
|
const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
|
|
5897
7045
|
const { manageAppServer, noAuth } = inferRestartMode(
|
|
5898
7046
|
currentBridgeState,
|
|
@@ -5916,7 +7064,7 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5916
7064
|
commsDir: ctx.commsDir,
|
|
5917
7065
|
bridgeScript,
|
|
5918
7066
|
platform: ctx.platform,
|
|
5919
|
-
agentName:
|
|
7067
|
+
agentName: resolvedAgentName,
|
|
5920
7068
|
runtimeCommand: resolvedConfig.runtimeCommand,
|
|
5921
7069
|
appServerUrl: resolvedConfig.appServerUrl,
|
|
5922
7070
|
repoRoot,
|
|
@@ -5924,7 +7072,8 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5924
7072
|
headless: inst.headless,
|
|
5925
7073
|
drainTimeoutSeconds: drainTimeout,
|
|
5926
7074
|
manageAppServer,
|
|
5927
|
-
noAuth
|
|
7075
|
+
noAuth,
|
|
7076
|
+
previousLifecycle: inst.bridgeLifecycle ?? inst.bridge?.lifecycle ?? null
|
|
5928
7077
|
});
|
|
5929
7078
|
} finally {
|
|
5930
7079
|
if (previousColdStartWarmup === void 0) {
|
|
@@ -5934,7 +7083,14 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5934
7083
|
}
|
|
5935
7084
|
}
|
|
5936
7085
|
logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
|
|
5937
|
-
const updated = {
|
|
7086
|
+
const updated = {
|
|
7087
|
+
...inst,
|
|
7088
|
+
agentName: resolvedAgentName ?? inst.agentName ?? null,
|
|
7089
|
+
bridge,
|
|
7090
|
+
bridgeLifecycle: bridge.lifecycle ?? inst.bridgeLifecycle ?? null,
|
|
7091
|
+
manageAppServer,
|
|
7092
|
+
noAuth
|
|
7093
|
+
};
|
|
5938
7094
|
const newState = updateInstanceState(state, instanceId, updated);
|
|
5939
7095
|
saveState(repoRoot, newState);
|
|
5940
7096
|
return {
|
|
@@ -5960,6 +7116,50 @@ async function bridgeRestart(identifier, flags) {
|
|
|
5960
7116
|
};
|
|
5961
7117
|
}
|
|
5962
7118
|
}
|
|
7119
|
+
|
|
7120
|
+
// src/commands/bridge.ts
|
|
7121
|
+
var BRIDGE_HELP = `
|
|
7122
|
+
Usage:
|
|
7123
|
+
tap bridge <subcommand> [instance] [options]
|
|
7124
|
+
|
|
7125
|
+
Subcommands:
|
|
7126
|
+
start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
|
|
7127
|
+
start --all Start all registered app-server instances
|
|
7128
|
+
stop <instance> Stop bridge for an instance
|
|
7129
|
+
stop Stop all running bridges
|
|
7130
|
+
status Show bridge status for all instances
|
|
7131
|
+
status <instance> Show bridge status for a specific instance
|
|
7132
|
+
tui <instance> Show the safe Codex TUI attach command for a running bridge
|
|
7133
|
+
watch Monitor bridges and auto-restart stuck/stale ones
|
|
7134
|
+
|
|
7135
|
+
Options:
|
|
7136
|
+
--agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
|
|
7137
|
+
Overrides the stored name from 'tap add' when needed
|
|
7138
|
+
--all Start all registered app-server instances
|
|
7139
|
+
--busy-mode <steer|wait> How to handle active turns (default: steer)
|
|
7140
|
+
--poll-seconds <n> Inbox poll interval (default: 5)
|
|
7141
|
+
--reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
|
|
7142
|
+
--message-lookback-minutes <n> Process messages from last N minutes (default: 10)
|
|
7143
|
+
--thread-id <id> Resume specific thread
|
|
7144
|
+
--ephemeral Use ephemeral thread (no persistence)
|
|
7145
|
+
--process-existing-messages Process all existing inbox messages
|
|
7146
|
+
--no-server Skip app-server auto-start and connect only
|
|
7147
|
+
--no-auth Skip auth gateway (app-server listens directly, localhost only)
|
|
7148
|
+
|
|
7149
|
+
Port Assignment:
|
|
7150
|
+
Ports are auto-assigned from 4501 on first bridge start if not set via --port
|
|
7151
|
+
during 'tap add'. Auto-assigned ports are saved to state for future starts.
|
|
7152
|
+
|
|
7153
|
+
Examples:
|
|
7154
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent
|
|
7155
|
+
npx @hua-labs/tap bridge start --all
|
|
7156
|
+
npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
|
|
7157
|
+
npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
|
|
7158
|
+
npx @hua-labs/tap bridge stop codex
|
|
7159
|
+
npx @hua-labs/tap bridge stop
|
|
7160
|
+
npx @hua-labs/tap bridge status
|
|
7161
|
+
npx @hua-labs/tap bridge tui codex
|
|
7162
|
+
`.trim();
|
|
5963
7163
|
async function bridgeCommand(args) {
|
|
5964
7164
|
const { positional, flags } = parseArgs(args);
|
|
5965
7165
|
const subcommand = positional[0];
|
|
@@ -6065,21 +7265,67 @@ async function bridgeCommand(args) {
|
|
|
6065
7265
|
}
|
|
6066
7266
|
|
|
6067
7267
|
// src/engine/dashboard.ts
|
|
6068
|
-
|
|
6069
|
-
import * as
|
|
7268
|
+
init_config();
|
|
7269
|
+
import * as fs28 from "fs";
|
|
7270
|
+
import * as path28 from "path";
|
|
6070
7271
|
import { execSync as execSync4 } from "child_process";
|
|
6071
|
-
function
|
|
6072
|
-
const
|
|
6073
|
-
|
|
7272
|
+
function formatAgentLabel(agentIdOrName, displayName) {
|
|
7273
|
+
const normalizedId = agentIdOrName.trim();
|
|
7274
|
+
const normalizedName = displayName?.trim();
|
|
7275
|
+
if (!normalizedId) {
|
|
7276
|
+
return normalizedName ?? agentIdOrName;
|
|
7277
|
+
}
|
|
7278
|
+
if (!normalizedName || normalizedName === normalizedId) {
|
|
7279
|
+
return normalizedId;
|
|
7280
|
+
}
|
|
7281
|
+
return `${normalizedName} [${normalizedId}]`;
|
|
7282
|
+
}
|
|
7283
|
+
function parseIsoAgeSeconds(value) {
|
|
7284
|
+
if (!value) return null;
|
|
7285
|
+
const timestamp = new Date(value).getTime();
|
|
7286
|
+
if (Number.isNaN(timestamp)) return null;
|
|
7287
|
+
return Math.max(0, Math.floor((Date.now() - timestamp) / 1e3));
|
|
7288
|
+
}
|
|
7289
|
+
function resolveHeartbeatInstanceId(heartbeatId, displayName, state) {
|
|
7290
|
+
if (!state) return null;
|
|
7291
|
+
if (state.instances[heartbeatId]?.installed) return heartbeatId;
|
|
7292
|
+
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
7293
|
+
if (state.instances[hyphenated]?.installed) return hyphenated;
|
|
7294
|
+
const underscored = heartbeatId.replace(/-/g, "_");
|
|
7295
|
+
if (state.instances[underscored]?.installed) return underscored;
|
|
7296
|
+
if (!displayName) return null;
|
|
7297
|
+
const matches = Object.values(state.instances).filter(
|
|
7298
|
+
(inst) => inst?.installed && inst.agentName === displayName
|
|
7299
|
+
);
|
|
7300
|
+
return matches.length === 1 ? matches[0].instanceId : null;
|
|
7301
|
+
}
|
|
7302
|
+
function collectAgents(commsDir, state, bridges) {
|
|
7303
|
+
const heartbeatsPath = path28.join(commsDir, "heartbeats.json");
|
|
7304
|
+
if (!fs28.existsSync(heartbeatsPath)) return [];
|
|
6074
7305
|
try {
|
|
6075
|
-
const raw =
|
|
7306
|
+
const raw = fs28.readFileSync(heartbeatsPath, "utf-8");
|
|
6076
7307
|
const data = JSON.parse(raw);
|
|
6077
|
-
return Object.entries(data).map(([
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
7308
|
+
return Object.entries(data).map(([agentId, info]) => {
|
|
7309
|
+
const instanceId = resolveHeartbeatInstanceId(
|
|
7310
|
+
agentId,
|
|
7311
|
+
info.agent ?? null,
|
|
7312
|
+
state
|
|
7313
|
+
);
|
|
7314
|
+
const bridge = instanceId ? bridges.find((candidate) => candidate.instanceId === instanceId) ?? null : null;
|
|
7315
|
+
const presence = bridge?.status === "stale" || bridge?.lifecycle?.status === "bridge-stale" ? "bridge-stale" : bridge?.status === "running" ? "bridge-live" : "mcp-only";
|
|
7316
|
+
const lastActivity = info.lastActivity ?? info.timestamp ?? null;
|
|
7317
|
+
const idleBasis = bridge?.session?.idleSince ?? lastActivity;
|
|
7318
|
+
return {
|
|
7319
|
+
name: formatAgentLabel(agentId, info.agent ?? null),
|
|
7320
|
+
instanceId,
|
|
7321
|
+
presence,
|
|
7322
|
+
lifecycle: bridge?.lifecycle?.status ?? null,
|
|
7323
|
+
status: info.status ?? null,
|
|
7324
|
+
lastActivity,
|
|
7325
|
+
joinedAt: info.joinedAt ?? null,
|
|
7326
|
+
idleSeconds: parseIsoAgeSeconds(idleBasis)
|
|
7327
|
+
};
|
|
7328
|
+
});
|
|
6083
7329
|
} catch {
|
|
6084
7330
|
return [];
|
|
6085
7331
|
}
|
|
@@ -6095,12 +7341,21 @@ function collectBridges(repoRoot) {
|
|
|
6095
7341
|
if (inst.bridgeMode !== "app-server") continue;
|
|
6096
7342
|
const instanceId = id;
|
|
6097
7343
|
const status = getBridgeStatus(stateDir, instanceId);
|
|
6098
|
-
const
|
|
7344
|
+
const persistedBridgeState = loadBridgeState(stateDir, instanceId);
|
|
7345
|
+
const bridgeState = persistedBridgeState ?? inst.bridge ?? null;
|
|
6099
7346
|
const age = getHeartbeatAge(stateDir, instanceId);
|
|
7347
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
7348
|
+
const lifecycle = bridgeState != null ? resolveBridgeLifecycleSnapshot(stateDir, instanceId, bridgeState) : null;
|
|
7349
|
+
const session = bridgeState != null ? deriveCodexSessionState({
|
|
7350
|
+
runtimeHeartbeat,
|
|
7351
|
+
runtimeStateDir: bridgeState.runtimeStateDir ?? null
|
|
7352
|
+
}) : null;
|
|
6100
7353
|
bridges.push({
|
|
6101
7354
|
instanceId: id,
|
|
6102
7355
|
runtime: inst.runtime,
|
|
6103
7356
|
status,
|
|
7357
|
+
lifecycle,
|
|
7358
|
+
session,
|
|
6104
7359
|
pid: bridgeState?.pid ?? null,
|
|
6105
7360
|
port: inst.port ?? null,
|
|
6106
7361
|
heartbeatAge: age,
|
|
@@ -6108,22 +7363,22 @@ function collectBridges(repoRoot) {
|
|
|
6108
7363
|
});
|
|
6109
7364
|
}
|
|
6110
7365
|
}
|
|
6111
|
-
const tmpDir =
|
|
6112
|
-
if (
|
|
7366
|
+
const tmpDir = path28.join(repoRoot, ".tmp");
|
|
7367
|
+
if (fs28.existsSync(tmpDir)) {
|
|
6113
7368
|
try {
|
|
6114
|
-
const dirs =
|
|
7369
|
+
const dirs = fs28.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
6115
7370
|
for (const dir of dirs) {
|
|
6116
|
-
const daemonPath =
|
|
6117
|
-
if (!
|
|
7371
|
+
const daemonPath = path28.join(tmpDir, dir, "bridge-daemon.json");
|
|
7372
|
+
if (!fs28.existsSync(daemonPath)) continue;
|
|
6118
7373
|
try {
|
|
6119
|
-
const raw =
|
|
7374
|
+
const raw = fs28.readFileSync(daemonPath, "utf-8");
|
|
6120
7375
|
const daemon = JSON.parse(raw);
|
|
6121
7376
|
const alreadyCovered = bridges.some(
|
|
6122
7377
|
(b) => b.pid === daemon.pid && b.pid !== null
|
|
6123
7378
|
);
|
|
6124
7379
|
if (alreadyCovered) continue;
|
|
6125
|
-
const agentFile =
|
|
6126
|
-
const agentName =
|
|
7380
|
+
const agentFile = path28.join(tmpDir, dir, "agent-name.txt");
|
|
7381
|
+
const agentName = fs28.existsSync(agentFile) ? fs28.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
6127
7382
|
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
6128
7383
|
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
6129
7384
|
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
@@ -6131,6 +7386,8 @@ function collectBridges(repoRoot) {
|
|
|
6131
7386
|
instanceId: agentName,
|
|
6132
7387
|
runtime: "codex",
|
|
6133
7388
|
status: running ? "running" : "stale",
|
|
7389
|
+
lifecycle: null,
|
|
7390
|
+
session: null,
|
|
6134
7391
|
pid: daemon.pid ?? null,
|
|
6135
7392
|
port,
|
|
6136
7393
|
heartbeatAge: null,
|
|
@@ -6177,6 +7434,12 @@ function collectWarnings(bridges, agents) {
|
|
|
6177
7434
|
message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
|
|
6178
7435
|
});
|
|
6179
7436
|
}
|
|
7437
|
+
if (bridge.lifecycle?.status === "degraded-no-thread") {
|
|
7438
|
+
warnings.push({
|
|
7439
|
+
level: "warn",
|
|
7440
|
+
message: `Bridge ${bridge.instanceId} is degraded (no active thread)`
|
|
7441
|
+
});
|
|
7442
|
+
}
|
|
6180
7443
|
}
|
|
6181
7444
|
if (bridges.length === 0) {
|
|
6182
7445
|
warnings.push({
|
|
@@ -6198,8 +7461,9 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
|
6198
7461
|
repoRoot
|
|
6199
7462
|
);
|
|
6200
7463
|
const resolved = config;
|
|
6201
|
-
const
|
|
7464
|
+
const state = loadState(resolved.repoRoot);
|
|
6202
7465
|
const bridges = collectBridges(resolved.repoRoot);
|
|
7466
|
+
const agents = collectAgents(resolved.commsDir, state, bridges);
|
|
6203
7467
|
const prs = collectPRs();
|
|
6204
7468
|
const warnings = collectWarnings(bridges, agents);
|
|
6205
7469
|
return {
|
|
@@ -6214,6 +7478,7 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
|
6214
7478
|
}
|
|
6215
7479
|
|
|
6216
7480
|
// src/commands/up.ts
|
|
7481
|
+
init_utils();
|
|
6217
7482
|
var UP_HELP = `
|
|
6218
7483
|
Usage:
|
|
6219
7484
|
tap up [bridge-start options]
|
|
@@ -6228,6 +7493,18 @@ Examples:
|
|
|
6228
7493
|
npx @hua-labs/tap up --no-auth
|
|
6229
7494
|
npx @hua-labs/tap up --busy-mode wait
|
|
6230
7495
|
`.trim();
|
|
7496
|
+
function summarizeLifecycle(snapshot) {
|
|
7497
|
+
const ready = snapshot.bridges.filter(
|
|
7498
|
+
(bridge) => bridge.lifecycle?.status === "ready"
|
|
7499
|
+
).length;
|
|
7500
|
+
const initializing = snapshot.bridges.filter(
|
|
7501
|
+
(bridge) => bridge.lifecycle?.status === "initializing"
|
|
7502
|
+
).length;
|
|
7503
|
+
const degraded = snapshot.bridges.filter(
|
|
7504
|
+
(bridge) => bridge.lifecycle?.status === "degraded-no-thread"
|
|
7505
|
+
).length;
|
|
7506
|
+
return `${ready} ready, ${initializing} initializing, ${degraded} degraded`;
|
|
7507
|
+
}
|
|
6231
7508
|
async function upCommand(args) {
|
|
6232
7509
|
if (args.includes("--help") || args.includes("-h")) {
|
|
6233
7510
|
log(UP_HELP);
|
|
@@ -6276,7 +7553,7 @@ async function upCommand(args) {
|
|
|
6276
7553
|
ok: true,
|
|
6277
7554
|
command: "up",
|
|
6278
7555
|
code: "TAP_UP_OK",
|
|
6279
|
-
message: `tap up: ${activeBridges} bridge(s) running`,
|
|
7556
|
+
message: `tap up: ${activeBridges} bridge(s) running (${summarizeLifecycle(snapshot)})`,
|
|
6280
7557
|
warnings: result.warnings,
|
|
6281
7558
|
data: {
|
|
6282
7559
|
...result.data,
|
|
@@ -6286,6 +7563,7 @@ async function upCommand(args) {
|
|
|
6286
7563
|
}
|
|
6287
7564
|
|
|
6288
7565
|
// src/commands/down.ts
|
|
7566
|
+
init_utils();
|
|
6289
7567
|
var DOWN_HELP = `
|
|
6290
7568
|
Usage:
|
|
6291
7569
|
tap down
|
|
@@ -6335,8 +7613,10 @@ async function downCommand(args) {
|
|
|
6335
7613
|
}
|
|
6336
7614
|
|
|
6337
7615
|
// src/commands/serve.ts
|
|
6338
|
-
import * as
|
|
7616
|
+
import * as path29 from "path";
|
|
6339
7617
|
import { spawn as spawn2 } from "child_process";
|
|
7618
|
+
init_utils();
|
|
7619
|
+
init_config();
|
|
6340
7620
|
var SERVE_HELP = `
|
|
6341
7621
|
Usage:
|
|
6342
7622
|
tap serve [options]
|
|
@@ -6369,10 +7649,10 @@ async function serveCommand(args) {
|
|
|
6369
7649
|
let commsDir;
|
|
6370
7650
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
6371
7651
|
if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
|
|
6372
|
-
commsDir =
|
|
7652
|
+
commsDir = path29.resolve(normalizeTapPath(args[commsDirIdx + 1]));
|
|
6373
7653
|
}
|
|
6374
7654
|
if (!commsDir && process.env.TAP_COMMS_DIR) {
|
|
6375
|
-
commsDir =
|
|
7655
|
+
commsDir = path29.resolve(normalizeTapPath(process.env.TAP_COMMS_DIR));
|
|
6376
7656
|
}
|
|
6377
7657
|
if (!commsDir) {
|
|
6378
7658
|
const state = loadState(repoRoot);
|
|
@@ -6412,9 +7692,9 @@ async function serveCommand(args) {
|
|
|
6412
7692
|
TAP_COMMS_DIR: commsDir
|
|
6413
7693
|
}
|
|
6414
7694
|
});
|
|
6415
|
-
return new Promise((
|
|
7695
|
+
return new Promise((resolve15) => {
|
|
6416
7696
|
child.on("error", (err) => {
|
|
6417
|
-
|
|
7697
|
+
resolve15({
|
|
6418
7698
|
ok: false,
|
|
6419
7699
|
command: "serve",
|
|
6420
7700
|
code: "TAP_INTERNAL_ERROR",
|
|
@@ -6424,7 +7704,7 @@ async function serveCommand(args) {
|
|
|
6424
7704
|
});
|
|
6425
7705
|
});
|
|
6426
7706
|
child.on("exit", (code) => {
|
|
6427
|
-
|
|
7707
|
+
resolve15({
|
|
6428
7708
|
ok: code === 0,
|
|
6429
7709
|
command: "serve",
|
|
6430
7710
|
code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
|
|
@@ -6437,8 +7717,10 @@ async function serveCommand(args) {
|
|
|
6437
7717
|
}
|
|
6438
7718
|
|
|
6439
7719
|
// src/commands/init-worktree.ts
|
|
6440
|
-
|
|
6441
|
-
|
|
7720
|
+
init_config();
|
|
7721
|
+
init_utils();
|
|
7722
|
+
import * as fs29 from "fs";
|
|
7723
|
+
import * as path30 from "path";
|
|
6442
7724
|
import { execSync as execSync5 } from "child_process";
|
|
6443
7725
|
var INIT_WORKTREE_HELP = `
|
|
6444
7726
|
Usage:
|
|
@@ -6475,7 +7757,7 @@ function run(cmd, opts) {
|
|
|
6475
7757
|
}
|
|
6476
7758
|
}
|
|
6477
7759
|
function toAbsolute(p) {
|
|
6478
|
-
const resolved =
|
|
7760
|
+
const resolved = path30.resolve(p);
|
|
6479
7761
|
return resolved.replace(/\\/g, "/");
|
|
6480
7762
|
}
|
|
6481
7763
|
function probeBun(candidate) {
|
|
@@ -6506,18 +7788,18 @@ function findBun() {
|
|
|
6506
7788
|
}
|
|
6507
7789
|
}
|
|
6508
7790
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
6509
|
-
const bunHome =
|
|
7791
|
+
const bunHome = path30.join(
|
|
6510
7792
|
home,
|
|
6511
7793
|
".bun",
|
|
6512
7794
|
"bin",
|
|
6513
7795
|
process.platform === "win32" ? "bun.exe" : "bun"
|
|
6514
7796
|
);
|
|
6515
|
-
if (
|
|
7797
|
+
if (fs29.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
|
|
6516
7798
|
return null;
|
|
6517
7799
|
}
|
|
6518
7800
|
function step1CreateWorktree(opts) {
|
|
6519
7801
|
log("Step 1/9: Creating worktree...");
|
|
6520
|
-
if (
|
|
7802
|
+
if (fs29.existsSync(opts.worktreePath)) {
|
|
6521
7803
|
logWarn(`Directory already exists: ${opts.worktreePath}`);
|
|
6522
7804
|
try {
|
|
6523
7805
|
run("git rev-parse --git-dir", { cwd: opts.worktreePath });
|
|
@@ -6579,22 +7861,22 @@ function step2MergeMain(opts, warnings) {
|
|
|
6579
7861
|
}
|
|
6580
7862
|
function step3CopyPermissions(opts, warnings) {
|
|
6581
7863
|
log("Step 3/9: Copying permissions...");
|
|
6582
|
-
const srcSettings =
|
|
7864
|
+
const srcSettings = path30.join(
|
|
6583
7865
|
opts.repoRoot,
|
|
6584
7866
|
".claude",
|
|
6585
7867
|
"settings.local.json"
|
|
6586
7868
|
);
|
|
6587
|
-
const destDir =
|
|
6588
|
-
const destSettings =
|
|
6589
|
-
if (!
|
|
7869
|
+
const destDir = path30.join(opts.worktreePath, ".claude");
|
|
7870
|
+
const destSettings = path30.join(destDir, "settings.local.json");
|
|
7871
|
+
if (!fs29.existsSync(srcSettings)) {
|
|
6590
7872
|
warn(
|
|
6591
7873
|
warnings,
|
|
6592
7874
|
"No .claude/settings.local.json found in main repo. Skipping."
|
|
6593
7875
|
);
|
|
6594
7876
|
return;
|
|
6595
7877
|
}
|
|
6596
|
-
|
|
6597
|
-
|
|
7878
|
+
fs29.mkdirSync(destDir, { recursive: true });
|
|
7879
|
+
fs29.copyFileSync(srcSettings, destSettings);
|
|
6598
7880
|
logSuccess("Copied settings.local.json");
|
|
6599
7881
|
try {
|
|
6600
7882
|
run("git update-index --skip-worktree .claude/settings.local.json", {
|
|
@@ -6619,7 +7901,7 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
6619
7901
|
const wtAbs = toAbsolute(opts.worktreePath);
|
|
6620
7902
|
const bunAbs = toAbsolute(bunPath);
|
|
6621
7903
|
const commsAbs = toAbsolute(opts.commsDir);
|
|
6622
|
-
const channelEntry =
|
|
7904
|
+
const channelEntry = path30.join(
|
|
6623
7905
|
wtAbs,
|
|
6624
7906
|
"packages/tap-plugin/channels/tap-comms.ts"
|
|
6625
7907
|
);
|
|
@@ -6636,8 +7918,8 @@ function step4GenerateMcpJson(opts, warnings) {
|
|
|
6636
7918
|
}
|
|
6637
7919
|
}
|
|
6638
7920
|
};
|
|
6639
|
-
const mcpPath =
|
|
6640
|
-
|
|
7921
|
+
const mcpPath = path30.join(opts.worktreePath, ".mcp.json");
|
|
7922
|
+
fs29.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
6641
7923
|
logSuccess(`.mcp.json generated (absolute paths + cwd)`);
|
|
6642
7924
|
log(` bun: ${bunAbs}`);
|
|
6643
7925
|
log(` comms: ${commsAbs}`);
|
|
@@ -6675,16 +7957,16 @@ function step6BuildEslintPlugin(opts, warnings) {
|
|
|
6675
7957
|
}
|
|
6676
7958
|
function step7VerifyComms(opts, warnings) {
|
|
6677
7959
|
log("Step 7/9: Verifying comms directory...");
|
|
6678
|
-
if (!
|
|
7960
|
+
if (!fs29.existsSync(opts.commsDir)) {
|
|
6679
7961
|
warn(warnings, `Comms directory not found: ${opts.commsDir}`);
|
|
6680
7962
|
warn(warnings, "Create it or run: npx @hua-labs/tap init");
|
|
6681
7963
|
return;
|
|
6682
7964
|
}
|
|
6683
7965
|
const requiredDirs = ["inbox", "findings", "reviews", "letters"];
|
|
6684
7966
|
for (const dir of requiredDirs) {
|
|
6685
|
-
const dirPath =
|
|
6686
|
-
if (!
|
|
6687
|
-
|
|
7967
|
+
const dirPath = path30.join(opts.commsDir, dir);
|
|
7968
|
+
if (!fs29.existsSync(dirPath)) {
|
|
7969
|
+
fs29.mkdirSync(dirPath, { recursive: true });
|
|
6688
7970
|
logSuccess(`Created ${dir}/`);
|
|
6689
7971
|
}
|
|
6690
7972
|
}
|
|
@@ -6743,17 +8025,17 @@ async function initWorktreeCommand(args) {
|
|
|
6743
8025
|
}
|
|
6744
8026
|
const repoRoot = findRepoRoot();
|
|
6745
8027
|
const { config } = resolveConfig({}, repoRoot);
|
|
6746
|
-
const branch = typeof flags["branch"] === "string" ? flags["branch"] :
|
|
8028
|
+
const branch = typeof flags["branch"] === "string" ? flags["branch"] : path30.basename(path30.resolve(worktreePath));
|
|
6747
8029
|
const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
|
|
6748
8030
|
const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
|
|
6749
8031
|
const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
|
|
6750
8032
|
const skipInstall = flags["skip-install"] === true;
|
|
6751
8033
|
const opts = {
|
|
6752
|
-
worktreePath:
|
|
8034
|
+
worktreePath: path30.resolve(worktreePath),
|
|
6753
8035
|
branch,
|
|
6754
8036
|
base,
|
|
6755
8037
|
mission,
|
|
6756
|
-
commsDir:
|
|
8038
|
+
commsDir: path30.resolve(commsDir),
|
|
6757
8039
|
skipInstall,
|
|
6758
8040
|
repoRoot
|
|
6759
8041
|
};
|
|
@@ -6799,6 +8081,7 @@ async function initWorktreeCommand(args) {
|
|
|
6799
8081
|
}
|
|
6800
8082
|
|
|
6801
8083
|
// src/commands/dashboard.ts
|
|
8084
|
+
init_utils();
|
|
6802
8085
|
function formatAge2(seconds) {
|
|
6803
8086
|
if (seconds === null) return "-";
|
|
6804
8087
|
if (seconds < 60) return `${seconds}s ago`;
|
|
@@ -6836,14 +8119,16 @@ function renderSnapshot(snapshot) {
|
|
|
6836
8119
|
if (snapshot.agents.length === 0) {
|
|
6837
8120
|
log(" (no heartbeats)");
|
|
6838
8121
|
} else {
|
|
8122
|
+
log(
|
|
8123
|
+
` ${"Agent".padEnd(18)} ${"Presence".padEnd(18)} ${"Lifecycle".padEnd(20)} ${"Idle"}`
|
|
8124
|
+
);
|
|
8125
|
+
log(
|
|
8126
|
+
` ${"\u2500".repeat(18)} ${"\u2500".repeat(18)} ${"\u2500".repeat(20)} ${"\u2500".repeat(12)}`
|
|
8127
|
+
);
|
|
6839
8128
|
for (const agent of snapshot.agents) {
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
)
|
|
6844
|
-
) : "unknown";
|
|
6845
|
-
const status = agent.status ?? "unknown";
|
|
6846
|
-
log(` ${agent.name.padEnd(12)} ${status.padEnd(10)} active ${activity}`);
|
|
8129
|
+
log(
|
|
8130
|
+
` ${truncate(agent.name, 18).padEnd(18)} ${agent.presence.padEnd(18)} ${String(agent.lifecycle ?? "-").padEnd(20)} ${formatAge2(agent.idleSeconds)}`
|
|
8131
|
+
);
|
|
6847
8132
|
}
|
|
6848
8133
|
}
|
|
6849
8134
|
log("");
|
|
@@ -6852,15 +8137,16 @@ function renderSnapshot(snapshot) {
|
|
|
6852
8137
|
log(" (none)");
|
|
6853
8138
|
} else {
|
|
6854
8139
|
log(
|
|
6855
|
-
` ${"Instance".padEnd(20)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Heartbeat"}`
|
|
8140
|
+
` ${"Instance".padEnd(20)} ${"Status".padEnd(10)} ${"Lifecycle".padEnd(20)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Heartbeat"}`
|
|
6856
8141
|
);
|
|
6857
8142
|
log(
|
|
6858
|
-
` ${"\u2500".repeat(20)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(12)}`
|
|
8143
|
+
` ${"\u2500".repeat(20)} ${"\u2500".repeat(10)} ${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(12)}`
|
|
6859
8144
|
);
|
|
6860
8145
|
for (const b of snapshot.bridges) {
|
|
6861
8146
|
const headlessTag = b.headless ? " [H]" : "";
|
|
8147
|
+
const lifecycle = b.lifecycle?.status ?? "-";
|
|
6862
8148
|
log(
|
|
6863
|
-
` ${truncate(b.instanceId + headlessTag, 20).padEnd(20)} ${formatStatus(b.status).padEnd(10)} ${(b.pid ? String(b.pid) : "-").padEnd(8)} ${(b.port ? String(b.port) : "-").padEnd(6)} ${formatAge2(b.heartbeatAge)}`
|
|
8149
|
+
` ${truncate(b.instanceId + headlessTag, 20).padEnd(20)} ${formatStatus(b.status).padEnd(10)} ${truncate(lifecycle, 20).padEnd(20)} ${(b.pid ? String(b.pid) : "-").padEnd(8)} ${(b.port ? String(b.port) : "-").padEnd(6)} ${formatAge2(b.heartbeatAge)}`
|
|
6864
8150
|
);
|
|
6865
8151
|
}
|
|
6866
8152
|
}
|
|
@@ -6974,18 +8260,21 @@ async function dashboardCommand(args) {
|
|
|
6974
8260
|
|
|
6975
8261
|
// src/commands/doctor.ts
|
|
6976
8262
|
import {
|
|
6977
|
-
existsSync as
|
|
6978
|
-
mkdirSync as
|
|
6979
|
-
readdirSync as
|
|
6980
|
-
readFileSync as
|
|
6981
|
-
renameSync as
|
|
8263
|
+
existsSync as existsSync28,
|
|
8264
|
+
mkdirSync as mkdirSync14,
|
|
8265
|
+
readdirSync as readdirSync8,
|
|
8266
|
+
readFileSync as readFileSync23,
|
|
8267
|
+
renameSync as renameSync14,
|
|
6982
8268
|
statSync as statSync3,
|
|
6983
|
-
unlinkSync as
|
|
6984
|
-
writeFileSync as
|
|
8269
|
+
unlinkSync as unlinkSync8,
|
|
8270
|
+
writeFileSync as writeFileSync16
|
|
6985
8271
|
} from "fs";
|
|
6986
8272
|
import { homedir as homedir3 } from "os";
|
|
6987
8273
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
6988
|
-
import { dirname as dirname15, join as
|
|
8274
|
+
import { dirname as dirname15, join as join27, resolve as resolve14 } from "path";
|
|
8275
|
+
init_config();
|
|
8276
|
+
init_drift_detector();
|
|
8277
|
+
init_utils();
|
|
6989
8278
|
var PASS = "pass";
|
|
6990
8279
|
var WARN = "warn";
|
|
6991
8280
|
var FAIL = "fail";
|
|
@@ -6999,7 +8288,7 @@ var CODEX_ENV_DRIFT_KEYS = [
|
|
|
6999
8288
|
];
|
|
7000
8289
|
var CODEX_SESSION_NEUTRAL_NAME = "<set-per-session>";
|
|
7001
8290
|
function normalizeComparablePath2(value) {
|
|
7002
|
-
return
|
|
8291
|
+
return resolve14(value).replace(/\\/g, "/").toLowerCase();
|
|
7003
8292
|
}
|
|
7004
8293
|
function samePath(left, right) {
|
|
7005
8294
|
return normalizeComparablePath2(left) === normalizeComparablePath2(right);
|
|
@@ -7017,10 +8306,10 @@ function appendWarningMessage(message, extra) {
|
|
|
7017
8306
|
return message.includes(extra) ? message : `${message}; ${extra}`;
|
|
7018
8307
|
}
|
|
7019
8308
|
function findCodexConfigPath3() {
|
|
7020
|
-
return
|
|
8309
|
+
return join27(homedir3(), ".codex", "config.toml");
|
|
7021
8310
|
}
|
|
7022
8311
|
function canonicalizeTrustPath3(targetPath) {
|
|
7023
|
-
let resolved =
|
|
8312
|
+
let resolved = resolve14(targetPath).replace(/\//g, "\\");
|
|
7024
8313
|
const driveRoot = /^[A-Za-z]:\\$/;
|
|
7025
8314
|
if (!driveRoot.test(resolved)) {
|
|
7026
8315
|
resolved = resolved.replace(/\\+$/g, "");
|
|
@@ -7032,10 +8321,10 @@ function trustSelector2(targetPath) {
|
|
|
7032
8321
|
}
|
|
7033
8322
|
function writeTomlAtomically(filePath, content) {
|
|
7034
8323
|
const dir = dirname15(filePath);
|
|
7035
|
-
|
|
8324
|
+
mkdirSync14(dir, { recursive: true });
|
|
7036
8325
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
7037
|
-
|
|
7038
|
-
|
|
8326
|
+
writeFileSync16(tmp, content, "utf-8");
|
|
8327
|
+
renameSync14(tmp, filePath);
|
|
7039
8328
|
}
|
|
7040
8329
|
function hasInstalledCodexInstance(state) {
|
|
7041
8330
|
return state ? Object.values(state.instances).some(
|
|
@@ -7043,7 +8332,7 @@ function hasInstalledCodexInstance(state) {
|
|
|
7043
8332
|
) : false;
|
|
7044
8333
|
}
|
|
7045
8334
|
function getCodexTrustTargets(repoRoot) {
|
|
7046
|
-
return [...new Set([repoRoot, process.cwd()].map((value) =>
|
|
8335
|
+
return [...new Set([repoRoot, process.cwd()].map((value) => resolve14(value)))];
|
|
7047
8336
|
}
|
|
7048
8337
|
function buildSessionNeutralCodexEnv(env) {
|
|
7049
8338
|
const neutralEnv = {
|
|
@@ -7087,7 +8376,7 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
7087
8376
|
spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
|
|
7088
8377
|
);
|
|
7089
8378
|
}
|
|
7090
|
-
const existingContent =
|
|
8379
|
+
const existingContent = existsSync28(spec.configPath) ? readFileSync23(spec.configPath, "utf-8") : "";
|
|
7091
8380
|
const existingTapEnvTable = extractTomlTable(
|
|
7092
8381
|
existingContent,
|
|
7093
8382
|
"mcp_servers.tap.env"
|
|
@@ -7121,7 +8410,8 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
7121
8410
|
"mcp_servers.tap",
|
|
7122
8411
|
{
|
|
7123
8412
|
command: spec.managed.command,
|
|
7124
|
-
args: spec.managed.args
|
|
8413
|
+
args: spec.managed.args,
|
|
8414
|
+
approval_mode: "auto"
|
|
7125
8415
|
},
|
|
7126
8416
|
extractTomlTable(existingContent, "mcp_servers.tap")
|
|
7127
8417
|
)
|
|
@@ -7153,22 +8443,22 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
7153
8443
|
return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
|
|
7154
8444
|
}
|
|
7155
8445
|
function countFiles(dir, ext = ".md") {
|
|
7156
|
-
if (!
|
|
8446
|
+
if (!existsSync28(dir)) return 0;
|
|
7157
8447
|
try {
|
|
7158
|
-
return
|
|
8448
|
+
return readdirSync8(dir).filter((f) => f.endsWith(ext)).length;
|
|
7159
8449
|
} catch {
|
|
7160
8450
|
return 0;
|
|
7161
8451
|
}
|
|
7162
8452
|
}
|
|
7163
8453
|
function recentFileCount(dir, withinMs) {
|
|
7164
|
-
if (!
|
|
8454
|
+
if (!existsSync28(dir)) return 0;
|
|
7165
8455
|
const cutoff = Date.now() - withinMs;
|
|
7166
8456
|
let count = 0;
|
|
7167
8457
|
try {
|
|
7168
|
-
for (const f of
|
|
8458
|
+
for (const f of readdirSync8(dir)) {
|
|
7169
8459
|
if (!f.endsWith(".md")) continue;
|
|
7170
8460
|
try {
|
|
7171
|
-
if (statSync3(
|
|
8461
|
+
if (statSync3(join27(dir, f)).mtimeMs > cutoff) count++;
|
|
7172
8462
|
} catch {
|
|
7173
8463
|
}
|
|
7174
8464
|
}
|
|
@@ -7177,19 +8467,19 @@ function recentFileCount(dir, withinMs) {
|
|
|
7177
8467
|
return count;
|
|
7178
8468
|
}
|
|
7179
8469
|
function loadDoctorHeartbeatStore(commsDir) {
|
|
7180
|
-
const heartbeatsPath =
|
|
7181
|
-
if (!
|
|
8470
|
+
const heartbeatsPath = join27(commsDir, "heartbeats.json");
|
|
8471
|
+
if (!existsSync28(heartbeatsPath)) return null;
|
|
7182
8472
|
try {
|
|
7183
|
-
return JSON.parse(
|
|
8473
|
+
return JSON.parse(readFileSync23(heartbeatsPath, "utf-8"));
|
|
7184
8474
|
} catch {
|
|
7185
8475
|
return null;
|
|
7186
8476
|
}
|
|
7187
8477
|
}
|
|
7188
8478
|
function saveDoctorHeartbeatStore(commsDir, store) {
|
|
7189
|
-
const heartbeatsPath =
|
|
8479
|
+
const heartbeatsPath = join27(commsDir, "heartbeats.json");
|
|
7190
8480
|
const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
7191
|
-
|
|
7192
|
-
|
|
8481
|
+
writeFileSync16(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
8482
|
+
renameSync14(tmp, heartbeatsPath);
|
|
7193
8483
|
}
|
|
7194
8484
|
function parseHeartbeatAgeMs(record, now) {
|
|
7195
8485
|
const raw = record.lastActivity ?? record.timestamp;
|
|
@@ -7198,7 +8488,7 @@ function parseHeartbeatAgeMs(record, now) {
|
|
|
7198
8488
|
if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
|
|
7199
8489
|
return Math.max(0, now - parsed);
|
|
7200
8490
|
}
|
|
7201
|
-
function
|
|
8491
|
+
function resolveHeartbeatInstanceId2(state, heartbeatId) {
|
|
7202
8492
|
if (!state) return null;
|
|
7203
8493
|
if (state.instances[heartbeatId]) return heartbeatId;
|
|
7204
8494
|
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
@@ -7214,7 +8504,7 @@ function collectStaleHeartbeatIds(commsDir, state, stateDir) {
|
|
|
7214
8504
|
const stale = [];
|
|
7215
8505
|
for (const [heartbeatId, heartbeat] of Object.entries(store)) {
|
|
7216
8506
|
const ageMs = parseHeartbeatAgeMs(heartbeat, now);
|
|
7217
|
-
const instanceId =
|
|
8507
|
+
const instanceId = resolveHeartbeatInstanceId2(state, heartbeatId);
|
|
7218
8508
|
const instance = instanceId ? state?.instances[instanceId] : null;
|
|
7219
8509
|
const bridgeBacked = instance?.bridgeMode === "app-server";
|
|
7220
8510
|
const bridgeRunning = bridgeBacked && instanceId ? isBridgeRunning(stateDir, instanceId) : false;
|
|
@@ -7252,10 +8542,10 @@ function checkComms(commsDir) {
|
|
|
7252
8542
|
const checks = [];
|
|
7253
8543
|
checks.push({
|
|
7254
8544
|
name: "comms directory",
|
|
7255
|
-
status:
|
|
7256
|
-
message:
|
|
7257
|
-
fix:
|
|
7258
|
-
|
|
8545
|
+
status: existsSync28(commsDir) ? PASS : FAIL,
|
|
8546
|
+
message: existsSync28(commsDir) ? commsDir : `Not found: ${commsDir}`,
|
|
8547
|
+
fix: existsSync28(commsDir) ? void 0 : () => {
|
|
8548
|
+
mkdirSync14(commsDir, { recursive: true });
|
|
7259
8549
|
return `Created ${commsDir}`;
|
|
7260
8550
|
}
|
|
7261
8551
|
});
|
|
@@ -7264,22 +8554,22 @@ function checkComms(commsDir) {
|
|
|
7264
8554
|
["reviews", false],
|
|
7265
8555
|
["findings", false]
|
|
7266
8556
|
]) {
|
|
7267
|
-
const dir =
|
|
7268
|
-
const exists =
|
|
8557
|
+
const dir = join27(commsDir, subdir);
|
|
8558
|
+
const exists = existsSync28(dir);
|
|
7269
8559
|
checks.push({
|
|
7270
8560
|
name: `${subdir} directory`,
|
|
7271
8561
|
status: exists ? PASS : required ? FAIL : WARN,
|
|
7272
8562
|
message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
|
|
7273
8563
|
fix: exists ? void 0 : () => {
|
|
7274
|
-
|
|
8564
|
+
mkdirSync14(dir, { recursive: true });
|
|
7275
8565
|
return `Created ${dir}`;
|
|
7276
8566
|
}
|
|
7277
8567
|
});
|
|
7278
8568
|
}
|
|
7279
|
-
const heartbeats =
|
|
7280
|
-
if (
|
|
8569
|
+
const heartbeats = join27(commsDir, "heartbeats.json");
|
|
8570
|
+
if (existsSync28(heartbeats)) {
|
|
7281
8571
|
try {
|
|
7282
|
-
const store = JSON.parse(
|
|
8572
|
+
const store = JSON.parse(readFileSync23(heartbeats, "utf-8"));
|
|
7283
8573
|
const agents = Object.keys(store);
|
|
7284
8574
|
const now = Date.now();
|
|
7285
8575
|
const active = agents.filter((a) => {
|
|
@@ -7393,9 +8683,9 @@ function checkInstances(repoRoot, stateDir, commsDir) {
|
|
|
7393
8683
|
}
|
|
7394
8684
|
}
|
|
7395
8685
|
}
|
|
7396
|
-
const pidPath =
|
|
8686
|
+
const pidPath = join27(stateDir, "pids", `bridge-${id}.json`);
|
|
7397
8687
|
try {
|
|
7398
|
-
|
|
8688
|
+
unlinkSync8(pidPath);
|
|
7399
8689
|
} catch {
|
|
7400
8690
|
}
|
|
7401
8691
|
const currentState = loadState(repoRoot);
|
|
@@ -7455,8 +8745,8 @@ function checkInstances(repoRoot, stateDir, commsDir) {
|
|
|
7455
8745
|
}
|
|
7456
8746
|
function checkMessageLifecycle(commsDir) {
|
|
7457
8747
|
const checks = [];
|
|
7458
|
-
const inbox =
|
|
7459
|
-
if (!
|
|
8748
|
+
const inbox = join27(commsDir, "inbox");
|
|
8749
|
+
if (!existsSync28(inbox)) {
|
|
7460
8750
|
checks.push({
|
|
7461
8751
|
name: "message flow",
|
|
7462
8752
|
status: FAIL,
|
|
@@ -7472,10 +8762,10 @@ function checkMessageLifecycle(commsDir) {
|
|
|
7472
8762
|
status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
|
|
7473
8763
|
message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
|
|
7474
8764
|
});
|
|
7475
|
-
const receiptsPath =
|
|
7476
|
-
if (
|
|
8765
|
+
const receiptsPath = join27(commsDir, "receipts", "receipts.json");
|
|
8766
|
+
if (existsSync28(receiptsPath)) {
|
|
7477
8767
|
try {
|
|
7478
|
-
const receipts = JSON.parse(
|
|
8768
|
+
const receipts = JSON.parse(readFileSync23(receiptsPath, "utf-8"));
|
|
7479
8769
|
const receiptCount = Object.keys(receipts).length;
|
|
7480
8770
|
checks.push({
|
|
7481
8771
|
name: "read receipts",
|
|
@@ -7494,8 +8784,8 @@ function checkMessageLifecycle(commsDir) {
|
|
|
7494
8784
|
}
|
|
7495
8785
|
function checkMcpServer(repoRoot) {
|
|
7496
8786
|
const checks = [];
|
|
7497
|
-
const mcpJson =
|
|
7498
|
-
if (!
|
|
8787
|
+
const mcpJson = join27(repoRoot, ".mcp.json");
|
|
8788
|
+
if (!existsSync28(mcpJson)) {
|
|
7499
8789
|
checks.push({
|
|
7500
8790
|
name: "MCP config (.mcp.json)",
|
|
7501
8791
|
status: WARN,
|
|
@@ -7505,7 +8795,7 @@ function checkMcpServer(repoRoot) {
|
|
|
7505
8795
|
}
|
|
7506
8796
|
let config;
|
|
7507
8797
|
try {
|
|
7508
|
-
config = JSON.parse(
|
|
8798
|
+
config = JSON.parse(readFileSync23(mcpJson, "utf-8"));
|
|
7509
8799
|
} catch {
|
|
7510
8800
|
checks.push({
|
|
7511
8801
|
name: "MCP config (.mcp.json)",
|
|
@@ -7548,7 +8838,7 @@ function checkMcpServer(repoRoot) {
|
|
|
7548
8838
|
});
|
|
7549
8839
|
if (hasTapComms.command) {
|
|
7550
8840
|
const cmd = hasTapComms.command;
|
|
7551
|
-
let cmdAvailable =
|
|
8841
|
+
let cmdAvailable = existsSync28(cmd);
|
|
7552
8842
|
if (!cmdAvailable) {
|
|
7553
8843
|
try {
|
|
7554
8844
|
const result = spawnSync6(cmd, ["--version"], {
|
|
@@ -7570,8 +8860,8 @@ function checkMcpServer(repoRoot) {
|
|
|
7570
8860
|
const mcpScript = hasTapComms.args[0];
|
|
7571
8861
|
checks.push({
|
|
7572
8862
|
name: "MCP server script",
|
|
7573
|
-
status:
|
|
7574
|
-
message:
|
|
8863
|
+
status: existsSync28(mcpScript) ? PASS : FAIL,
|
|
8864
|
+
message: existsSync28(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
7575
8865
|
});
|
|
7576
8866
|
if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
|
|
7577
8867
|
checks.push({
|
|
@@ -7604,8 +8894,8 @@ function checkMcpServer(repoRoot) {
|
|
|
7604
8894
|
} else {
|
|
7605
8895
|
checks.push({
|
|
7606
8896
|
name: "MCP TAP_COMMS_DIR",
|
|
7607
|
-
status:
|
|
7608
|
-
message:
|
|
8897
|
+
status: existsSync28(envCommsDir) ? PASS : FAIL,
|
|
8898
|
+
message: existsSync28(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
|
|
7609
8899
|
});
|
|
7610
8900
|
}
|
|
7611
8901
|
checks.push({
|
|
@@ -7622,7 +8912,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7622
8912
|
}
|
|
7623
8913
|
const checks = [];
|
|
7624
8914
|
const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
|
|
7625
|
-
if (!
|
|
8915
|
+
if (!existsSync28(spec.configPath)) {
|
|
7626
8916
|
checks.push({
|
|
7627
8917
|
name: "MCP config (~/.codex/config.toml)",
|
|
7628
8918
|
status: WARN,
|
|
@@ -7631,7 +8921,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7631
8921
|
});
|
|
7632
8922
|
return checks;
|
|
7633
8923
|
}
|
|
7634
|
-
const content =
|
|
8924
|
+
const content = readFileSync23(spec.configPath, "utf-8");
|
|
7635
8925
|
const tapTable = extractTomlTable(content, "mcp_servers.tap");
|
|
7636
8926
|
const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
|
|
7637
8927
|
const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
|
|
@@ -7683,6 +8973,14 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7683
8973
|
if (typeof actualAgentId === "string" && actualAgentId.trim()) {
|
|
7684
8974
|
issues.push(`concrete TAP_AGENT_ID persisted (${actualAgentId})`);
|
|
7685
8975
|
}
|
|
8976
|
+
if (tapTable) {
|
|
8977
|
+
const actualApprovalMode = selectedMain.approval_mode;
|
|
8978
|
+
if (typeof actualApprovalMode !== "string") {
|
|
8979
|
+
issues.push("approval_mode missing (expected auto)");
|
|
8980
|
+
} else if (actualApprovalMode !== "auto") {
|
|
8981
|
+
issues.push(`approval_mode drift (${actualApprovalMode})`);
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
7686
8984
|
for (const trustTarget of spec.trustTargets) {
|
|
7687
8985
|
const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
|
|
7688
8986
|
if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
|
|
@@ -7707,8 +9005,8 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7707
9005
|
}
|
|
7708
9006
|
function checkBridgeTurnHealth(repoRoot) {
|
|
7709
9007
|
const checks = [];
|
|
7710
|
-
const tmpDir =
|
|
7711
|
-
if (!
|
|
9008
|
+
const tmpDir = join27(repoRoot, ".tmp");
|
|
9009
|
+
if (!existsSync28(tmpDir)) return checks;
|
|
7712
9010
|
const state = loadState(repoRoot);
|
|
7713
9011
|
const activeMatchers = /* @__PURE__ */ new Set();
|
|
7714
9012
|
if (state) {
|
|
@@ -7721,7 +9019,7 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
7721
9019
|
}
|
|
7722
9020
|
let dirs;
|
|
7723
9021
|
try {
|
|
7724
|
-
dirs =
|
|
9022
|
+
dirs = readdirSync8(tmpDir).filter((d) => {
|
|
7725
9023
|
if (!d.startsWith("codex-app-server-bridge")) return false;
|
|
7726
9024
|
const suffix = d.replace("codex-app-server-bridge-", "");
|
|
7727
9025
|
if (activeMatchers.size === 0) return true;
|
|
@@ -7734,11 +9032,11 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
7734
9032
|
return checks;
|
|
7735
9033
|
}
|
|
7736
9034
|
for (const dir of dirs) {
|
|
7737
|
-
const heartbeatPath =
|
|
7738
|
-
if (!
|
|
9035
|
+
const heartbeatPath = join27(tmpDir, dir, "heartbeat.json");
|
|
9036
|
+
if (!existsSync28(heartbeatPath)) continue;
|
|
7739
9037
|
let heartbeat;
|
|
7740
9038
|
try {
|
|
7741
|
-
heartbeat = JSON.parse(
|
|
9039
|
+
heartbeat = JSON.parse(readFileSync23(heartbeatPath, "utf-8"));
|
|
7742
9040
|
} catch {
|
|
7743
9041
|
checks.push({
|
|
7744
9042
|
name: `turn: ${dir}`,
|
|
@@ -7870,11 +9168,71 @@ async function doctorCommand(args) {
|
|
|
7870
9168
|
const state = loadState(repoRoot);
|
|
7871
9169
|
const commsDir = overrides.commsDir ? config.commsDir : state?.commsDir ?? config.commsDir;
|
|
7872
9170
|
logHeader(`@hua-labs/tap doctor (v${version})${fixMode ? " --fix" : ""}`);
|
|
9171
|
+
function checkConfigDrift() {
|
|
9172
|
+
let driftResults;
|
|
9173
|
+
try {
|
|
9174
|
+
driftResults = checkAllDrift(config.stateDir, state);
|
|
9175
|
+
} catch (err) {
|
|
9176
|
+
return [
|
|
9177
|
+
{
|
|
9178
|
+
name: "drift:infrastructure",
|
|
9179
|
+
status: "warn",
|
|
9180
|
+
message: `Config drift check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
9181
|
+
}
|
|
9182
|
+
];
|
|
9183
|
+
}
|
|
9184
|
+
const checks = [];
|
|
9185
|
+
for (const result of driftResults) {
|
|
9186
|
+
for (const dc of result.checks) {
|
|
9187
|
+
const check = {
|
|
9188
|
+
name: `drift:${result.instanceId}:${dc.name}`,
|
|
9189
|
+
status: dc.status === "ok" ? "pass" : dc.autoFixable ? "warn" : "fail",
|
|
9190
|
+
message: dc.details ?? void 0
|
|
9191
|
+
};
|
|
9192
|
+
if (dc.autoFixable && dc.status !== "ok") {
|
|
9193
|
+
check.fix = () => {
|
|
9194
|
+
const {
|
|
9195
|
+
loadInstanceConfig: loadInst,
|
|
9196
|
+
saveInstanceConfig: saveInst
|
|
9197
|
+
} = (init_instance_config(), __toCommonJS(instance_config_exports));
|
|
9198
|
+
const {
|
|
9199
|
+
computeFileHash: hashFile
|
|
9200
|
+
} = (init_drift_detector(), __toCommonJS(drift_detector_exports));
|
|
9201
|
+
const instConfig = loadInst(config.stateDir, result.instanceId);
|
|
9202
|
+
if (!instConfig || !state) {
|
|
9203
|
+
return `Skipped: instance config not found for ${result.instanceId}`;
|
|
9204
|
+
}
|
|
9205
|
+
const inst = state.instances[result.instanceId];
|
|
9206
|
+
if (!inst) {
|
|
9207
|
+
return `Skipped: instance not in state.json for ${result.instanceId}`;
|
|
9208
|
+
}
|
|
9209
|
+
inst.agentName = instConfig.agentName;
|
|
9210
|
+
inst.port = instConfig.port;
|
|
9211
|
+
inst.configHash = instConfig.configHash;
|
|
9212
|
+
inst.configSourceFile = inst.configSourceFile || join27(config.stateDir, "instances", `${result.instanceId}.json`);
|
|
9213
|
+
saveState(repoRoot, state);
|
|
9214
|
+
if (inst.configPath && existsSync28(inst.configPath)) {
|
|
9215
|
+
const currentHash = hashFile(inst.configPath);
|
|
9216
|
+
if (instConfig.runtimeConfigHash !== currentHash) {
|
|
9217
|
+
instConfig.runtimeConfigHash = currentHash;
|
|
9218
|
+
instConfig.lastSyncedToRuntime = (/* @__PURE__ */ new Date()).toISOString();
|
|
9219
|
+
saveInst(config.stateDir, instConfig);
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
return `Synced state.json + runtime hash for ${result.instanceId}`;
|
|
9223
|
+
};
|
|
9224
|
+
}
|
|
9225
|
+
checks.push(check);
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
return checks;
|
|
9229
|
+
}
|
|
7873
9230
|
function runAllChecks() {
|
|
7874
9231
|
const checks = [];
|
|
7875
9232
|
checks.push(...checkComms(commsDir));
|
|
7876
9233
|
checks.push(...checkStaleHeartbeats(repoRoot, commsDir, config.stateDir));
|
|
7877
9234
|
checks.push(...checkInstances(repoRoot, config.stateDir, commsDir));
|
|
9235
|
+
checks.push(...checkConfigDrift());
|
|
7878
9236
|
checks.push(...checkMessageLifecycle(commsDir));
|
|
7879
9237
|
checks.push(...checkMcpServer(repoRoot));
|
|
7880
9238
|
checks.push(...checkCodexConfig(repoRoot, commsDir));
|
|
@@ -7885,6 +9243,7 @@ async function doctorCommand(args) {
|
|
|
7885
9243
|
for (const section of [
|
|
7886
9244
|
"Comms",
|
|
7887
9245
|
"Instances",
|
|
9246
|
+
"Config Drift",
|
|
7888
9247
|
"Messages",
|
|
7889
9248
|
"MCP",
|
|
7890
9249
|
"Turns"
|
|
@@ -7903,6 +9262,7 @@ async function doctorCommand(args) {
|
|
|
7903
9262
|
Instances: initialChecks.filter(
|
|
7904
9263
|
(c) => c.name.startsWith("bridge:") || c.name.startsWith("instance:") || c.name === "tap state"
|
|
7905
9264
|
),
|
|
9265
|
+
"Config Drift": initialChecks.filter((c) => c.name.startsWith("drift:")),
|
|
7906
9266
|
Messages: initialChecks.filter(
|
|
7907
9267
|
(c) => ["message flow", "read receipts"].includes(c.name)
|
|
7908
9268
|
),
|
|
@@ -7970,9 +9330,10 @@ async function doctorCommand(args) {
|
|
|
7970
9330
|
}
|
|
7971
9331
|
|
|
7972
9332
|
// src/commands/comms.ts
|
|
9333
|
+
init_utils();
|
|
7973
9334
|
import { execSync as execSync6, spawnSync as spawnSync7 } from "child_process";
|
|
7974
|
-
import * as
|
|
7975
|
-
import * as
|
|
9335
|
+
import * as fs30 from "fs";
|
|
9336
|
+
import * as path31 from "path";
|
|
7976
9337
|
var COMMS_HELP = `
|
|
7977
9338
|
Usage:
|
|
7978
9339
|
tap comms <subcommand>
|
|
@@ -7986,7 +9347,7 @@ Examples:
|
|
|
7986
9347
|
npx @hua-labs/tap comms push
|
|
7987
9348
|
`.trim();
|
|
7988
9349
|
function isGitRepo(dir) {
|
|
7989
|
-
return
|
|
9350
|
+
return fs30.existsSync(path31.join(dir, ".git"));
|
|
7990
9351
|
}
|
|
7991
9352
|
function commsPull(commsDir) {
|
|
7992
9353
|
logHeader("tap comms pull");
|
|
@@ -8134,6 +9495,7 @@ async function commsCommand(args) {
|
|
|
8134
9495
|
}
|
|
8135
9496
|
|
|
8136
9497
|
// src/commands/watch.ts
|
|
9498
|
+
init_utils();
|
|
8137
9499
|
var WATCH_HELP = `
|
|
8138
9500
|
Usage:
|
|
8139
9501
|
tap watch [options]
|
|
@@ -8155,7 +9517,7 @@ Examples:
|
|
|
8155
9517
|
npx @hua-labs/tap watch --stuck-threshold 120 # 2 min threshold
|
|
8156
9518
|
`.trim();
|
|
8157
9519
|
function delay2(ms) {
|
|
8158
|
-
return new Promise((
|
|
9520
|
+
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
8159
9521
|
}
|
|
8160
9522
|
async function watchCommand(args) {
|
|
8161
9523
|
const { flags } = parseArgs(args);
|
|
@@ -8244,8 +9606,8 @@ async function watchCommand(args) {
|
|
|
8244
9606
|
import * as http from "http";
|
|
8245
9607
|
|
|
8246
9608
|
// src/engine/missions.ts
|
|
8247
|
-
import * as
|
|
8248
|
-
import * as
|
|
9609
|
+
import * as fs31 from "fs";
|
|
9610
|
+
import * as path32 from "path";
|
|
8249
9611
|
function parseStatus(raw) {
|
|
8250
9612
|
const trimmed = raw.trim();
|
|
8251
9613
|
if (trimmed.includes("active")) return "active";
|
|
@@ -8271,10 +9633,10 @@ function parseRow(line) {
|
|
|
8271
9633
|
return { id: id.toUpperCase(), title, branch, status, owner };
|
|
8272
9634
|
}
|
|
8273
9635
|
function parseMissionsFile(repoRoot) {
|
|
8274
|
-
const missionsPath =
|
|
9636
|
+
const missionsPath = path32.join(repoRoot, "docs", "missions", "MISSIONS.md");
|
|
8275
9637
|
let content;
|
|
8276
9638
|
try {
|
|
8277
|
-
content =
|
|
9639
|
+
content = fs31.readFileSync(missionsPath, "utf-8");
|
|
8278
9640
|
} catch {
|
|
8279
9641
|
return [];
|
|
8280
9642
|
}
|
|
@@ -8348,6 +9710,8 @@ function fetchPrs(repoRoot) {
|
|
|
8348
9710
|
}
|
|
8349
9711
|
|
|
8350
9712
|
// src/commands/gui.ts
|
|
9713
|
+
init_config();
|
|
9714
|
+
init_utils();
|
|
8351
9715
|
var GUI_HELP = `
|
|
8352
9716
|
Usage:
|
|
8353
9717
|
tap gui [options]
|
|
@@ -8369,7 +9733,7 @@ function esc(str) {
|
|
|
8369
9733
|
}
|
|
8370
9734
|
function buildHtml(snapshot, turnData) {
|
|
8371
9735
|
const agentRows = snapshot.agents.map(
|
|
8372
|
-
(a) => `<tr><td>${esc(a.name)}</td><td class="${a.
|
|
9736
|
+
(a) => `<tr><td>${esc(a.name)}</td><td class="${a.presence === "bridge-live" ? "ok" : a.presence === "bridge-stale" ? "warn" : "off"}">${esc(a.presence)}</td><td>${esc(a.lifecycle ?? "-")}</td><td>${a.lastActivity ? esc(new Date(a.lastActivity).toLocaleTimeString()) : "-"}</td></tr>`
|
|
8373
9737
|
).join("\n");
|
|
8374
9738
|
const bridgeRows = snapshot.bridges.map((b) => {
|
|
8375
9739
|
const turn = turnData[b.instanceId];
|
|
@@ -8407,8 +9771,8 @@ function buildHtml(snapshot, turnData) {
|
|
|
8407
9771
|
|
|
8408
9772
|
<h2>Agents</h2>
|
|
8409
9773
|
<table>
|
|
8410
|
-
<tr><th>Name</th><th>
|
|
8411
|
-
${agentRows || '<tr><td colspan="
|
|
9774
|
+
<tr><th>Name</th><th>Presence</th><th>Lifecycle</th><th>Last Activity</th></tr>
|
|
9775
|
+
${agentRows || '<tr><td colspan="4" class="off">No agents</td></tr>'}
|
|
8412
9776
|
</table>
|
|
8413
9777
|
|
|
8414
9778
|
<h2>Bridges</h2>
|
|
@@ -8664,10 +10028,10 @@ async function guiCommand(args) {
|
|
|
8664
10028
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8665
10029
|
res.end(buildHtml(snapshot, turnData));
|
|
8666
10030
|
});
|
|
8667
|
-
return new Promise((
|
|
10031
|
+
return new Promise((resolve15) => {
|
|
8668
10032
|
server.on("error", (err) => {
|
|
8669
10033
|
if (err.code === "EADDRINUSE") {
|
|
8670
|
-
|
|
10034
|
+
resolve15({
|
|
8671
10035
|
ok: false,
|
|
8672
10036
|
command: "gui",
|
|
8673
10037
|
code: "TAP_PORT_IN_USE",
|
|
@@ -8676,7 +10040,7 @@ async function guiCommand(args) {
|
|
|
8676
10040
|
data: {}
|
|
8677
10041
|
});
|
|
8678
10042
|
} else {
|
|
8679
|
-
|
|
10043
|
+
resolve15({
|
|
8680
10044
|
ok: false,
|
|
8681
10045
|
command: "gui",
|
|
8682
10046
|
code: "TAP_GUI_ERROR",
|
|
@@ -8696,6 +10060,7 @@ async function guiCommand(args) {
|
|
|
8696
10060
|
}
|
|
8697
10061
|
|
|
8698
10062
|
// src/output.ts
|
|
10063
|
+
init_utils();
|
|
8699
10064
|
function emitResult(result, jsonMode) {
|
|
8700
10065
|
if (jsonMode) {
|
|
8701
10066
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -8724,6 +10089,9 @@ function extractJsonFlag(args) {
|
|
|
8724
10089
|
return { jsonMode, cleanArgs };
|
|
8725
10090
|
}
|
|
8726
10091
|
|
|
10092
|
+
// src/cli.ts
|
|
10093
|
+
init_utils();
|
|
10094
|
+
|
|
8727
10095
|
// src/cli-suggest.ts
|
|
8728
10096
|
var COMMANDS = [
|
|
8729
10097
|
"init",
|