@bondsmith/bondsmithd 0.1.13 → 0.1.14
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/index.js +156 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,13 +10,14 @@ import { fileURLToPath } from "url";
|
|
|
10
10
|
// ../../packages/local-agent-protocol/src/session.ts
|
|
11
11
|
var SESSION_STATUSES = ["running", "exited"];
|
|
12
12
|
var AGENT_STATES = ["working", "waiting", "done"];
|
|
13
|
+
var MAX_SKIPPED_FILES_REPORTED = 20;
|
|
13
14
|
|
|
14
15
|
// ../../packages/local-agent-protocol/src/messages.ts
|
|
15
16
|
var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
|
|
16
17
|
var INVALID_MESSAGE_ERROR = "invalid message";
|
|
17
18
|
|
|
18
19
|
// ../../packages/local-agent-protocol/src/protocol-version.ts
|
|
19
|
-
var PROTOCOL_VERSION =
|
|
20
|
+
var PROTOCOL_VERSION = 17;
|
|
20
21
|
|
|
21
22
|
// ../../packages/local-agent-protocol/src/branch-name.ts
|
|
22
23
|
var MAX_BRANCH_NAME_LENGTH = 250;
|
|
@@ -204,6 +205,7 @@ var localSessionSchema = z3.object({
|
|
|
204
205
|
// .catch(undefined): a state this version doesn't know must never drop the whole session message.
|
|
205
206
|
agentState: z3.enum(AGENT_STATES).nullable().optional().catch(void 0),
|
|
206
207
|
worktreeSetupFailed: z3.boolean().optional().catch(void 0),
|
|
208
|
+
worktreeSetupSkipped: z3.array(z3.string().max(300)).max(MAX_SKIPPED_FILES_REPORTED).optional().catch(void 0),
|
|
207
209
|
headline: z3.string().nullable().optional().catch(void 0)
|
|
208
210
|
});
|
|
209
211
|
var workspaceFileEntrySchema = z3.object({
|
|
@@ -1184,7 +1186,7 @@ function expandHome(cwd) {
|
|
|
1184
1186
|
}
|
|
1185
1187
|
|
|
1186
1188
|
// src/sessions/git/client.ts
|
|
1187
|
-
import { execFile as execFileCb } from "child_process";
|
|
1189
|
+
import { execFile as execFileCb, spawn as spawn2 } from "child_process";
|
|
1188
1190
|
import { promisify } from "util";
|
|
1189
1191
|
var execFile = promisify(execFileCb);
|
|
1190
1192
|
var MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
@@ -1239,6 +1241,43 @@ function runGitDiff(args, cwd) {
|
|
|
1239
1241
|
function runGitDiffAllowingExitCodes(args, cwd, allowedExitCodes) {
|
|
1240
1242
|
return runGitAllowingExitCodes(["diff", ...SAFE_DIFF_FLAGS, ...args], cwd, allowedExitCodes);
|
|
1241
1243
|
}
|
|
1244
|
+
var GIT_INPUT_TIMEOUT_MS = 3e4;
|
|
1245
|
+
async function runGitWithInput(args, cwd, input, options = {}) {
|
|
1246
|
+
const { allowedExitCodes = [], timeoutMs = GIT_INPUT_TIMEOUT_MS } = options;
|
|
1247
|
+
return new Promise((resolve2, reject) => {
|
|
1248
|
+
const child = spawn2("git", [...SAFE_GIT_CONFIG, ...args], { cwd });
|
|
1249
|
+
const stdout = [];
|
|
1250
|
+
const stderr = [];
|
|
1251
|
+
let settled = false;
|
|
1252
|
+
const settle = (finish) => {
|
|
1253
|
+
if (settled) return;
|
|
1254
|
+
settled = true;
|
|
1255
|
+
clearTimeout(timer);
|
|
1256
|
+
finish();
|
|
1257
|
+
};
|
|
1258
|
+
const timer = setTimeout(() => {
|
|
1259
|
+
child.kill("SIGKILL");
|
|
1260
|
+
settle(() => reject(new Error(`git ${args[0]} timed out after ${timeoutMs} ms`)));
|
|
1261
|
+
}, timeoutMs);
|
|
1262
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1263
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1264
|
+
child.stdin.on("error", () => {
|
|
1265
|
+
});
|
|
1266
|
+
child.on("error", (error2) => settle(() => reject(error2)));
|
|
1267
|
+
child.on(
|
|
1268
|
+
"close",
|
|
1269
|
+
(code) => settle(() => {
|
|
1270
|
+
if (code === 0 || code !== null && allowedExitCodes.includes(code)) {
|
|
1271
|
+
resolve2(Buffer.concat(stdout).toString("utf8"));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const message = Buffer.concat(stderr).toString("utf8").trim();
|
|
1275
|
+
reject(new Error(`git ${args[0]} exited with ${code}${message ? `: ${message}` : ""}`));
|
|
1276
|
+
})
|
|
1277
|
+
);
|
|
1278
|
+
child.stdin.end(input);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1242
1281
|
|
|
1243
1282
|
// src/sessions/files/plan-file.ts
|
|
1244
1283
|
import { constants as constants2 } from "fs";
|
|
@@ -2229,7 +2268,7 @@ function listInstalledAgents(env = process.env) {
|
|
|
2229
2268
|
}
|
|
2230
2269
|
|
|
2231
2270
|
// src/version.ts
|
|
2232
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
2271
|
+
var DAEMON_VERSION = true ? "0.1.14" : "dev";
|
|
2233
2272
|
var VERSION_FLAGS = ["--version", "-v"];
|
|
2234
2273
|
function versionLine() {
|
|
2235
2274
|
return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
@@ -3934,11 +3973,25 @@ function wrapCommandWithSetup(input) {
|
|
|
3934
3973
|
].join(" ");
|
|
3935
3974
|
}
|
|
3936
3975
|
|
|
3976
|
+
// src/sessions/worktree-setup/skipped-files.ts
|
|
3977
|
+
var SETUP_SKIPPED_OPTION = "@bondsmith-setup-skipped";
|
|
3978
|
+
function encodeSkippedFiles(names) {
|
|
3979
|
+
return names.slice(0, MAX_SKIPPED_FILES_REPORTED).map(encodeURIComponent).join(",");
|
|
3980
|
+
}
|
|
3981
|
+
function decodeSkippedFiles(raw) {
|
|
3982
|
+
if (raw === "") return [];
|
|
3983
|
+
try {
|
|
3984
|
+
return raw.split(",").map(decodeURIComponent);
|
|
3985
|
+
} catch {
|
|
3986
|
+
return [];
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3937
3990
|
// src/sessions/tmux/agent-state.ts
|
|
3938
3991
|
var AGENT_STATE_OPTION = "@bondsmith-agent-state";
|
|
3939
3992
|
var HEADLINE_OPTION = "@bondsmith-headline";
|
|
3940
3993
|
var MAX_HEADLINE_CHARS = 200;
|
|
3941
|
-
var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${HEADLINE_OPTION}}`;
|
|
3994
|
+
var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${SETUP_SKIPPED_OPTION}} #{${HEADLINE_OPTION}}`;
|
|
3942
3995
|
var AGENT_STATE_OUTPUT_GRACE_MS = 2e3;
|
|
3943
3996
|
var FUTURE_REPORT_ALLOWANCE_MS = 2e3;
|
|
3944
3997
|
var WORKING_SILENCE_MS = 12e4;
|
|
@@ -3956,12 +4009,20 @@ function isAgentState(value) {
|
|
|
3956
4009
|
function parseLiveSessionReports(output) {
|
|
3957
4010
|
const reports = /* @__PURE__ */ new Map();
|
|
3958
4011
|
for (const line of output.split("\n")) {
|
|
3959
|
-
const [
|
|
4012
|
+
const [
|
|
4013
|
+
sessionName,
|
|
4014
|
+
agentState = "",
|
|
4015
|
+
bell = "",
|
|
4016
|
+
setupFailed = "",
|
|
4017
|
+
setupSkipped = "",
|
|
4018
|
+
...headline
|
|
4019
|
+
] = line.split(" ");
|
|
3960
4020
|
if (sessionName) {
|
|
3961
4021
|
reports.set(sessionName, {
|
|
3962
4022
|
agentState,
|
|
3963
4023
|
bell: bell === "1",
|
|
3964
4024
|
setupFailed: setupFailed === "1",
|
|
4025
|
+
setupSkipped: decodeSkippedFiles(setupSkipped),
|
|
3965
4026
|
headline: cleanHeadline(headline.join(" "))
|
|
3966
4027
|
});
|
|
3967
4028
|
}
|
|
@@ -4190,19 +4251,74 @@ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
|
|
|
4190
4251
|
import { constants as constants4 } from "fs";
|
|
4191
4252
|
import { copyFile, glob, lstat as lstat3, mkdir as mkdir5, realpath as realpath2 } from "fs/promises";
|
|
4192
4253
|
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative, sep as sep2 } from "path";
|
|
4254
|
+
var GIT_EXIT_NONE_IGNORED = 1;
|
|
4255
|
+
var TRACKED_CHECK_BATCH = 200;
|
|
4193
4256
|
function isInside(root, path) {
|
|
4194
4257
|
const rel = relative(root, path);
|
|
4195
4258
|
return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
|
|
4196
4259
|
}
|
|
4260
|
+
async function ignoredPaths(worktree, paths) {
|
|
4261
|
+
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
4262
|
+
const output = await runGitWithInput(
|
|
4263
|
+
["check-ignore", "--stdin", "-z"],
|
|
4264
|
+
worktree,
|
|
4265
|
+
paths.map((path) => `${path}\0`).join(""),
|
|
4266
|
+
{ allowedExitCodes: [GIT_EXIT_NONE_IGNORED] }
|
|
4267
|
+
);
|
|
4268
|
+
return new Set(output.split("\0").filter((path) => path !== ""));
|
|
4269
|
+
}
|
|
4270
|
+
async function trackedPaths(worktree, paths) {
|
|
4271
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
4272
|
+
for (let start2 = 0; start2 < paths.length; start2 += TRACKED_CHECK_BATCH) {
|
|
4273
|
+
const output = await runGit(
|
|
4274
|
+
[
|
|
4275
|
+
"--literal-pathspecs",
|
|
4276
|
+
"ls-files",
|
|
4277
|
+
"-z",
|
|
4278
|
+
"--",
|
|
4279
|
+
...paths.slice(start2, start2 + TRACKED_CHECK_BATCH)
|
|
4280
|
+
],
|
|
4281
|
+
worktree
|
|
4282
|
+
);
|
|
4283
|
+
for (const path of output.split("\0")) if (path !== "") tracked.add(path);
|
|
4284
|
+
}
|
|
4285
|
+
return tracked;
|
|
4286
|
+
}
|
|
4197
4287
|
async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
|
|
4198
|
-
const result = { copied: [], failed: 0 };
|
|
4288
|
+
const result = { copied: [], failed: 0, skipped: [] };
|
|
4199
4289
|
const patterns = globs.filter(isSafeCopyGlob);
|
|
4200
4290
|
if (patterns.length === 0) return result;
|
|
4201
4291
|
try {
|
|
4202
4292
|
const source = await realpath2(sourceRoot);
|
|
4203
4293
|
const worktree = await realpath2(worktreeRoot);
|
|
4294
|
+
const candidates = [];
|
|
4204
4295
|
for await (const match of glob(patterns, { cwd: source })) {
|
|
4205
4296
|
if (match.split(sep2).includes(".git")) continue;
|
|
4297
|
+
try {
|
|
4298
|
+
if (await isCopyCandidate(source, worktree, match)) candidates.push(match);
|
|
4299
|
+
} catch (error2) {
|
|
4300
|
+
result.failed += 1;
|
|
4301
|
+
console.error("Failed to inspect a file to copy into the worktree", { match, error: error2 });
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
let tracked;
|
|
4305
|
+
let ignored;
|
|
4306
|
+
try {
|
|
4307
|
+
tracked = await trackedPaths(worktree, candidates);
|
|
4308
|
+
ignored = await ignoredPaths(worktree, candidates);
|
|
4309
|
+
} catch (error2) {
|
|
4310
|
+
result.failed += candidates.length;
|
|
4311
|
+
console.error("Could not ask git what the worktree tracks and ignores; copying nothing", {
|
|
4312
|
+
error: error2
|
|
4313
|
+
});
|
|
4314
|
+
return result;
|
|
4315
|
+
}
|
|
4316
|
+
for (const match of candidates) {
|
|
4317
|
+
if (tracked.has(match)) continue;
|
|
4318
|
+
if (!ignored.has(match)) {
|
|
4319
|
+
result.skipped.push(match);
|
|
4320
|
+
continue;
|
|
4321
|
+
}
|
|
4206
4322
|
try {
|
|
4207
4323
|
if (await copyOne(source, worktree, match)) result.copied.push(match);
|
|
4208
4324
|
} catch (error2) {
|
|
@@ -4216,15 +4332,24 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
|
|
|
4216
4332
|
}
|
|
4217
4333
|
return result;
|
|
4218
4334
|
}
|
|
4219
|
-
async function
|
|
4335
|
+
async function isCopyCandidate(source, worktree, match) {
|
|
4220
4336
|
const from = join10(source, match);
|
|
4221
4337
|
if (!(await lstat3(from)).isFile()) return false;
|
|
4222
4338
|
if (!isInside(source, await realpath2(from))) return false;
|
|
4339
|
+
try {
|
|
4340
|
+
await lstat3(join10(worktree, match));
|
|
4341
|
+
return false;
|
|
4342
|
+
} catch (error2) {
|
|
4343
|
+
if (error2.code === "ENOENT") return true;
|
|
4344
|
+
throw error2;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
async function copyOne(source, worktree, match) {
|
|
4223
4348
|
const to = join10(worktree, match);
|
|
4224
4349
|
await mkdir5(dirname4(to), { recursive: true });
|
|
4225
4350
|
if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
|
|
4226
4351
|
try {
|
|
4227
|
-
await copyFile(
|
|
4352
|
+
await copyFile(join10(source, match), to, constants4.COPYFILE_EXCL);
|
|
4228
4353
|
} catch (error2) {
|
|
4229
4354
|
if (error2.code === "EEXIST") return false;
|
|
4230
4355
|
throw error2;
|
|
@@ -4247,6 +4372,7 @@ async function createTmuxSession(input) {
|
|
|
4247
4372
|
let cwd = sourceCwd;
|
|
4248
4373
|
let worktreeOf = null;
|
|
4249
4374
|
let setupFailed = false;
|
|
4375
|
+
let skippedFiles = [];
|
|
4250
4376
|
if (input.worktreeBranch) {
|
|
4251
4377
|
const worktreePath = worktreePathFor(input.id);
|
|
4252
4378
|
const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
|
|
@@ -4260,6 +4386,7 @@ async function createTmuxSession(input) {
|
|
|
4260
4386
|
input.worktreeSetup.copyGlobs
|
|
4261
4387
|
);
|
|
4262
4388
|
setupFailed = copy.failed > 0;
|
|
4389
|
+
skippedFiles = copy.skipped;
|
|
4263
4390
|
}
|
|
4264
4391
|
}
|
|
4265
4392
|
try {
|
|
@@ -4297,6 +4424,15 @@ async function createTmuxSession(input) {
|
|
|
4297
4424
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
|
|
4298
4425
|
}
|
|
4299
4426
|
if (setupFailed) await runTmux(["set-option", "-t", tmuxSessionName, SETUP_FAILED_OPTION, "1"]);
|
|
4427
|
+
if (skippedFiles.length > 0) {
|
|
4428
|
+
await runTmux([
|
|
4429
|
+
"set-option",
|
|
4430
|
+
"-t",
|
|
4431
|
+
tmuxSessionName,
|
|
4432
|
+
SETUP_SKIPPED_OPTION,
|
|
4433
|
+
encodeSkippedFiles(skippedFiles)
|
|
4434
|
+
]);
|
|
4435
|
+
}
|
|
4300
4436
|
if (baseline) {
|
|
4301
4437
|
await runTmux([
|
|
4302
4438
|
"set-option",
|
|
@@ -4319,7 +4455,10 @@ async function createTmuxSession(input) {
|
|
|
4319
4455
|
lastActivityAt: null,
|
|
4320
4456
|
changedFileCount: null,
|
|
4321
4457
|
worktreeOf,
|
|
4322
|
-
...setupFailed && { worktreeSetupFailed: true }
|
|
4458
|
+
...setupFailed && { worktreeSetupFailed: true },
|
|
4459
|
+
...skippedFiles.length > 0 && {
|
|
4460
|
+
worktreeSetupSkipped: skippedFiles.slice(0, MAX_SKIPPED_FILES_REPORTED)
|
|
4461
|
+
}
|
|
4323
4462
|
};
|
|
4324
4463
|
return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
|
|
4325
4464
|
}
|
|
@@ -4515,17 +4654,15 @@ ${error2.message}
|
|
|
4515
4654
|
session,
|
|
4516
4655
|
this.baselines.get(session.id)
|
|
4517
4656
|
);
|
|
4657
|
+
const report = agentReports.get(session.tmuxSessionName);
|
|
4518
4658
|
const updated = {
|
|
4519
4659
|
...enriched,
|
|
4520
|
-
agentState: resolveSessionAttention(
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
...agentReports.get(session.tmuxSessionName)?.setupFailed && {
|
|
4527
|
-
worktreeSetupFailed: true
|
|
4528
|
-
}
|
|
4660
|
+
agentState: resolveSessionAttention(report, enriched.lastActivityAt, {
|
|
4661
|
+
acknowledgedAtMs: this.acknowledgements.get(session.id) ?? null
|
|
4662
|
+
}),
|
|
4663
|
+
headline: report?.headline ?? null,
|
|
4664
|
+
...report?.setupFailed && { worktreeSetupFailed: true },
|
|
4665
|
+
...report?.setupSkipped?.length && { worktreeSetupSkipped: report.setupSkipped }
|
|
4529
4666
|
};
|
|
4530
4667
|
if (!snapshotEquals(session, updated)) {
|
|
4531
4668
|
this.sessions.set(session.id, updated);
|
|
@@ -4550,7 +4687,7 @@ ${error2.message}
|
|
|
4550
4687
|
}
|
|
4551
4688
|
};
|
|
4552
4689
|
function snapshotEquals(a, b) {
|
|
4553
|
-
return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && (a.headline ?? null) === (b.headline ?? null) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false);
|
|
4690
|
+
return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && (a.headline ?? null) === (b.headline ?? null) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false) && JSON.stringify(a.worktreeSetupSkipped ?? []) === JSON.stringify(b.worktreeSetupSkipped ?? []);
|
|
4554
4691
|
}
|
|
4555
4692
|
|
|
4556
4693
|
// src/ws/server.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondsmith/bondsmithd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local daemon for Bondsmith agent sessions — runs on the user's own machine, pairs with a Bondsmith account, and exposes tmux-backed terminal sessions to the Bondsmith web app over a signed WebSocket protocol.",
|
|
6
6
|
"homepage": "https://bondsmith.dev",
|