@snowyroad/braid 0.76.1 → 0.77.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/dist/cli.js
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
validateReplacementToken,
|
|
67
67
|
verifyCardSignature,
|
|
68
68
|
wsToHttp
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-SKUYOAV5.js";
|
|
70
70
|
|
|
71
71
|
// src/health.ts
|
|
72
72
|
var CEILING_STRIKES_TO_DEAD = 3;
|
|
@@ -5030,9 +5030,7 @@ import {
|
|
|
5030
5030
|
writeFileSync as writeFileSync2,
|
|
5031
5031
|
renameSync as renameSync2,
|
|
5032
5032
|
chmodSync as chmodSync2,
|
|
5033
|
-
|
|
5034
|
-
statSync as statSync2,
|
|
5035
|
-
existsSync as existsSync2
|
|
5033
|
+
statSync as statSync2
|
|
5036
5034
|
} from "fs";
|
|
5037
5035
|
import { createHash } from "crypto";
|
|
5038
5036
|
import { homedir as homedir3 } from "os";
|
|
@@ -5051,37 +5049,9 @@ function cardCacheReadPath(cfg) {
|
|
|
5051
5049
|
["card-cache", relayHostSegment(cfg.relayWsUrl), `${safe2(cfg.agentName)}.json`]
|
|
5052
5050
|
);
|
|
5053
5051
|
}
|
|
5054
|
-
function collectSkillMdPaths(root) {
|
|
5055
|
-
const out = [];
|
|
5056
|
-
if (!existsSync2(root)) return out;
|
|
5057
|
-
function walk(dir) {
|
|
5058
|
-
let entries;
|
|
5059
|
-
try {
|
|
5060
|
-
entries = readdirSync2(dir);
|
|
5061
|
-
} catch {
|
|
5062
|
-
return;
|
|
5063
|
-
}
|
|
5064
|
-
for (const entry of entries) {
|
|
5065
|
-
const full = join4(dir, entry);
|
|
5066
|
-
let st;
|
|
5067
|
-
try {
|
|
5068
|
-
st = statSync2(full);
|
|
5069
|
-
} catch {
|
|
5070
|
-
continue;
|
|
5071
|
-
}
|
|
5072
|
-
if (st.isDirectory()) {
|
|
5073
|
-
walk(full);
|
|
5074
|
-
} else if (entry === "SKILL.md") {
|
|
5075
|
-
out.push(full);
|
|
5076
|
-
}
|
|
5077
|
-
}
|
|
5078
|
-
}
|
|
5079
|
-
walk(root);
|
|
5080
|
-
return out;
|
|
5081
|
-
}
|
|
5082
5052
|
function computeCapabilityFingerprint(cfg, skillsDir = process.env.BRAID_SKILLS_DIR ?? join4(homedir3(), ".claude", "skills"), pluginsDir = process.env.BRAID_PLUGINS_DIR ?? join4(homedir3(), ".claude", "plugins")) {
|
|
5083
|
-
const paths1 =
|
|
5084
|
-
const paths2 =
|
|
5053
|
+
const paths1 = collectSkillMd(skillsDir);
|
|
5054
|
+
const paths2 = collectSkillMd(pluginsDir);
|
|
5085
5055
|
const allPaths = [...paths1, ...paths2].sort();
|
|
5086
5056
|
const pathEntries = allPaths.map((p) => {
|
|
5087
5057
|
let mtimeMs = 0;
|
|
@@ -5143,6 +5113,67 @@ function saveCachedCard(cfg, fingerprint, unsignedCard) {
|
|
|
5143
5113
|
}
|
|
5144
5114
|
}
|
|
5145
5115
|
|
|
5116
|
+
// src/cardRefresh.ts
|
|
5117
|
+
var CARD_REFRESH_INTERVAL_MS = 3e5;
|
|
5118
|
+
var JITTER_RATIO = 0.2;
|
|
5119
|
+
function computeCardRefreshDelay(intervalMs, random) {
|
|
5120
|
+
return Math.round(intervalMs * (1 - JITTER_RATIO + 2 * JITTER_RATIO * random()));
|
|
5121
|
+
}
|
|
5122
|
+
function startCardRefreshLoop(initialFingerprint, deps) {
|
|
5123
|
+
const intervalMs = deps.intervalMs ?? CARD_REFRESH_INTERVAL_MS;
|
|
5124
|
+
const random = deps.random ?? Math.random;
|
|
5125
|
+
const log2 = deps.log ?? ((m) => console.warn(m));
|
|
5126
|
+
let lastPushed = initialFingerprint;
|
|
5127
|
+
let warnedEmptyFor = null;
|
|
5128
|
+
let stopped = false;
|
|
5129
|
+
let timer = null;
|
|
5130
|
+
const schedule = () => {
|
|
5131
|
+
if (stopped) return;
|
|
5132
|
+
timer = setTimeout(() => {
|
|
5133
|
+
void tick();
|
|
5134
|
+
}, computeCardRefreshDelay(intervalMs, random));
|
|
5135
|
+
timer.unref?.();
|
|
5136
|
+
};
|
|
5137
|
+
const tick = async () => {
|
|
5138
|
+
try {
|
|
5139
|
+
const fp = deps.computeFingerprint();
|
|
5140
|
+
if (fp && fp !== lastPushed) {
|
|
5141
|
+
const fields = deps.buildFromDisk();
|
|
5142
|
+
if (!fields) {
|
|
5143
|
+
if (warnedEmptyFor !== fp) {
|
|
5144
|
+
warnedEmptyFor = fp;
|
|
5145
|
+
log2("[braid] card refresh: skills changed but disk build yielded nothing \u2014 keeping the current card");
|
|
5146
|
+
}
|
|
5147
|
+
} else {
|
|
5148
|
+
const unsigned = buildPartialCard(deps.agentName, fields);
|
|
5149
|
+
let card = unsigned;
|
|
5150
|
+
if (deps.signCard) card = await deps.signCard(card);
|
|
5151
|
+
await deps.putAgentCard(card, "full");
|
|
5152
|
+
deps.setCard(unsigned);
|
|
5153
|
+
try {
|
|
5154
|
+
deps.cacheSave(fp, unsigned);
|
|
5155
|
+
} catch {
|
|
5156
|
+
}
|
|
5157
|
+
lastPushed = fp;
|
|
5158
|
+
warnedEmptyFor = null;
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
} catch (e) {
|
|
5162
|
+
log2(`[braid] card refresh: tick failed (will retry): ${String(e)}`);
|
|
5163
|
+
} finally {
|
|
5164
|
+
schedule();
|
|
5165
|
+
}
|
|
5166
|
+
};
|
|
5167
|
+
schedule();
|
|
5168
|
+
return () => {
|
|
5169
|
+
stopped = true;
|
|
5170
|
+
if (timer) {
|
|
5171
|
+
clearTimeout(timer);
|
|
5172
|
+
timer = null;
|
|
5173
|
+
}
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5146
5177
|
// src/shutdown.ts
|
|
5147
5178
|
function restartHint(agentName) {
|
|
5148
5179
|
const cmdName = isShellSafeName(agentName) ? agentName : "<agent-name>";
|
|
@@ -5217,11 +5248,11 @@ function installCrashGuard() {
|
|
|
5217
5248
|
}
|
|
5218
5249
|
|
|
5219
5250
|
// src/service/needsHumanMarker.ts
|
|
5220
|
-
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as
|
|
5251
|
+
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
5221
5252
|
import { join as join7 } from "path";
|
|
5222
5253
|
|
|
5223
5254
|
// src/service/install.ts
|
|
5224
|
-
import { existsSync as
|
|
5255
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
5225
5256
|
import { join as join6 } from "path";
|
|
5226
5257
|
|
|
5227
5258
|
// src/service/runtime.ts
|
|
@@ -5230,13 +5261,13 @@ import {
|
|
|
5230
5261
|
mkdirSync as mkdirSync3,
|
|
5231
5262
|
readFileSync as readFileSync4,
|
|
5232
5263
|
writeFileSync as writeFileSync3,
|
|
5233
|
-
readdirSync as
|
|
5264
|
+
readdirSync as readdirSync2,
|
|
5234
5265
|
renameSync as renameSync3,
|
|
5235
5266
|
rmSync,
|
|
5236
5267
|
symlinkSync,
|
|
5237
5268
|
readlinkSync,
|
|
5238
5269
|
lstatSync as lstatSync2,
|
|
5239
|
-
existsSync as
|
|
5270
|
+
existsSync as existsSync2
|
|
5240
5271
|
} from "fs";
|
|
5241
5272
|
import { execFileSync } from "child_process";
|
|
5242
5273
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -5250,18 +5281,18 @@ function defaultBraidDir() {
|
|
|
5250
5281
|
function runtimeRootForRead(braidDir, platform) {
|
|
5251
5282
|
const primary = join5(braidDir, "runtime");
|
|
5252
5283
|
const markerOf = (root) => platform === "win32" ? currentTxtPath(root) : join5(root, "current");
|
|
5253
|
-
if (
|
|
5284
|
+
if (existsSync2(markerOf(primary))) return primary;
|
|
5254
5285
|
const override = process.env.BRAID_CONFIG_DIR?.trim();
|
|
5255
5286
|
if (!override && braidDir === join5(homedir4(), ".braid")) {
|
|
5256
5287
|
const legacy = join5(homedir4(), ".arp", "runtime");
|
|
5257
|
-
if (
|
|
5288
|
+
if (existsSync2(markerOf(legacy))) return legacy;
|
|
5258
5289
|
}
|
|
5259
5290
|
return primary;
|
|
5260
5291
|
}
|
|
5261
5292
|
function runningPackageRoot() {
|
|
5262
5293
|
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
5263
5294
|
for (let i = 0; i < 6; i++) {
|
|
5264
|
-
if (
|
|
5295
|
+
if (existsSync2(join5(dir, "package.json")) && existsSync2(join5(dir, "dist"))) return dir;
|
|
5265
5296
|
const parent = dirname4(dir);
|
|
5266
5297
|
if (parent === dir) break;
|
|
5267
5298
|
dir = parent;
|
|
@@ -5290,7 +5321,7 @@ function pinCurrentRuntime(opts = {}) {
|
|
|
5290
5321
|
cpSync(join5(sourceDir, "dist"), join5(versionDir, "dist"), { recursive: true, force: true });
|
|
5291
5322
|
cpSync(join5(sourceDir, "package.json"), join5(versionDir, "package.json"), { force: true });
|
|
5292
5323
|
const shrinkwrapSrc = join5(sourceDir, "npm-shrinkwrap.json");
|
|
5293
|
-
const hasShrinkwrap =
|
|
5324
|
+
const hasShrinkwrap = existsSync2(shrinkwrapSrc);
|
|
5294
5325
|
if (hasShrinkwrap) {
|
|
5295
5326
|
cpSync(shrinkwrapSrc, join5(versionDir, "npm-shrinkwrap.json"), { force: true });
|
|
5296
5327
|
}
|
|
@@ -5318,7 +5349,7 @@ function populateNodeModules(sourceDir, versionDir, prodDeps, hasShrinkwrap, exe
|
|
|
5318
5349
|
if (plainTree) {
|
|
5319
5350
|
rmSync(dest, { recursive: true, force: true });
|
|
5320
5351
|
cpSync(plainTree, dest, { recursive: true, force: true });
|
|
5321
|
-
if (
|
|
5352
|
+
if (existsSync2(join5(dest, marker))) return "copy";
|
|
5322
5353
|
rmSync(dest, { recursive: true, force: true });
|
|
5323
5354
|
}
|
|
5324
5355
|
return npmInstallProdDeps(versionDir, marker, hasShrinkwrap, exec);
|
|
@@ -5328,7 +5359,7 @@ function findCopyableNodeModules(sourceDir, marker) {
|
|
|
5328
5359
|
for (let i = 0; i < 8; i++) {
|
|
5329
5360
|
const nm = join5(dir, "node_modules");
|
|
5330
5361
|
const markerPath = join5(nm, marker);
|
|
5331
|
-
if (
|
|
5362
|
+
if (existsSync2(markerPath)) {
|
|
5332
5363
|
if (!lstatSync2(markerPath).isSymbolicLink()) return nm;
|
|
5333
5364
|
return null;
|
|
5334
5365
|
}
|
|
@@ -5356,7 +5387,7 @@ function npmInstallProdDeps(versionDir, marker, hasShrinkwrap, exec) {
|
|
|
5356
5387
|
`pinned runtime at ${versionDir} could not obtain its production dependencies (\`npm ${args[0]}\` failed \u2014 npm missing, no network, or shrinkwrap/platform mismatch?). Fix and re-run, or the service will crash-loop with ERR_MODULE_NOT_FOUND. Underlying error: ${detail}`
|
|
5357
5388
|
);
|
|
5358
5389
|
}
|
|
5359
|
-
if (!
|
|
5390
|
+
if (!existsSync2(join5(versionDir, "node_modules", marker))) {
|
|
5360
5391
|
throw new Error(
|
|
5361
5392
|
`pinned runtime at ${versionDir} is missing dependency "${marker}" after \`npm ${args[0]}\`. Refusing to leave a runtime that will crash-loop with ERR_MODULE_NOT_FOUND.`
|
|
5362
5393
|
);
|
|
@@ -5368,7 +5399,7 @@ function pruneOldVersions(runtimeRoot, currentVersion, previousVersion) {
|
|
|
5368
5399
|
if (previousVersion && previousVersion !== currentVersion) keep.add(previousVersion);
|
|
5369
5400
|
let entries;
|
|
5370
5401
|
try {
|
|
5371
|
-
entries =
|
|
5402
|
+
entries = readdirSync2(runtimeRoot);
|
|
5372
5403
|
} catch {
|
|
5373
5404
|
return;
|
|
5374
5405
|
}
|
|
@@ -5445,7 +5476,7 @@ function installedClosureRecord(q = {}) {
|
|
|
5445
5476
|
|
|
5446
5477
|
// src/service/socket.ts
|
|
5447
5478
|
import * as net from "net";
|
|
5448
|
-
import { mkdirSync as mkdirSync4, chmodSync as chmodSync3, existsSync as
|
|
5479
|
+
import { mkdirSync as mkdirSync4, chmodSync as chmodSync3, existsSync as existsSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
5449
5480
|
import { dirname as dirname5 } from "path";
|
|
5450
5481
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
5451
5482
|
import { spawnSync as nodeSpawnSync2 } from "child_process";
|
|
@@ -5461,7 +5492,7 @@ function startControlSocket(opts) {
|
|
|
5461
5492
|
chmodSync3(dir, 448);
|
|
5462
5493
|
} catch {
|
|
5463
5494
|
}
|
|
5464
|
-
if (
|
|
5495
|
+
if (existsSync3(opts.sockPath)) {
|
|
5465
5496
|
if (!opts.ownsLock()) {
|
|
5466
5497
|
return Promise.reject(
|
|
5467
5498
|
new Error(
|
|
@@ -5532,13 +5563,13 @@ function startControlSocket(opts) {
|
|
|
5532
5563
|
server.close(() => {
|
|
5533
5564
|
if (!isWin32) {
|
|
5534
5565
|
try {
|
|
5535
|
-
if (
|
|
5566
|
+
if (existsSync3(opts.sockPath)) unlinkSync(opts.sockPath);
|
|
5536
5567
|
} catch {
|
|
5537
5568
|
}
|
|
5538
5569
|
}
|
|
5539
5570
|
if (opts.tokenPath) {
|
|
5540
5571
|
try {
|
|
5541
|
-
if (
|
|
5572
|
+
if (existsSync3(opts.tokenPath)) unlinkSync(opts.tokenPath);
|
|
5542
5573
|
} catch {
|
|
5543
5574
|
}
|
|
5544
5575
|
}
|
|
@@ -5600,7 +5631,7 @@ async function probeStatus(sockPath, timeoutMs = 1500, token) {
|
|
|
5600
5631
|
}
|
|
5601
5632
|
function socketLive(sockPath, platform = process.platform, timeoutMs = 1e3) {
|
|
5602
5633
|
if (platform !== "win32") {
|
|
5603
|
-
return Promise.resolve(
|
|
5634
|
+
return Promise.resolve(existsSync3(sockPath));
|
|
5604
5635
|
}
|
|
5605
5636
|
return new Promise((resolve2) => {
|
|
5606
5637
|
let settled = false;
|
|
@@ -5625,6 +5656,8 @@ function handleConnection(conn, opts, token, log2) {
|
|
|
5625
5656
|
conn.setEncoding("utf8");
|
|
5626
5657
|
let buf = "";
|
|
5627
5658
|
let dispatched = false;
|
|
5659
|
+
let onLine = null;
|
|
5660
|
+
let ignoredBytes = 0;
|
|
5628
5661
|
const writeLine = (obj) => {
|
|
5629
5662
|
try {
|
|
5630
5663
|
conn.write(JSON.stringify(obj) + "\n");
|
|
@@ -5637,19 +5670,7 @@ function handleConnection(conn, opts, token, log2) {
|
|
|
5637
5670
|
};
|
|
5638
5671
|
conn.on("error", () => {
|
|
5639
5672
|
});
|
|
5640
|
-
|
|
5641
|
-
if (dispatched) return;
|
|
5642
|
-
buf += chunk;
|
|
5643
|
-
const nl = buf.indexOf("\n");
|
|
5644
|
-
if (nl < 0) {
|
|
5645
|
-
if (buf.length > 64 * 1024) {
|
|
5646
|
-
dispatched = true;
|
|
5647
|
-
closeUnknown();
|
|
5648
|
-
}
|
|
5649
|
-
return;
|
|
5650
|
-
}
|
|
5651
|
-
dispatched = true;
|
|
5652
|
-
const line = buf.slice(0, nl);
|
|
5673
|
+
const dispatchFirst = (line) => {
|
|
5653
5674
|
let req;
|
|
5654
5675
|
try {
|
|
5655
5676
|
req = JSON.parse(line);
|
|
@@ -5676,6 +5697,10 @@ function handleConnection(conn, opts, token, log2) {
|
|
|
5676
5697
|
conn.end();
|
|
5677
5698
|
return;
|
|
5678
5699
|
}
|
|
5700
|
+
if (opts.handoff) {
|
|
5701
|
+
const g = opts.handoff.gateSnapshot();
|
|
5702
|
+
if (g.state !== "idle") payload.handoff = g.state;
|
|
5703
|
+
}
|
|
5679
5704
|
writeLine(payload);
|
|
5680
5705
|
conn.end();
|
|
5681
5706
|
return;
|
|
@@ -5685,7 +5710,68 @@ function handleConnection(conn, opts, token, log2) {
|
|
|
5685
5710
|
startAttachStream(conn, opts.hub, channelId);
|
|
5686
5711
|
return;
|
|
5687
5712
|
}
|
|
5713
|
+
if (verb === "handoff") {
|
|
5714
|
+
if (!opts.handoff) {
|
|
5715
|
+
closeUnknown();
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
const channelId = typeof req.channelId === "string" ? req.channelId : void 0;
|
|
5719
|
+
onLine = startHandoffConversation(conn, opts.handoff, channelId, writeLine, log2);
|
|
5720
|
+
return;
|
|
5721
|
+
}
|
|
5722
|
+
if (verb === "handoff.recover") {
|
|
5723
|
+
if (!opts.handoff) {
|
|
5724
|
+
closeUnknown();
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
onLine = startRecoverConversation(conn, opts.handoff, writeLine, log2);
|
|
5728
|
+
return;
|
|
5729
|
+
}
|
|
5688
5730
|
closeUnknown();
|
|
5731
|
+
};
|
|
5732
|
+
conn.on("data", (chunk) => {
|
|
5733
|
+
buf += chunk;
|
|
5734
|
+
if (dispatched && !onLine) {
|
|
5735
|
+
ignoredBytes += chunk.length;
|
|
5736
|
+
if (ignoredBytes > 64 * 1024) {
|
|
5737
|
+
conn.destroy();
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
buf = "";
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
let nl;
|
|
5744
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
5745
|
+
const line = buf.slice(0, nl);
|
|
5746
|
+
buf = buf.slice(nl + 1);
|
|
5747
|
+
if (!dispatched) {
|
|
5748
|
+
dispatched = true;
|
|
5749
|
+
dispatchFirst(line);
|
|
5750
|
+
} else if (onLine) {
|
|
5751
|
+
let obj;
|
|
5752
|
+
try {
|
|
5753
|
+
obj = JSON.parse(line);
|
|
5754
|
+
} catch {
|
|
5755
|
+
conn.end();
|
|
5756
|
+
return;
|
|
5757
|
+
}
|
|
5758
|
+
onLine(obj);
|
|
5759
|
+
} else {
|
|
5760
|
+
buf = "";
|
|
5761
|
+
break;
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
if (!dispatched) {
|
|
5765
|
+
if (buf.length > 64 * 1024) {
|
|
5766
|
+
dispatched = true;
|
|
5767
|
+
closeUnknown();
|
|
5768
|
+
}
|
|
5769
|
+
} else if (onLine) {
|
|
5770
|
+
if (buf.length > 64 * 1024) {
|
|
5771
|
+
conn.destroy();
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5689
5775
|
});
|
|
5690
5776
|
}
|
|
5691
5777
|
function startAttachStream(conn, hub, channelId) {
|
|
@@ -5706,6 +5792,116 @@ function startAttachStream(conn, hub, channelId) {
|
|
|
5706
5792
|
conn.on("error", cleanup);
|
|
5707
5793
|
conn.on("end", cleanup);
|
|
5708
5794
|
}
|
|
5795
|
+
function startHandoffConversation(conn, ctl, initialChannelId, writeLine, log2) {
|
|
5796
|
+
let phase = "selecting";
|
|
5797
|
+
let channelId = null;
|
|
5798
|
+
const fail = (error) => {
|
|
5799
|
+
phase = "closed";
|
|
5800
|
+
writeLine({ error });
|
|
5801
|
+
conn.end();
|
|
5802
|
+
};
|
|
5803
|
+
const doOffer = (id) => {
|
|
5804
|
+
const res = ctl.offer(id);
|
|
5805
|
+
if (!res.ok) {
|
|
5806
|
+
fail(res.error);
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
channelId = id;
|
|
5810
|
+
phase = "offered";
|
|
5811
|
+
writeLine({ handoff: "offer", channel: res.channel, provider: res.provider, pendingWork: res.pendingWork });
|
|
5812
|
+
};
|
|
5813
|
+
conn.on("close", () => {
|
|
5814
|
+
const p = phase;
|
|
5815
|
+
phase = "closed";
|
|
5816
|
+
if (p === "spec_sent") {
|
|
5817
|
+
ctl.orphan();
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
if (p === "beginning") {
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5823
|
+
});
|
|
5824
|
+
if (initialChannelId !== void 0) doOffer(initialChannelId);
|
|
5825
|
+
else writeLine({ handoff: "channels", channels: ctl.listChannels() });
|
|
5826
|
+
return (obj) => {
|
|
5827
|
+
const msg = obj;
|
|
5828
|
+
if (phase === "selecting" && msg["handoff"] === "select" && typeof msg["channelId"] === "string") {
|
|
5829
|
+
doOffer(msg["channelId"]);
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
if (phase === "offered" && msg["handoff"] === "decline") {
|
|
5833
|
+
phase = "closed";
|
|
5834
|
+
conn.end();
|
|
5835
|
+
return;
|
|
5836
|
+
}
|
|
5837
|
+
if (phase === "offered" && msg["handoff"] === "proceed" && channelId) {
|
|
5838
|
+
phase = "beginning";
|
|
5839
|
+
void ctl.begin(channelId).then((res) => {
|
|
5840
|
+
if (phase === "closed") {
|
|
5841
|
+
void ctl.abortAfterBegin();
|
|
5842
|
+
return;
|
|
5843
|
+
}
|
|
5844
|
+
if (!res.ok) {
|
|
5845
|
+
fail(res.error);
|
|
5846
|
+
return;
|
|
5847
|
+
}
|
|
5848
|
+
phase = "spec_sent";
|
|
5849
|
+
writeLine({ handoff: "spec", ...res.spec });
|
|
5850
|
+
}).catch((e) => {
|
|
5851
|
+
log2(`[braid] handoff begin failed: ${e.message}`);
|
|
5852
|
+
if (phase !== "closed") fail("begin_failed");
|
|
5853
|
+
});
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5856
|
+
if (phase === "spec_sent" && msg["handoff"] === "done") {
|
|
5857
|
+
phase = "resuming";
|
|
5858
|
+
void ctl.complete().then(() => {
|
|
5859
|
+
writeLine({ handoff: "resumed" });
|
|
5860
|
+
phase = "closed";
|
|
5861
|
+
conn.end();
|
|
5862
|
+
}).catch((e) => {
|
|
5863
|
+
log2(`[braid] handoff resume failed: ${e.message}`);
|
|
5864
|
+
try {
|
|
5865
|
+
conn.end();
|
|
5866
|
+
} catch {
|
|
5867
|
+
}
|
|
5868
|
+
});
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
5871
|
+
fail("protocol_error");
|
|
5872
|
+
};
|
|
5873
|
+
}
|
|
5874
|
+
function startRecoverConversation(conn, ctl, writeLine, log2) {
|
|
5875
|
+
let offered = false;
|
|
5876
|
+
const res = ctl.recoverOffer();
|
|
5877
|
+
if (!res.ok) {
|
|
5878
|
+
writeLine({ error: res.error });
|
|
5879
|
+
conn.end();
|
|
5880
|
+
return () => {
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
offered = true;
|
|
5884
|
+
writeLine({ recover: "offer", channel: res.channel });
|
|
5885
|
+
return (obj) => {
|
|
5886
|
+
const msg = obj;
|
|
5887
|
+
if (offered && msg["recover"] === "proceed") {
|
|
5888
|
+
offered = false;
|
|
5889
|
+
void ctl.recoverProceed().then(() => {
|
|
5890
|
+
writeLine({ recover: "resumed" });
|
|
5891
|
+
conn.end();
|
|
5892
|
+
}).catch((e) => {
|
|
5893
|
+
log2(`[braid] handoff resume failed: ${e.message}`);
|
|
5894
|
+
try {
|
|
5895
|
+
conn.end();
|
|
5896
|
+
} catch {
|
|
5897
|
+
}
|
|
5898
|
+
});
|
|
5899
|
+
return;
|
|
5900
|
+
}
|
|
5901
|
+
writeLine({ error: "protocol_error" });
|
|
5902
|
+
conn.end();
|
|
5903
|
+
};
|
|
5904
|
+
}
|
|
5709
5905
|
|
|
5710
5906
|
// src/service/install.ts
|
|
5711
5907
|
function validateServiceKey(name) {
|
|
@@ -5817,18 +6013,18 @@ function dedupe(names) {
|
|
|
5817
6013
|
function isLegacyFamilyService(platform, name, homeDir) {
|
|
5818
6014
|
if (platform === "darwin") {
|
|
5819
6015
|
const dir = join6(homeDir, "Library", "LaunchAgents");
|
|
5820
|
-
return !
|
|
6016
|
+
return !existsSync4(join6(dir, `com.snowyroad.braid.${name}.plist`)) && existsSync4(join6(dir, `dev.arp.bridge.${name}.plist`));
|
|
5821
6017
|
}
|
|
5822
6018
|
if (platform === "linux") {
|
|
5823
6019
|
const dir = join6(homeDir, ".config", "systemd", "user");
|
|
5824
|
-
return !
|
|
6020
|
+
return !existsSync4(join6(dir, `braid-${name}.service`)) && existsSync4(join6(dir, `arp-bridge-${name}.service`));
|
|
5825
6021
|
}
|
|
5826
6022
|
return false;
|
|
5827
6023
|
}
|
|
5828
6024
|
function globNames(dir, pattern) {
|
|
5829
6025
|
let entries = [];
|
|
5830
6026
|
try {
|
|
5831
|
-
entries =
|
|
6027
|
+
entries = readdirSync3(dir);
|
|
5832
6028
|
} catch {
|
|
5833
6029
|
return [];
|
|
5834
6030
|
}
|
|
@@ -5903,12 +6099,12 @@ function readNeedsHumanMarker(name, braidDir) {
|
|
|
5903
6099
|
function clearNeedsHumanMarker(name, braidDir) {
|
|
5904
6100
|
try {
|
|
5905
6101
|
const p = needsHumanMarkerPath(name, braidDir);
|
|
5906
|
-
if (
|
|
6102
|
+
if (existsSync5(p)) unlinkSync2(p);
|
|
5907
6103
|
} catch {
|
|
5908
6104
|
}
|
|
5909
6105
|
try {
|
|
5910
6106
|
const legacy = legacyTwinPath(braidDir, ["run", `${name}.needs-human`]);
|
|
5911
|
-
if (legacy &&
|
|
6107
|
+
if (legacy && existsSync5(legacy)) unlinkSync2(legacy);
|
|
5912
6108
|
} catch {
|
|
5913
6109
|
}
|
|
5914
6110
|
}
|
|
@@ -6004,6 +6200,174 @@ var ObservationHub = class {
|
|
|
6004
6200
|
import { decodeJwt as decodeJwt2 } from "jose";
|
|
6005
6201
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6006
6202
|
import path3 from "path";
|
|
6203
|
+
|
|
6204
|
+
// src/observe/render.ts
|
|
6205
|
+
function channelTag(channelId) {
|
|
6206
|
+
return sanitizeForTty(channelId).slice(0, 8);
|
|
6207
|
+
}
|
|
6208
|
+
function renderEvent(ev) {
|
|
6209
|
+
switch (ev.kind) {
|
|
6210
|
+
case "activity": {
|
|
6211
|
+
const a = ev.event;
|
|
6212
|
+
const kind = a.kind ? sanitizeForTty(a.kind) : "tool";
|
|
6213
|
+
const status = a.status ? sanitizeForTty(a.status) : "?";
|
|
6214
|
+
const title = a.title ? sanitizeForTty(a.title) : "(untitled)";
|
|
6215
|
+
const loc = a.locations && a.locations.length > 0 ? " " + a.locations.map((l) => sanitizeForTty(l.path) + (l.line != null ? `:${l.line}` : "")).join(", ") : "";
|
|
6216
|
+
return `
|
|
6217
|
+
\xB7 [${kind}/${status}] ${title}${loc}`;
|
|
6218
|
+
}
|
|
6219
|
+
case "plan": {
|
|
6220
|
+
const entries = ev.snapshot.entries;
|
|
6221
|
+
const done = entries.filter((e) => e.status === "completed").length;
|
|
6222
|
+
const total = entries.length;
|
|
6223
|
+
const current = entries.find((e) => e.status === "in_progress");
|
|
6224
|
+
const head = `
|
|
6225
|
+
\u25B8 plan ${done}/${total}`;
|
|
6226
|
+
return current ? `${head} - ${sanitizeForTty(current.content)}` : head;
|
|
6227
|
+
}
|
|
6228
|
+
case "message": {
|
|
6229
|
+
const delta = sanitizeForTty(ev.delta);
|
|
6230
|
+
return delta.length === 0 ? null : delta;
|
|
6231
|
+
}
|
|
6232
|
+
case "reply": {
|
|
6233
|
+
const u = ev.usage;
|
|
6234
|
+
const ctx = u?.contextUsed != null && u?.contextSize != null ? ` ctx ${u.contextUsed}/${u.contextSize}` : "";
|
|
6235
|
+
const cost = u?.costUsed != null && u.costUsed > 0 ? ` cost ${u.costUsed}${u.costCurrency ? " " + sanitizeForTty(u.costCurrency) : ""}` : "";
|
|
6236
|
+
const chars = sanitizeForTty(ev.text).length;
|
|
6237
|
+
return `
|
|
6238
|
+
\u2192 posted ${chars} chars${ctx}${cost}`;
|
|
6239
|
+
}
|
|
6240
|
+
default: {
|
|
6241
|
+
const _exhaustive = ev;
|
|
6242
|
+
return null;
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
|
|
6247
|
+
// src/observe/socketHandoff.ts
|
|
6248
|
+
function makeSocketHandoffController(deps) {
|
|
6249
|
+
const specFor = deps.specFor ?? interactiveResumeSpecFor;
|
|
6250
|
+
const scopedLaunchFn = deps.scopedLaunchFn ?? scopedLaunch;
|
|
6251
|
+
const log2 = deps.log ?? (() => {
|
|
6252
|
+
});
|
|
6253
|
+
let active = null;
|
|
6254
|
+
const label = (channelId) => {
|
|
6255
|
+
const name = deps.channelName(channelId);
|
|
6256
|
+
const tag = channelTag(channelId);
|
|
6257
|
+
return name ? `#${sanitizeForTty(name).slice(0, 40)} (${tag})` : `#${tag}`;
|
|
6258
|
+
};
|
|
6259
|
+
const gateBlocked = () => {
|
|
6260
|
+
const g = deps.gate.snapshot();
|
|
6261
|
+
if (g.state === "active") return { ok: false, error: "handoff_active" };
|
|
6262
|
+
if (g.state === "orphaned") return { ok: false, error: "handoff_orphaned" };
|
|
6263
|
+
return null;
|
|
6264
|
+
};
|
|
6265
|
+
const preflight = (channelId) => {
|
|
6266
|
+
const blocked = gateBlocked();
|
|
6267
|
+
if (blocked) return blocked;
|
|
6268
|
+
const session = deps.sessions.get(channelId);
|
|
6269
|
+
if (!session) return { ok: false, error: "unknown_channel" };
|
|
6270
|
+
const sessionId = session.liveSessionId();
|
|
6271
|
+
if (!sessionId) return { ok: false, error: "no_live_session" };
|
|
6272
|
+
let raw;
|
|
6273
|
+
try {
|
|
6274
|
+
raw = specFor(deps.provider, sessionId, deps.cwdFor(channelId));
|
|
6275
|
+
} catch (e) {
|
|
6276
|
+
return { ok: false, error: sanitizeForTty(String(e?.message ?? e)) };
|
|
6277
|
+
}
|
|
6278
|
+
if (!raw) return { ok: false, error: "provider_unverified" };
|
|
6279
|
+
return { ok: true, session, sessionId, spec: raw };
|
|
6280
|
+
};
|
|
6281
|
+
return {
|
|
6282
|
+
listChannels() {
|
|
6283
|
+
return [...deps.sessions.entries()].map(([id, s]) => ({
|
|
6284
|
+
id,
|
|
6285
|
+
label: label(id),
|
|
6286
|
+
provider: deps.provider,
|
|
6287
|
+
pendingWork: s.hasPendingWork?.() === true
|
|
6288
|
+
}));
|
|
6289
|
+
},
|
|
6290
|
+
offer(channelId) {
|
|
6291
|
+
const pre = preflight(channelId);
|
|
6292
|
+
if (!pre.ok) return pre;
|
|
6293
|
+
return {
|
|
6294
|
+
ok: true,
|
|
6295
|
+
channel: { id: channelId, label: label(channelId) },
|
|
6296
|
+
provider: deps.provider,
|
|
6297
|
+
pendingWork: pre.session.hasPendingWork?.() === true
|
|
6298
|
+
};
|
|
6299
|
+
},
|
|
6300
|
+
async begin(channelId) {
|
|
6301
|
+
const pre = preflight(channelId);
|
|
6302
|
+
if (!pre.ok) return pre;
|
|
6303
|
+
if (!deps.gate.tryAcquire(channelId, "socket")) return gateBlocked() ?? { ok: false, error: "handoff_active" };
|
|
6304
|
+
let pauseAttempted = false;
|
|
6305
|
+
try {
|
|
6306
|
+
const scoped = await scopedLaunchFn({ command: pre.spec.command, args: pre.spec.args, cwd: pre.spec.cwd }, { scope: deps.scope }, deps.provider);
|
|
6307
|
+
pauseAttempted = true;
|
|
6308
|
+
await pre.session.handoffPause();
|
|
6309
|
+
active = { channelId, session: pre.session };
|
|
6310
|
+
log2(`[braid] handoff: paused ${label(channelId)} for a socket client`);
|
|
6311
|
+
return { ok: true, spec: { command: scoped.command, args: scoped.args, cwd: scoped.cwd, env: scoped.env ?? null } };
|
|
6312
|
+
} catch (e) {
|
|
6313
|
+
if (pauseAttempted) {
|
|
6314
|
+
try {
|
|
6315
|
+
await pre.session.handoffResume();
|
|
6316
|
+
} catch {
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
deps.gate.release();
|
|
6320
|
+
return { ok: false, error: sanitizeForTty(String(e?.message ?? e)) };
|
|
6321
|
+
}
|
|
6322
|
+
},
|
|
6323
|
+
async abortAfterBegin() {
|
|
6324
|
+
if (!active) return;
|
|
6325
|
+
const a = active;
|
|
6326
|
+
active = null;
|
|
6327
|
+
try {
|
|
6328
|
+
await a.session.handoffResume();
|
|
6329
|
+
} finally {
|
|
6330
|
+
deps.gate.release();
|
|
6331
|
+
}
|
|
6332
|
+
log2(`[braid] handoff: client vanished before the spec was delivered \u2014 resumed ${label(a.channelId)}`);
|
|
6333
|
+
},
|
|
6334
|
+
async complete() {
|
|
6335
|
+
if (!active) return;
|
|
6336
|
+
const a = active;
|
|
6337
|
+
active = null;
|
|
6338
|
+
try {
|
|
6339
|
+
await a.session.handoffResume();
|
|
6340
|
+
} finally {
|
|
6341
|
+
deps.gate.release();
|
|
6342
|
+
}
|
|
6343
|
+
log2(`[braid] handoff: owner handed back \u2014 resumed ${label(a.channelId)}`);
|
|
6344
|
+
},
|
|
6345
|
+
orphan() {
|
|
6346
|
+
if (!active) return;
|
|
6347
|
+
deps.gate.markOrphaned();
|
|
6348
|
+
log2(`[braid] HANDOFF ORPHANED: the client vanished after receiving the session spec for ${label(active.channelId)}. The channel STAYS PAUSED (cannot prove the native session is closed). Recover with: braid handoff --recover (or restart this bridge)`);
|
|
6349
|
+
},
|
|
6350
|
+
recoverOffer() {
|
|
6351
|
+
const g = deps.gate.snapshot();
|
|
6352
|
+
if (g.state !== "orphaned") return { ok: false, error: "not_orphaned" };
|
|
6353
|
+
return { ok: true, channel: { id: g.channelId, label: label(g.channelId) } };
|
|
6354
|
+
},
|
|
6355
|
+
async recoverProceed() {
|
|
6356
|
+
if (!active || deps.gate.snapshot().state !== "orphaned") return;
|
|
6357
|
+
const a = active;
|
|
6358
|
+
active = null;
|
|
6359
|
+
try {
|
|
6360
|
+
await a.session.handoffResume();
|
|
6361
|
+
} finally {
|
|
6362
|
+
deps.gate.release();
|
|
6363
|
+
}
|
|
6364
|
+
log2(`[braid] handoff: recovery attested \u2014 resumed ${label(a.channelId)}`);
|
|
6365
|
+
},
|
|
6366
|
+
gateSnapshot: () => deps.gate.snapshot()
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
|
|
6370
|
+
// src/bridge.ts
|
|
6007
6371
|
var REFRESH_MARGIN_MS = 24 * 60 * 60 * 1e3;
|
|
6008
6372
|
var MIN_DELAY_MS = 60 * 1e3;
|
|
6009
6373
|
var JOIN_HISTORY_LIMIT = 20;
|
|
@@ -6040,6 +6404,7 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6040
6404
|
const tornDownWhilePending = /* @__PURE__ */ new Set();
|
|
6041
6405
|
let selfCardUnsigned = null;
|
|
6042
6406
|
let cardRefreshTimer = null;
|
|
6407
|
+
let stopCardContentRefresh = null;
|
|
6043
6408
|
const WARMUP_LABEL_MS = 1e4;
|
|
6044
6409
|
const WARMUP_HEARTBEAT_MS = 25e3;
|
|
6045
6410
|
function scheduleCardRefresh(att) {
|
|
@@ -6128,6 +6493,19 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6128
6493
|
});
|
|
6129
6494
|
}
|
|
6130
6495
|
}
|
|
6496
|
+
if (cfg.agent === "claude-code") {
|
|
6497
|
+
stopCardContentRefresh?.();
|
|
6498
|
+
stopCardContentRefresh = startCardRefreshLoop(fingerprint, {
|
|
6499
|
+
computeFingerprint: () => computeCapabilityFingerprint(cfg),
|
|
6500
|
+
buildFromDisk: () => buildCardFromDisk(),
|
|
6501
|
+
setCard: onUnsigned,
|
|
6502
|
+
cacheSave,
|
|
6503
|
+
signCard: cfg.signCard,
|
|
6504
|
+
putAgentCard: (c, scope) => relay.putAgentCard(c, scope),
|
|
6505
|
+
agentName: cfg.agentName,
|
|
6506
|
+
log: (m) => console.warn(sanitizeForTty(m))
|
|
6507
|
+
});
|
|
6508
|
+
}
|
|
6131
6509
|
} else {
|
|
6132
6510
|
await buildAndPublishSelfCard(cfg, relay, { converseLocal: async () => "" }, onUnsigned, onAtt);
|
|
6133
6511
|
}
|
|
@@ -6318,6 +6696,15 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6318
6696
|
logsPath: cs.logsPath,
|
|
6319
6697
|
http: snapshotHttpMetrics()
|
|
6320
6698
|
});
|
|
6699
|
+
const handoffCtl = cs.handoffGate ? makeSocketHandoffController({
|
|
6700
|
+
sessions,
|
|
6701
|
+
channelName: (id) => channelNames.get(id),
|
|
6702
|
+
provider: cfg.agent,
|
|
6703
|
+
cwdFor: (id) => handoffCwdFor(cfg, id),
|
|
6704
|
+
scope: cfg.scope,
|
|
6705
|
+
gate: cs.handoffGate,
|
|
6706
|
+
log: (line) => console.log(line)
|
|
6707
|
+
}) : void 0;
|
|
6321
6708
|
try {
|
|
6322
6709
|
const handle = await startControlSocket({
|
|
6323
6710
|
sockPath: cs.sockPath,
|
|
@@ -6325,7 +6712,8 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6325
6712
|
getStatus: buildStatus,
|
|
6326
6713
|
ownsLock: cs.ownsLock,
|
|
6327
6714
|
log: (line) => console.log(line),
|
|
6328
|
-
tokenPath: cs.tokenPath
|
|
6715
|
+
tokenPath: cs.tokenPath,
|
|
6716
|
+
...handoffCtl ? { handoff: handoffCtl } : {}
|
|
6329
6717
|
});
|
|
6330
6718
|
controlSocketClose = handle.close;
|
|
6331
6719
|
} catch (e) {
|
|
@@ -6520,6 +6908,10 @@ Commands:
|
|
|
6520
6908
|
service atomically.
|
|
6521
6909
|
attach [name] Watch a running service's live activity over the local control
|
|
6522
6910
|
socket (same machine only).
|
|
6911
|
+
handoff [<agent>] [--channel <id-or-name>]
|
|
6912
|
+
take over the agent's live session on THIS terminal
|
|
6913
|
+
(quit the native UI to hand control back)
|
|
6914
|
+
handoff --recover resume a bridge left paused by a vanished handoff client
|
|
6523
6915
|
version Print the installed braid version (also --version, -v)
|
|
6524
6916
|
|
|
6525
6917
|
Options for join/start (each also has an env twin; flag > env > file > default):
|
|
@@ -6540,6 +6932,7 @@ Options for join/start (each also has an env twin; flag > env > file > default):
|
|
|
6540
6932
|
--allow-write <path> widen sandbox write access (repeatable)
|
|
6541
6933
|
--allow-read <path> widen sandbox read access (repeatable)
|
|
6542
6934
|
--allow-domain <host> widen sandbox network egress (repeatable)
|
|
6935
|
+
--no-socket do not open the local control socket (on by default)
|
|
6543
6936
|
At join, explicit choices persist to the saved agent; at start they apply to
|
|
6544
6937
|
that run only.`;
|
|
6545
6938
|
var SERVICE_VERBS = /* @__PURE__ */ new Set([
|
|
@@ -6631,6 +7024,30 @@ function parseCliArgs(argv) {
|
|
|
6631
7024
|
const agent = argv[1] && !argv[1].startsWith("-") ? argv[1].trim() : void 0;
|
|
6632
7025
|
return { kind: "attach", agent };
|
|
6633
7026
|
}
|
|
7027
|
+
if (first === "handoff") {
|
|
7028
|
+
let agent;
|
|
7029
|
+
let channel;
|
|
7030
|
+
let recover = false;
|
|
7031
|
+
const rest = argv.slice(1);
|
|
7032
|
+
for (let i = 0; i < rest.length; i++) {
|
|
7033
|
+
const a = rest[i];
|
|
7034
|
+
if (a === "--recover") {
|
|
7035
|
+
recover = true;
|
|
7036
|
+
continue;
|
|
7037
|
+
}
|
|
7038
|
+
if (a === "--channel") {
|
|
7039
|
+
const v = rest[i + 1]?.trim();
|
|
7040
|
+
if (v === void 0 || v === "" || v.startsWith("--")) return { kind: "usage", error: "Missing value for --channel" };
|
|
7041
|
+
channel = v;
|
|
7042
|
+
i++;
|
|
7043
|
+
continue;
|
|
7044
|
+
}
|
|
7045
|
+
if (a.startsWith("-")) return { kind: "usage", error: `Unknown flag for handoff: ${a}` };
|
|
7046
|
+
if (agent !== void 0) return { kind: "usage", error: "handoff takes at most one agent name" };
|
|
7047
|
+
agent = a.trim();
|
|
7048
|
+
}
|
|
7049
|
+
return { kind: "handoff", agent, channel, recover };
|
|
7050
|
+
}
|
|
6634
7051
|
if (first === "version" || first === "--version" || first === "-v") {
|
|
6635
7052
|
return { kind: "version" };
|
|
6636
7053
|
}
|
|
@@ -6837,51 +7254,6 @@ function connectedBannerLines(opts) {
|
|
|
6837
7254
|
// src/observe/attach.ts
|
|
6838
7255
|
import * as readline from "readline";
|
|
6839
7256
|
import * as net2 from "net";
|
|
6840
|
-
|
|
6841
|
-
// src/observe/render.ts
|
|
6842
|
-
function channelTag(channelId) {
|
|
6843
|
-
return sanitizeForTty(channelId).slice(0, 8);
|
|
6844
|
-
}
|
|
6845
|
-
function renderEvent(ev) {
|
|
6846
|
-
switch (ev.kind) {
|
|
6847
|
-
case "activity": {
|
|
6848
|
-
const a = ev.event;
|
|
6849
|
-
const kind = a.kind ? sanitizeForTty(a.kind) : "tool";
|
|
6850
|
-
const status = a.status ? sanitizeForTty(a.status) : "?";
|
|
6851
|
-
const title = a.title ? sanitizeForTty(a.title) : "(untitled)";
|
|
6852
|
-
const loc = a.locations && a.locations.length > 0 ? " " + a.locations.map((l) => sanitizeForTty(l.path) + (l.line != null ? `:${l.line}` : "")).join(", ") : "";
|
|
6853
|
-
return `
|
|
6854
|
-
\xB7 [${kind}/${status}] ${title}${loc}`;
|
|
6855
|
-
}
|
|
6856
|
-
case "plan": {
|
|
6857
|
-
const entries = ev.snapshot.entries;
|
|
6858
|
-
const done = entries.filter((e) => e.status === "completed").length;
|
|
6859
|
-
const total = entries.length;
|
|
6860
|
-
const current = entries.find((e) => e.status === "in_progress");
|
|
6861
|
-
const head = `
|
|
6862
|
-
\u25B8 plan ${done}/${total}`;
|
|
6863
|
-
return current ? `${head} - ${sanitizeForTty(current.content)}` : head;
|
|
6864
|
-
}
|
|
6865
|
-
case "message": {
|
|
6866
|
-
const delta = sanitizeForTty(ev.delta);
|
|
6867
|
-
return delta.length === 0 ? null : delta;
|
|
6868
|
-
}
|
|
6869
|
-
case "reply": {
|
|
6870
|
-
const u = ev.usage;
|
|
6871
|
-
const ctx = u?.contextUsed != null && u?.contextSize != null ? ` ctx ${u.contextUsed}/${u.contextSize}` : "";
|
|
6872
|
-
const cost = u?.costUsed != null && u.costUsed > 0 ? ` cost ${u.costUsed}${u.costCurrency ? " " + sanitizeForTty(u.costCurrency) : ""}` : "";
|
|
6873
|
-
const chars = sanitizeForTty(ev.text).length;
|
|
6874
|
-
return `
|
|
6875
|
-
\u2192 posted ${chars} chars${ctx}${cost}`;
|
|
6876
|
-
}
|
|
6877
|
-
default: {
|
|
6878
|
-
const _exhaustive = ev;
|
|
6879
|
-
return null;
|
|
6880
|
-
}
|
|
6881
|
-
}
|
|
6882
|
-
}
|
|
6883
|
-
|
|
6884
|
-
// src/observe/attach.ts
|
|
6885
7257
|
var MAX_PICKER = 9;
|
|
6886
7258
|
function createAttachController(deps) {
|
|
6887
7259
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -7208,6 +7580,355 @@ async function runHandoff(opts) {
|
|
|
7208
7580
|
}
|
|
7209
7581
|
}
|
|
7210
7582
|
|
|
7583
|
+
// src/service/handoffClient.ts
|
|
7584
|
+
import * as net3 from "net";
|
|
7585
|
+
import { createInterface } from "readline/promises";
|
|
7586
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
7587
|
+
var YES = /^\s*(y|yes)\s*$/i;
|
|
7588
|
+
async function runHandoffCommand(opts) {
|
|
7589
|
+
const env = opts.env ?? process.env;
|
|
7590
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
7591
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
7592
|
+
const write = (s) => stdout.write(s);
|
|
7593
|
+
const connectFn = opts.connectFn ?? ((p) => net3.createConnection(p));
|
|
7594
|
+
const spawnFn = opts.spawnFn ?? nodeSpawn2;
|
|
7595
|
+
const promptFn = opts.promptFn ?? ((q) => defaultPrompt(stdin, stdout, q));
|
|
7596
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
7597
|
+
write("[handoff] an interactive terminal is required (no --yes exists, by design)\n");
|
|
7598
|
+
return 1;
|
|
7599
|
+
}
|
|
7600
|
+
const braidDir = configDir(env);
|
|
7601
|
+
const names = listAgents(braidDir).map((e) => e.agent.agentName);
|
|
7602
|
+
let name;
|
|
7603
|
+
if (opts.agent && opts.agent.trim() !== "") {
|
|
7604
|
+
const want = opts.agent.trim();
|
|
7605
|
+
if (!names.includes(want)) {
|
|
7606
|
+
const enrolled = names.length ? names.map(sanitizeForTty).join(", ") : "(none)";
|
|
7607
|
+
write(`[handoff] no agent named "${sanitizeForTty(want)}" is enrolled. Enrolled: ${enrolled}
|
|
7608
|
+
`);
|
|
7609
|
+
return 1;
|
|
7610
|
+
}
|
|
7611
|
+
name = want;
|
|
7612
|
+
} else if (names.length === 1) {
|
|
7613
|
+
name = names[0];
|
|
7614
|
+
} else if (names.length === 0) {
|
|
7615
|
+
write("[handoff] no agents are enrolled - run: braid join <code>\n");
|
|
7616
|
+
return 1;
|
|
7617
|
+
} else {
|
|
7618
|
+
write(`[handoff] multiple agents enrolled (${names.map(sanitizeForTty).join(", ")}) - specify one: braid handoff <name>
|
|
7619
|
+
`);
|
|
7620
|
+
return 1;
|
|
7621
|
+
}
|
|
7622
|
+
const safeName = sanitizeForTty(name);
|
|
7623
|
+
const sockPath = await liveControlSocketPath(name, braidDir);
|
|
7624
|
+
if (!await socketLive(sockPath)) {
|
|
7625
|
+
write(`[handoff] ${safeName} isn't running - start it with: braid start ${safeName}
|
|
7626
|
+
`);
|
|
7627
|
+
return 1;
|
|
7628
|
+
}
|
|
7629
|
+
const token = readControlToken(name, braidDir);
|
|
7630
|
+
const socket = connectFn(sockPath);
|
|
7631
|
+
const lc = lineClient(socket);
|
|
7632
|
+
try {
|
|
7633
|
+
return opts.recover ? await runRecover(lc, { write, promptFn, token, name: safeName }) : await runTakeover(lc, { write, promptFn, token, name: safeName, env, spawnFn, channel: opts.channel });
|
|
7634
|
+
} catch (err) {
|
|
7635
|
+
write(`[handoff] control socket error: ${sanitizeForTty(String(err?.message ?? err))}
|
|
7636
|
+
`);
|
|
7637
|
+
return 1;
|
|
7638
|
+
} finally {
|
|
7639
|
+
try {
|
|
7640
|
+
socket.destroy();
|
|
7641
|
+
} catch {
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
async function runTakeover(lc, ctx) {
|
|
7646
|
+
const { write, promptFn, token, name, env, spawnFn, channel } = ctx;
|
|
7647
|
+
lc.send({ verb: "handoff", ...token ? { token } : {} });
|
|
7648
|
+
const first = await lc.next();
|
|
7649
|
+
if (typeof first["error"] === "string") return reportError(write, first["error"], name);
|
|
7650
|
+
const channels = Array.isArray(first["channels"]) ? first["channels"] : [];
|
|
7651
|
+
const picked = await pickChannel(channels, channel, promptFn, write);
|
|
7652
|
+
if (!picked) return 1;
|
|
7653
|
+
lc.send({ handoff: "select", channelId: picked.id });
|
|
7654
|
+
const offer = await lc.next();
|
|
7655
|
+
if (typeof offer["error"] === "string") return reportError(write, offer["error"], name);
|
|
7656
|
+
renderOffer(write, offer);
|
|
7657
|
+
const ans = await promptFn("take control now? [y/N] ");
|
|
7658
|
+
if (!YES.test(ans)) {
|
|
7659
|
+
lc.send({ handoff: "decline" });
|
|
7660
|
+
await lc.drained(500);
|
|
7661
|
+
write("[handoff] declined - the bridge stays in control\n");
|
|
7662
|
+
return 0;
|
|
7663
|
+
}
|
|
7664
|
+
lc.send({ handoff: "proceed" });
|
|
7665
|
+
const specLine = await lc.next();
|
|
7666
|
+
if (typeof specLine["error"] === "string") return reportError(write, specLine["error"], name);
|
|
7667
|
+
const sendDone = async () => {
|
|
7668
|
+
lc.send({ handoff: "done" });
|
|
7669
|
+
try {
|
|
7670
|
+
await lc.next();
|
|
7671
|
+
} catch {
|
|
7672
|
+
}
|
|
7673
|
+
};
|
|
7674
|
+
const spec = validateSpec(specLine);
|
|
7675
|
+
if (!spec.ok) {
|
|
7676
|
+
await sendDone();
|
|
7677
|
+
write(`[handoff] malformed session spec from the bridge: ${sanitizeForTty(spec.error)}
|
|
7678
|
+
`);
|
|
7679
|
+
return 1;
|
|
7680
|
+
}
|
|
7681
|
+
const outcome = await new Promise((resolve2) => {
|
|
7682
|
+
let settled = false;
|
|
7683
|
+
const settle = (o) => {
|
|
7684
|
+
if (!settled) {
|
|
7685
|
+
settled = true;
|
|
7686
|
+
resolve2(o);
|
|
7687
|
+
}
|
|
7688
|
+
};
|
|
7689
|
+
try {
|
|
7690
|
+
const child = spawnFn(spec.command, spec.args, {
|
|
7691
|
+
stdio: "inherit",
|
|
7692
|
+
cwd: spec.cwd,
|
|
7693
|
+
env: buildAcpEnv(env, spec.env ?? void 0)
|
|
7694
|
+
});
|
|
7695
|
+
child.on("exit", () => settle({}));
|
|
7696
|
+
child.on("error", (e) => settle({ error: e }));
|
|
7697
|
+
} catch (e) {
|
|
7698
|
+
settle({ error: e });
|
|
7699
|
+
}
|
|
7700
|
+
});
|
|
7701
|
+
await sendDone();
|
|
7702
|
+
if (outcome.error) {
|
|
7703
|
+
write(`[handoff] failed to open the session: ${sanitizeForTty(String(outcome.error.message ?? outcome.error))}
|
|
7704
|
+
`);
|
|
7705
|
+
return 1;
|
|
7706
|
+
}
|
|
7707
|
+
write("[handoff] control handed back - the bridge is driving again\n");
|
|
7708
|
+
return 0;
|
|
7709
|
+
}
|
|
7710
|
+
function validateSpec(line) {
|
|
7711
|
+
const command = line["command"];
|
|
7712
|
+
if (typeof command !== "string" || command.trim() === "") return { ok: false, error: "command is not a non-empty string" };
|
|
7713
|
+
const cwd = line["cwd"];
|
|
7714
|
+
if (typeof cwd !== "string") return { ok: false, error: "cwd is not a string" };
|
|
7715
|
+
const args = line["args"];
|
|
7716
|
+
if (!Array.isArray(args) || args.some((a) => typeof a !== "string")) return { ok: false, error: "args is not an array of strings" };
|
|
7717
|
+
const env = line["env"] ?? null;
|
|
7718
|
+
if (env !== null) {
|
|
7719
|
+
if (typeof env !== "object" || Array.isArray(env) || Object.values(env).some((v) => typeof v !== "string")) {
|
|
7720
|
+
return { ok: false, error: "env is not a string-valued record or null" };
|
|
7721
|
+
}
|
|
7722
|
+
}
|
|
7723
|
+
return { ok: true, command, args, cwd, env };
|
|
7724
|
+
}
|
|
7725
|
+
async function pickChannel(channels, filter, promptFn, write) {
|
|
7726
|
+
if (channels.length === 0) {
|
|
7727
|
+
write("[handoff] no channels are available to take over\n");
|
|
7728
|
+
return null;
|
|
7729
|
+
}
|
|
7730
|
+
if (filter && filter.trim() !== "") {
|
|
7731
|
+
const f = filter.trim();
|
|
7732
|
+
const exact = channels.filter((c) => c.id === f);
|
|
7733
|
+
if (exact.length === 1) return exact[0];
|
|
7734
|
+
const matches = channels.filter((c) => c.id.startsWith(f) || c.label.includes(f));
|
|
7735
|
+
if (matches.length === 1) return matches[0];
|
|
7736
|
+
write(`[handoff] "${sanitizeForTty(f)}" ${matches.length === 0 ? "matched no channel" : `is ambiguous (${matches.length} channels)`} - available:
|
|
7737
|
+
`);
|
|
7738
|
+
listChannels(write, channels);
|
|
7739
|
+
return null;
|
|
7740
|
+
}
|
|
7741
|
+
if (channels.length === 1) return channels[0];
|
|
7742
|
+
write("[handoff] which channel do you want to take over?\n");
|
|
7743
|
+
listChannels(write, channels);
|
|
7744
|
+
const ans = (await promptFn(`select a channel [1-${channels.length}]: `)).trim();
|
|
7745
|
+
const idx = Number.parseInt(ans, 10);
|
|
7746
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > channels.length) {
|
|
7747
|
+
write("[handoff] not a valid selection\n");
|
|
7748
|
+
return null;
|
|
7749
|
+
}
|
|
7750
|
+
return channels[idx - 1];
|
|
7751
|
+
}
|
|
7752
|
+
function listChannels(write, channels) {
|
|
7753
|
+
channels.forEach((c, i) => {
|
|
7754
|
+
write(` [${i + 1}] ${sanitizeForTty(c.label)}${c.pendingWork ? " (working)" : ""}
|
|
7755
|
+
`);
|
|
7756
|
+
});
|
|
7757
|
+
}
|
|
7758
|
+
function renderOffer(write, offer) {
|
|
7759
|
+
const channel = offer["channel"] ?? {};
|
|
7760
|
+
const label = sanitizeForTty(String(channel.label ?? ""));
|
|
7761
|
+
const pendingWork = offer["pendingWork"] === true;
|
|
7762
|
+
write(
|
|
7763
|
+
banner("handoff", `TAKING CONTROL of ${label}`, [
|
|
7764
|
+
"pausing the bridge and opening the agent's REAL interactive session",
|
|
7765
|
+
"on this terminal. Quit it (Ctrl-C or /exit) to hand control back.",
|
|
7766
|
+
"",
|
|
7767
|
+
"WARNING: changes you make inside PERSIST after you leave -",
|
|
7768
|
+
"switching model or mode (or compacting/clearing) carries over into",
|
|
7769
|
+
"how this agent serves the channel afterwards.",
|
|
7770
|
+
...pendingWork ? [
|
|
7771
|
+
"",
|
|
7772
|
+
"NOTE: a reply is being worked on right now; taking control interrupts",
|
|
7773
|
+
"it. The interrupted message will re-run when you hand back."
|
|
7774
|
+
] : []
|
|
7775
|
+
])
|
|
7776
|
+
);
|
|
7777
|
+
}
|
|
7778
|
+
async function runRecover(lc, ctx) {
|
|
7779
|
+
const { write, promptFn, token, name } = ctx;
|
|
7780
|
+
lc.send({ verb: "handoff.recover", ...token ? { token } : {} });
|
|
7781
|
+
const first = await lc.next();
|
|
7782
|
+
if (typeof first["error"] === "string") {
|
|
7783
|
+
if (first["error"] === "not_orphaned") {
|
|
7784
|
+
write("[handoff] nothing to recover - no orphaned handoff\n");
|
|
7785
|
+
return 0;
|
|
7786
|
+
}
|
|
7787
|
+
return reportError(write, first["error"], name);
|
|
7788
|
+
}
|
|
7789
|
+
const channel = first["channel"] ?? {};
|
|
7790
|
+
write(`[handoff] a previous handoff of ${sanitizeForTty(String(channel.label ?? ""))} was orphaned and the channel is PAUSED.
|
|
7791
|
+
`);
|
|
7792
|
+
const ans = await promptFn("Is the interactive session fully closed? [y/N] ");
|
|
7793
|
+
if (!YES.test(ans)) {
|
|
7794
|
+
write("[handoff] recovery aborted - nothing changed\n");
|
|
7795
|
+
return 0;
|
|
7796
|
+
}
|
|
7797
|
+
lc.send({ recover: "proceed" });
|
|
7798
|
+
try {
|
|
7799
|
+
await lc.next();
|
|
7800
|
+
} catch {
|
|
7801
|
+
}
|
|
7802
|
+
write("[handoff] recovery confirmed - the bridge is driving again\n");
|
|
7803
|
+
return 0;
|
|
7804
|
+
}
|
|
7805
|
+
function reportError(write, code, agent) {
|
|
7806
|
+
write(`[handoff] ${codeToMessage(code, agent)}
|
|
7807
|
+
`);
|
|
7808
|
+
return 1;
|
|
7809
|
+
}
|
|
7810
|
+
function codeToMessage(code, agent) {
|
|
7811
|
+
switch (code) {
|
|
7812
|
+
case "handoff_active":
|
|
7813
|
+
return "another handoff is already in progress";
|
|
7814
|
+
case "handoff_orphaned":
|
|
7815
|
+
return `a previous handoff was orphaned - run: braid handoff ${agent} --recover`;
|
|
7816
|
+
case "provider_unverified":
|
|
7817
|
+
return "not available for this provider yet (verified for Claude Code, Codex, Grok, and opencode so far)";
|
|
7818
|
+
case "no_live_session":
|
|
7819
|
+
return "no live session yet for that channel";
|
|
7820
|
+
case "unknown_channel":
|
|
7821
|
+
return "that channel has no active session";
|
|
7822
|
+
default:
|
|
7823
|
+
return sanitizeForTty(code);
|
|
7824
|
+
}
|
|
7825
|
+
}
|
|
7826
|
+
function lineClient(sock) {
|
|
7827
|
+
sock.setEncoding("utf8");
|
|
7828
|
+
let buf = "";
|
|
7829
|
+
const queue = [];
|
|
7830
|
+
let closed = false;
|
|
7831
|
+
let sockErr = null;
|
|
7832
|
+
let wake = null;
|
|
7833
|
+
const bump = () => {
|
|
7834
|
+
const w = wake;
|
|
7835
|
+
wake = null;
|
|
7836
|
+
w?.();
|
|
7837
|
+
};
|
|
7838
|
+
sock.on("data", (d) => {
|
|
7839
|
+
buf += d;
|
|
7840
|
+
let nl;
|
|
7841
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
7842
|
+
queue.push(buf.slice(0, nl));
|
|
7843
|
+
buf = buf.slice(nl + 1);
|
|
7844
|
+
}
|
|
7845
|
+
bump();
|
|
7846
|
+
});
|
|
7847
|
+
let onClose = null;
|
|
7848
|
+
sock.on("close", () => {
|
|
7849
|
+
closed = true;
|
|
7850
|
+
onClose?.();
|
|
7851
|
+
bump();
|
|
7852
|
+
});
|
|
7853
|
+
sock.on("error", (e) => {
|
|
7854
|
+
sockErr = e;
|
|
7855
|
+
bump();
|
|
7856
|
+
});
|
|
7857
|
+
return {
|
|
7858
|
+
send(obj) {
|
|
7859
|
+
try {
|
|
7860
|
+
sock.write(JSON.stringify(obj) + "\n");
|
|
7861
|
+
} catch {
|
|
7862
|
+
}
|
|
7863
|
+
},
|
|
7864
|
+
drained(timeoutMs) {
|
|
7865
|
+
if (closed) return Promise.resolve();
|
|
7866
|
+
return new Promise((resolve2) => {
|
|
7867
|
+
const t = setTimeout(() => {
|
|
7868
|
+
onClose = null;
|
|
7869
|
+
resolve2();
|
|
7870
|
+
}, timeoutMs);
|
|
7871
|
+
if (typeof t.unref === "function") t.unref();
|
|
7872
|
+
onClose = () => {
|
|
7873
|
+
clearTimeout(t);
|
|
7874
|
+
resolve2();
|
|
7875
|
+
};
|
|
7876
|
+
});
|
|
7877
|
+
},
|
|
7878
|
+
async next() {
|
|
7879
|
+
for (; ; ) {
|
|
7880
|
+
if (queue.length > 0) {
|
|
7881
|
+
const line = queue.shift();
|
|
7882
|
+
try {
|
|
7883
|
+
return JSON.parse(line);
|
|
7884
|
+
} catch {
|
|
7885
|
+
throw new Error("control socket sent a malformed line");
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
if (sockErr) throw sockErr;
|
|
7889
|
+
if (closed) throw new Error("control socket closed before a response");
|
|
7890
|
+
await new Promise((r) => {
|
|
7891
|
+
wake = r;
|
|
7892
|
+
});
|
|
7893
|
+
}
|
|
7894
|
+
}
|
|
7895
|
+
};
|
|
7896
|
+
}
|
|
7897
|
+
async function defaultPrompt(stdin, stdout, question) {
|
|
7898
|
+
const rl = createInterface({
|
|
7899
|
+
input: stdin,
|
|
7900
|
+
output: stdout
|
|
7901
|
+
});
|
|
7902
|
+
try {
|
|
7903
|
+
return await rl.question(question);
|
|
7904
|
+
} finally {
|
|
7905
|
+
rl.close();
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
|
|
7909
|
+
// src/observe/handoffGate.ts
|
|
7910
|
+
var HandoffGate = class {
|
|
7911
|
+
st = { state: "idle" };
|
|
7912
|
+
/** Enter the critical section. False = another handoff is active or orphaned. */
|
|
7913
|
+
tryAcquire(channelId, via) {
|
|
7914
|
+
if (this.st.state !== "idle") return false;
|
|
7915
|
+
this.st = { state: "active", channelId, via };
|
|
7916
|
+
return true;
|
|
7917
|
+
}
|
|
7918
|
+
/** Leave the critical section (normal completion, pre-spec abort, or attested recovery). */
|
|
7919
|
+
release() {
|
|
7920
|
+
this.st = { state: "idle" };
|
|
7921
|
+
}
|
|
7922
|
+
/** Socket client vanished after the spec was sent: fail closed, stay paused. */
|
|
7923
|
+
markOrphaned() {
|
|
7924
|
+
if (this.st.state !== "active") return;
|
|
7925
|
+
this.st = { state: "orphaned", channelId: this.st.channelId };
|
|
7926
|
+
}
|
|
7927
|
+
snapshot() {
|
|
7928
|
+
return { ...this.st };
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
|
|
7211
7932
|
// src/scope/gate.ts
|
|
7212
7933
|
function scopeStartupDecision(avail, toolMode, scopeOff, installerAvailable, platform = process.platform) {
|
|
7213
7934
|
if (scopeOff) {
|
|
@@ -7269,8 +7990,8 @@ async function maybeOfferSandboxInstall(deps) {
|
|
|
7269
7990
|
}
|
|
7270
7991
|
function readlinePromptYesNo(input, output) {
|
|
7271
7992
|
return import("readline").then(
|
|
7272
|
-
({ createInterface }) => new Promise((resolve2) => {
|
|
7273
|
-
const rl =
|
|
7993
|
+
({ createInterface: createInterface2 }) => new Promise((resolve2) => {
|
|
7994
|
+
const rl = createInterface2({ input, output });
|
|
7274
7995
|
rl.once("line", (line) => {
|
|
7275
7996
|
resolve2(line.trim().toLowerCase() === "y");
|
|
7276
7997
|
rl.close();
|
|
@@ -7613,7 +8334,7 @@ import { execFile } from "child_process";
|
|
|
7613
8334
|
import { homedir as homedir6 } from "os";
|
|
7614
8335
|
|
|
7615
8336
|
// src/service/manager.ts
|
|
7616
|
-
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, rmSync as rmSync2, existsSync as
|
|
8337
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, rmSync as rmSync2, existsSync as existsSync6 } from "fs";
|
|
7617
8338
|
import { join as join9 } from "path";
|
|
7618
8339
|
|
|
7619
8340
|
// src/service/templates.ts
|
|
@@ -7785,7 +8506,7 @@ function schtasksXml(p) {
|
|
|
7785
8506
|
}
|
|
7786
8507
|
|
|
7787
8508
|
// src/service/win32.ts
|
|
7788
|
-
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readdirSync as
|
|
8509
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
7789
8510
|
import { join as join8 } from "path";
|
|
7790
8511
|
function win32LogDir(homeDir) {
|
|
7791
8512
|
return join8(homeDir, "AppData", "Local", "braid", "logs");
|
|
@@ -7811,7 +8532,7 @@ function win32Manager(ctx) {
|
|
|
7811
8532
|
const hostDirs = [];
|
|
7812
8533
|
for (const agentsDir of agentDirs) {
|
|
7813
8534
|
try {
|
|
7814
|
-
for (const host of
|
|
8535
|
+
for (const host of readdirSync4(agentsDir)) hostDirs.push(join8(agentsDir, host));
|
|
7815
8536
|
} catch {
|
|
7816
8537
|
}
|
|
7817
8538
|
}
|
|
@@ -7944,7 +8665,7 @@ function darwinManager(ctx) {
|
|
|
7944
8665
|
const legacyLogDir = join9(ctx.homeDir, "Library", "Logs", "arp-bridge");
|
|
7945
8666
|
const plistPath = (name) => join9(launchAgentsDir, `${launchdLabel(name)}.plist`);
|
|
7946
8667
|
const legacyPlistPath = (name) => join9(launchAgentsDir, `${legacyLaunchdLabel(name)}.plist`);
|
|
7947
|
-
const legacyInstalled = (name) => !
|
|
8668
|
+
const legacyInstalled = (name) => !existsSync6(plistPath(name)) && existsSync6(legacyPlistPath(name));
|
|
7948
8669
|
const domainTarget = `gui/${ctx.uid}`;
|
|
7949
8670
|
const serviceTarget = (name) => `${domainTarget}/${legacyInstalled(name) ? legacyLaunchdLabel(name) : launchdLabel(name)}`;
|
|
7950
8671
|
const write = (name) => {
|
|
@@ -7981,7 +8702,7 @@ function darwinManager(ctx) {
|
|
|
7981
8702
|
}
|
|
7982
8703
|
},
|
|
7983
8704
|
async start(name) {
|
|
7984
|
-
if (!
|
|
8705
|
+
if (!existsSync6(plistPath(name)) && !existsSync6(legacyPlistPath(name))) write(name);
|
|
7985
8706
|
await ctx.exec("launchctl", [
|
|
7986
8707
|
"bootstrap",
|
|
7987
8708
|
domainTarget,
|
|
@@ -7995,7 +8716,7 @@ function darwinManager(ctx) {
|
|
|
7995
8716
|
await ctx.exec("launchctl", ["kickstart", "-k", serviceTarget(name)]);
|
|
7996
8717
|
},
|
|
7997
8718
|
async status(name) {
|
|
7998
|
-
const installed =
|
|
8719
|
+
const installed = existsSync6(plistPath(name)) || existsSync6(legacyPlistPath(name));
|
|
7999
8720
|
if (!installed) return { installed: false, loaded: false, running: false };
|
|
8000
8721
|
const res = await ctx.exec("launchctl", ["print", serviceTarget(name)]);
|
|
8001
8722
|
const loaded = res.code === 0;
|
|
@@ -8004,7 +8725,7 @@ function darwinManager(ctx) {
|
|
|
8004
8725
|
installed,
|
|
8005
8726
|
loaded,
|
|
8006
8727
|
running,
|
|
8007
|
-
staleNodePath: !
|
|
8728
|
+
staleNodePath: !existsSync6(ctx.nodePath)
|
|
8008
8729
|
};
|
|
8009
8730
|
},
|
|
8010
8731
|
logsPath(name) {
|
|
@@ -8018,7 +8739,7 @@ function linuxManager(ctx) {
|
|
|
8018
8739
|
const legacyUnitName = (name) => `arp-bridge-${name}.service`;
|
|
8019
8740
|
const newUnitPath = (name) => join9(unitDir, newUnitName(name));
|
|
8020
8741
|
const legacyUnitPath = (name) => join9(unitDir, legacyUnitName(name));
|
|
8021
|
-
const legacyInstalled = (name) => !
|
|
8742
|
+
const legacyInstalled = (name) => !existsSync6(newUnitPath(name)) && existsSync6(legacyUnitPath(name));
|
|
8022
8743
|
const unitName = (name) => legacyInstalled(name) ? legacyUnitName(name) : newUnitName(name);
|
|
8023
8744
|
const unitPath = (name) => legacyInstalled(name) ? legacyUnitPath(name) : newUnitPath(name);
|
|
8024
8745
|
const sc = (...args) => ctx.exec("systemctl", ["--user", ...args]);
|
|
@@ -8035,7 +8756,7 @@ function linuxManager(ctx) {
|
|
|
8035
8756
|
async install(name) {
|
|
8036
8757
|
write(name);
|
|
8037
8758
|
await sc("daemon-reload");
|
|
8038
|
-
if (
|
|
8759
|
+
if (existsSync6(legacyUnitPath(name))) {
|
|
8039
8760
|
await sc("disable", "--now", legacyUnitName(name));
|
|
8040
8761
|
try {
|
|
8041
8762
|
rmSync2(legacyUnitPath(name), { force: true });
|
|
@@ -8047,7 +8768,7 @@ function linuxManager(ctx) {
|
|
|
8047
8768
|
},
|
|
8048
8769
|
async uninstall(name) {
|
|
8049
8770
|
await sc("disable", "--now", newUnitName(name));
|
|
8050
|
-
if (
|
|
8771
|
+
if (existsSync6(legacyUnitPath(name))) {
|
|
8051
8772
|
await sc("disable", "--now", legacyUnitName(name));
|
|
8052
8773
|
}
|
|
8053
8774
|
try {
|
|
@@ -8061,7 +8782,7 @@ function linuxManager(ctx) {
|
|
|
8061
8782
|
await sc("daemon-reload");
|
|
8062
8783
|
},
|
|
8063
8784
|
async start(name) {
|
|
8064
|
-
if (!
|
|
8785
|
+
if (!existsSync6(unitPath(name))) {
|
|
8065
8786
|
write(name);
|
|
8066
8787
|
await sc("daemon-reload");
|
|
8067
8788
|
}
|
|
@@ -8074,7 +8795,7 @@ function linuxManager(ctx) {
|
|
|
8074
8795
|
await sc("restart", unitName(name));
|
|
8075
8796
|
},
|
|
8076
8797
|
async status(name) {
|
|
8077
|
-
const installed =
|
|
8798
|
+
const installed = existsSync6(unitPath(name));
|
|
8078
8799
|
if (!installed) return { installed: false, loaded: false, running: false };
|
|
8079
8800
|
const active = await sc("is-active", unitName(name));
|
|
8080
8801
|
const enabled = await sc("is-enabled", unitName(name));
|
|
@@ -8084,7 +8805,7 @@ function linuxManager(ctx) {
|
|
|
8084
8805
|
installed,
|
|
8085
8806
|
loaded,
|
|
8086
8807
|
running,
|
|
8087
|
-
staleNodePath: !
|
|
8808
|
+
staleNodePath: !existsSync6(ctx.nodePath)
|
|
8088
8809
|
};
|
|
8089
8810
|
},
|
|
8090
8811
|
logsPath(name) {
|
|
@@ -8303,7 +9024,7 @@ import { dirname as dirname6, join as join10 } from "path";
|
|
|
8303
9024
|
|
|
8304
9025
|
// src/service/logRotation.ts
|
|
8305
9026
|
import {
|
|
8306
|
-
existsSync as
|
|
9027
|
+
existsSync as existsSync7,
|
|
8307
9028
|
statSync as statSync3,
|
|
8308
9029
|
renameSync as renameSync4,
|
|
8309
9030
|
rmSync as rmSync3,
|
|
@@ -8317,7 +9038,7 @@ function rotateLogFileIfNeeded(logPath, opts = {}) {
|
|
|
8317
9038
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
8318
9039
|
const keepCount = opts.keepCount ?? DEFAULT_KEEP_COUNT;
|
|
8319
9040
|
try {
|
|
8320
|
-
if (!
|
|
9041
|
+
if (!existsSync7(logPath)) return;
|
|
8321
9042
|
let size;
|
|
8322
9043
|
try {
|
|
8323
9044
|
size = statSync3(logPath).size;
|
|
@@ -8327,12 +9048,12 @@ function rotateLogFileIfNeeded(logPath, opts = {}) {
|
|
|
8327
9048
|
if (size <= maxBytes) return;
|
|
8328
9049
|
const gz = (n) => `${logPath}.${n}.gz`;
|
|
8329
9050
|
try {
|
|
8330
|
-
if (
|
|
9051
|
+
if (existsSync7(gz(keepCount))) rmSync3(gz(keepCount), { force: true });
|
|
8331
9052
|
} catch {
|
|
8332
9053
|
}
|
|
8333
9054
|
for (let n = keepCount - 1; n >= 1; n--) {
|
|
8334
9055
|
try {
|
|
8335
|
-
if (
|
|
9056
|
+
if (existsSync7(gz(n))) renameSync4(gz(n), gz(n + 1));
|
|
8336
9057
|
} catch {
|
|
8337
9058
|
}
|
|
8338
9059
|
}
|
|
@@ -8688,6 +9409,13 @@ async function main() {
|
|
|
8688
9409
|
if (invocation.kind === "attach") {
|
|
8689
9410
|
process.exit(await runAttach(invocation.agent));
|
|
8690
9411
|
}
|
|
9412
|
+
if (invocation.kind === "handoff") {
|
|
9413
|
+
process.exit(await runHandoffCommand({
|
|
9414
|
+
agent: invocation.agent,
|
|
9415
|
+
channel: invocation.channel,
|
|
9416
|
+
recover: invocation.recover
|
|
9417
|
+
}));
|
|
9418
|
+
}
|
|
8691
9419
|
const cfg = await resolveConfig(invocation.argv, process.env);
|
|
8692
9420
|
resolvedAgentName = cfg.agentName ?? "";
|
|
8693
9421
|
{
|
|
@@ -8767,8 +9495,8 @@ async function main() {
|
|
|
8767
9495
|
// operator types at, so passing it to rl.question (rather than pre-writing it)
|
|
8768
9496
|
// means readline keeps it on screen instead of clearing it (BL-178).
|
|
8769
9497
|
ask: async (prompt) => {
|
|
8770
|
-
const { createInterface } = await import("readline/promises");
|
|
8771
|
-
const rl =
|
|
9498
|
+
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
9499
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
8772
9500
|
let cancelled = false;
|
|
8773
9501
|
rl.on("SIGINT", () => {
|
|
8774
9502
|
cancelled = true;
|
|
@@ -8879,7 +9607,8 @@ async function main() {
|
|
|
8879
9607
|
console.log("[braid] connecting to the relay...");
|
|
8880
9608
|
const keysLive = attachEligible(process.stdin, process.stdout, process.env);
|
|
8881
9609
|
const serviceEligible = process.env.BRAID_SERVICE !== "1" && !!cfg.agentName;
|
|
8882
|
-
const wantSocket = process.env.BRAID_SERVICE === "1" || invocation.argv.includes("--socket");
|
|
9610
|
+
const wantSocket = process.env.BRAID_SERVICE === "1" || !invocation.argv.includes("--no-socket");
|
|
9611
|
+
const handoffGate = new HandoffGate();
|
|
8883
9612
|
let controlSocketCfg;
|
|
8884
9613
|
if (wantSocket && cfg.agentName) {
|
|
8885
9614
|
const braidDir = configDir(process.env);
|
|
@@ -8894,7 +9623,8 @@ async function main() {
|
|
|
8894
9623
|
ownsLock: () => true,
|
|
8895
9624
|
logsPath,
|
|
8896
9625
|
blockedEndpoints: blockedEndpointHosts,
|
|
8897
|
-
tokenPath: controlTokenPath(cfg.agentName, braidDir)
|
|
9626
|
+
tokenPath: controlTokenPath(cfg.agentName, braidDir),
|
|
9627
|
+
handoffGate
|
|
8898
9628
|
};
|
|
8899
9629
|
}
|
|
8900
9630
|
const bridge = await createAndStartBridge(cfg, {
|
|
@@ -8955,7 +9685,17 @@ async function main() {
|
|
|
8955
9685
|
out.write("\n[handoff] that session is no longer active\n");
|
|
8956
9686
|
return;
|
|
8957
9687
|
}
|
|
8958
|
-
|
|
9688
|
+
if (!handoffGate.tryAcquire(channelId, "terminal")) {
|
|
9689
|
+
const g = handoffGate.snapshot();
|
|
9690
|
+
out.write(g.state === "orphaned" ? `
|
|
9691
|
+
[handoff] a previous handoff was orphaned - run: braid handoff ${cfg.agentName ?? ""} --recover`.trimEnd() + "\n" : "\n[handoff] another handoff is already in progress\n");
|
|
9692
|
+
return;
|
|
9693
|
+
}
|
|
9694
|
+
try {
|
|
9695
|
+
await runHandoff({ channelId, provider: cfg.agent, session, cwd: handoffCwdFor(cfg, channelId), out, channelLabel: channelLabel(channelId), scope: cfg.scope });
|
|
9696
|
+
} finally {
|
|
9697
|
+
handoffGate.release();
|
|
9698
|
+
}
|
|
8959
9699
|
}
|
|
8960
9700
|
});
|
|
8961
9701
|
}
|