@bondsmith/bondsmithd 0.1.12 → 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 +256 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,19 +4,28 @@
|
|
|
4
4
|
import { createServer } from "http";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import { hostname as hostname2 } from "os";
|
|
7
|
-
import { dirname as dirname5, join as
|
|
7
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
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;
|
|
21
|
+
|
|
22
|
+
// ../../packages/local-agent-protocol/src/branch-name.ts
|
|
23
|
+
var MAX_BRANCH_NAME_LENGTH = 250;
|
|
24
|
+
var BRANCH_CHARACTERS = /^[\p{L}\p{N}._/+#@-]+$/u;
|
|
25
|
+
var REJECTED_SHAPES = /^[-/]|\/\/|\.\.|@\{|^@$|[/.]$|(^|\/)\.|\.lock(\/|$)/;
|
|
26
|
+
function isValidBranchName(name) {
|
|
27
|
+
return name.length > 0 && name.length <= MAX_BRANCH_NAME_LENGTH && BRANCH_CHARACTERS.test(name) && !REJECTED_SHAPES.test(name);
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
// ../../packages/local-agent-protocol/src/worktree-setup.ts
|
|
22
31
|
var MAX_COPY_GLOBS = 50;
|
|
@@ -196,6 +205,7 @@ var localSessionSchema = z3.object({
|
|
|
196
205
|
// .catch(undefined): a state this version doesn't know must never drop the whole session message.
|
|
197
206
|
agentState: z3.enum(AGENT_STATES).nullable().optional().catch(void 0),
|
|
198
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),
|
|
199
209
|
headline: z3.string().nullable().optional().catch(void 0)
|
|
200
210
|
});
|
|
201
211
|
var workspaceFileEntrySchema = z3.object({
|
|
@@ -271,6 +281,7 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
|
|
|
271
281
|
worktreeBranch: z3.string().min(1).max(250).refine((value) => !value.startsWith("-"), {
|
|
272
282
|
message: "branch name can't start with -"
|
|
273
283
|
}).optional(),
|
|
284
|
+
baseBranch: z3.string().max(MAX_BRANCH_NAME_LENGTH).refine(isValidBranchName).optional(),
|
|
274
285
|
worktreeSetup: worktreeSetupSchema.optional(),
|
|
275
286
|
requestId: z3.string().min(1).max(200).optional()
|
|
276
287
|
}),
|
|
@@ -456,7 +467,8 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
|
|
|
456
467
|
expiresAt: z3.number().int().positive(),
|
|
457
468
|
// .catch(undefined): a malformed informational field must never fail the whole handshake message.
|
|
458
469
|
protocolVersion: z3.number().int().positive().optional().catch(void 0),
|
|
459
|
-
daemonVersion: z3.string().optional().catch(void 0)
|
|
470
|
+
daemonVersion: z3.string().optional().catch(void 0),
|
|
471
|
+
installedAgents: z3.array(z3.string().max(40)).max(20).optional().catch(void 0)
|
|
460
472
|
}),
|
|
461
473
|
z3.object({
|
|
462
474
|
type: z3.literal("connection.authenticated"),
|
|
@@ -1174,7 +1186,7 @@ function expandHome(cwd) {
|
|
|
1174
1186
|
}
|
|
1175
1187
|
|
|
1176
1188
|
// src/sessions/git/client.ts
|
|
1177
|
-
import { execFile as execFileCb } from "child_process";
|
|
1189
|
+
import { execFile as execFileCb, spawn as spawn2 } from "child_process";
|
|
1178
1190
|
import { promisify } from "util";
|
|
1179
1191
|
var execFile = promisify(execFileCb);
|
|
1180
1192
|
var MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
@@ -1229,6 +1241,43 @@ function runGitDiff(args, cwd) {
|
|
|
1229
1241
|
function runGitDiffAllowingExitCodes(args, cwd, allowedExitCodes) {
|
|
1230
1242
|
return runGitAllowingExitCodes(["diff", ...SAFE_DIFF_FLAGS, ...args], cwd, allowedExitCodes);
|
|
1231
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
|
+
}
|
|
1232
1281
|
|
|
1233
1282
|
// src/sessions/files/plan-file.ts
|
|
1234
1283
|
import { constants as constants2 } from "fs";
|
|
@@ -1542,12 +1591,28 @@ async function commitExists(cwd, sha) {
|
|
|
1542
1591
|
return false;
|
|
1543
1592
|
}
|
|
1544
1593
|
}
|
|
1594
|
+
async function isAncestor(cwd, ancestor, descendant) {
|
|
1595
|
+
try {
|
|
1596
|
+
await runGit(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
1597
|
+
return true;
|
|
1598
|
+
} catch (error2) {
|
|
1599
|
+
if (error2.code === 1) return false;
|
|
1600
|
+
throw error2;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1545
1603
|
async function resolveBaseFor(cwd, baseline) {
|
|
1546
1604
|
if (!baseline) return resolveComparisonBase(cwd);
|
|
1547
1605
|
if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
|
|
1548
1606
|
return resolveComparisonBase(cwd);
|
|
1549
1607
|
}
|
|
1550
|
-
|
|
1608
|
+
const start2 = {
|
|
1609
|
+
sha: baseline.baseSha ?? EMPTY_TREE_SHA,
|
|
1610
|
+
ref: SESSION_START_REF
|
|
1611
|
+
};
|
|
1612
|
+
if (baseline.baseSha === null) return start2;
|
|
1613
|
+
const meeting = await resolveComparisonBase(cwd);
|
|
1614
|
+
if (meeting.ref === null || await isAncestor(cwd, meeting.sha, baseline.baseSha)) return start2;
|
|
1615
|
+
return meeting;
|
|
1551
1616
|
}
|
|
1552
1617
|
async function dropPreexisting(cwd, entries, baseline) {
|
|
1553
1618
|
if (baseline.preexisting.size === 0) return entries;
|
|
@@ -2183,8 +2248,27 @@ var ConnectionAuthState = class {
|
|
|
2183
2248
|
}
|
|
2184
2249
|
};
|
|
2185
2250
|
|
|
2251
|
+
// src/sessions/agents/installed-agents.ts
|
|
2252
|
+
import { accessSync, constants as constants3, statSync } from "fs";
|
|
2253
|
+
import { delimiter, join as join6 } from "path";
|
|
2254
|
+
var KNOWN_AGENTS = ["claude", "codex", "opencode"];
|
|
2255
|
+
function isExecutableFile(path) {
|
|
2256
|
+
try {
|
|
2257
|
+
accessSync(path, constants3.X_OK);
|
|
2258
|
+
return statSync(path).isFile();
|
|
2259
|
+
} catch {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function listInstalledAgents(env = process.env) {
|
|
2264
|
+
const directories = (env.PATH ?? "").split(delimiter).filter((directory) => directory !== "");
|
|
2265
|
+
return KNOWN_AGENTS.filter(
|
|
2266
|
+
(agent) => directories.some((directory) => isExecutableFile(join6(directory, agent)))
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2186
2270
|
// src/version.ts
|
|
2187
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
2271
|
+
var DAEMON_VERSION = true ? "0.1.14" : "dev";
|
|
2188
2272
|
var VERSION_FLAGS = ["--version", "-v"];
|
|
2189
2273
|
function versionLine() {
|
|
2190
2274
|
return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
@@ -2555,13 +2639,14 @@ async function dispatch(conn, message, {
|
|
|
2555
2639
|
return;
|
|
2556
2640
|
}
|
|
2557
2641
|
case "session.create": {
|
|
2558
|
-
const { title, cwd, command, worktreeBranch, worktreeSetup, requestId } = message;
|
|
2642
|
+
const { title, cwd, command, worktreeBranch, baseBranch, worktreeSetup, requestId } = message;
|
|
2559
2643
|
try {
|
|
2560
2644
|
const session = await sessionManager.create({
|
|
2561
2645
|
title,
|
|
2562
2646
|
cwd,
|
|
2563
2647
|
command,
|
|
2564
2648
|
worktreeBranch,
|
|
2649
|
+
baseBranch,
|
|
2565
2650
|
worktreeSetup
|
|
2566
2651
|
});
|
|
2567
2652
|
broadcast({ type: "session.created", session, requestId });
|
|
@@ -2997,7 +3082,8 @@ function startRelayClient(deps) {
|
|
|
2997
3082
|
hostname: deps.hostname(),
|
|
2998
3083
|
expiresAt: challenge.expiresAt,
|
|
2999
3084
|
protocolVersion: PROTOCOL_VERSION,
|
|
3000
|
-
daemonVersion: DAEMON_VERSION
|
|
3085
|
+
daemonVersion: DAEMON_VERSION,
|
|
3086
|
+
installedAgents: listInstalledAgents()
|
|
3001
3087
|
});
|
|
3002
3088
|
}
|
|
3003
3089
|
function handleFrame(frame) {
|
|
@@ -3137,9 +3223,9 @@ function startRelayClient(deps) {
|
|
|
3137
3223
|
import { generateKeyPairSync, sign as sign2, verify as verify4 } from "crypto";
|
|
3138
3224
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3139
3225
|
import { homedir as homedir4 } from "os";
|
|
3140
|
-
import { dirname as dirname2, join as
|
|
3226
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
3141
3227
|
function defaultCredentialsPath() {
|
|
3142
|
-
return
|
|
3228
|
+
return join7(homedir4(), ".bondsmith", "credentials");
|
|
3143
3229
|
}
|
|
3144
3230
|
async function loadOrCreateDaemonIdentity(credentialsPath = defaultCredentialsPath()) {
|
|
3145
3231
|
const existing = await tryReadIdentity(credentialsPath);
|
|
@@ -3208,9 +3294,9 @@ function createDaemonProofSigner(privateKeyPem) {
|
|
|
3208
3294
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3209
3295
|
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
3210
3296
|
import { homedir as homedir5 } from "os";
|
|
3211
|
-
import { dirname as dirname3, join as
|
|
3297
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
3212
3298
|
function defaultDevTunnelTokenPath() {
|
|
3213
|
-
return
|
|
3299
|
+
return join8(homedir5(), ".bondsmith", "dev-tunnel-token");
|
|
3214
3300
|
}
|
|
3215
3301
|
async function loadOrCreateDevTunnelToken(tokenPath = defaultDevTunnelTokenPath()) {
|
|
3216
3302
|
const existing = await tryReadToken(tokenPath);
|
|
@@ -3834,11 +3920,11 @@ function stderrOf(error2) {
|
|
|
3834
3920
|
const stderr = error2.stderr;
|
|
3835
3921
|
return typeof stderr === "string" ? stderr : "";
|
|
3836
3922
|
}
|
|
3837
|
-
async function createWorktree(sourceCwd, worktreePath, branch) {
|
|
3923
|
+
async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
|
|
3924
|
+
const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
|
|
3925
|
+
if (startPoint) args.push(startPoint);
|
|
3838
3926
|
try {
|
|
3839
|
-
await execFile3("git", [...SAFE_GIT_CONFIG,
|
|
3840
|
-
cwd: sourceCwd
|
|
3841
|
-
});
|
|
3927
|
+
await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
|
|
3842
3928
|
} catch (error2) {
|
|
3843
3929
|
const stderr = stderrOf(error2);
|
|
3844
3930
|
if (stderr.includes("a branch named")) {
|
|
@@ -3887,11 +3973,25 @@ function wrapCommandWithSetup(input) {
|
|
|
3887
3973
|
].join(" ");
|
|
3888
3974
|
}
|
|
3889
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
|
+
|
|
3890
3990
|
// src/sessions/tmux/agent-state.ts
|
|
3891
3991
|
var AGENT_STATE_OPTION = "@bondsmith-agent-state";
|
|
3892
3992
|
var HEADLINE_OPTION = "@bondsmith-headline";
|
|
3893
3993
|
var MAX_HEADLINE_CHARS = 200;
|
|
3894
|
-
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}}`;
|
|
3895
3995
|
var AGENT_STATE_OUTPUT_GRACE_MS = 2e3;
|
|
3896
3996
|
var FUTURE_REPORT_ALLOWANCE_MS = 2e3;
|
|
3897
3997
|
var WORKING_SILENCE_MS = 12e4;
|
|
@@ -3909,12 +4009,20 @@ function isAgentState(value) {
|
|
|
3909
4009
|
function parseLiveSessionReports(output) {
|
|
3910
4010
|
const reports = /* @__PURE__ */ new Map();
|
|
3911
4011
|
for (const line of output.split("\n")) {
|
|
3912
|
-
const [
|
|
4012
|
+
const [
|
|
4013
|
+
sessionName,
|
|
4014
|
+
agentState = "",
|
|
4015
|
+
bell = "",
|
|
4016
|
+
setupFailed = "",
|
|
4017
|
+
setupSkipped = "",
|
|
4018
|
+
...headline
|
|
4019
|
+
] = line.split(" ");
|
|
3913
4020
|
if (sessionName) {
|
|
3914
4021
|
reports.set(sessionName, {
|
|
3915
4022
|
agentState,
|
|
3916
4023
|
bell: bell === "1",
|
|
3917
4024
|
setupFailed: setupFailed === "1",
|
|
4025
|
+
setupSkipped: decodeSkippedFiles(setupSkipped),
|
|
3918
4026
|
headline: cleanHeadline(headline.join(" "))
|
|
3919
4027
|
});
|
|
3920
4028
|
}
|
|
@@ -3955,14 +4063,14 @@ function resolveSessionAttention(report, lastActivityAt, { nowMs, acknowledgedAt
|
|
|
3955
4063
|
// src/sessions/tmux/create.ts
|
|
3956
4064
|
import { access } from "fs/promises";
|
|
3957
4065
|
import { homedir as homedir7 } from "os";
|
|
3958
|
-
import { join as
|
|
4066
|
+
import { join as join11 } from "path";
|
|
3959
4067
|
|
|
3960
4068
|
// src/sessions/agents/claude-hooks.ts
|
|
3961
4069
|
import { execFile as execFile4 } from "child_process";
|
|
3962
4070
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3963
4071
|
import { mkdir as mkdir4, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
3964
4072
|
import { homedir as homedir6 } from "os";
|
|
3965
|
-
import { join as
|
|
4073
|
+
import { join as join9 } from "path";
|
|
3966
4074
|
import { promisify as promisify4 } from "util";
|
|
3967
4075
|
|
|
3968
4076
|
// src/sessions/session-env.ts
|
|
@@ -4044,7 +4152,7 @@ function claudeSettings() {
|
|
|
4044
4152
|
|
|
4045
4153
|
// src/sessions/agents/claude-hooks.ts
|
|
4046
4154
|
var execFileAsync = promisify4(execFile4);
|
|
4047
|
-
var CLAUDE_HOOKS_DIR =
|
|
4155
|
+
var CLAUDE_HOOKS_DIR = join9(homedir6(), ".bondsmith", "hooks");
|
|
4048
4156
|
var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
|
|
4049
4157
|
var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
|
|
4050
4158
|
var CLAUDE_BINARY = "claude";
|
|
@@ -4096,7 +4204,7 @@ async function prepareAgentCommand(command, {
|
|
|
4096
4204
|
if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
|
|
4097
4205
|
if (!isPlainClaudeCommand(command)) return command;
|
|
4098
4206
|
if (!await supportsSettings()) return command;
|
|
4099
|
-
const file =
|
|
4207
|
+
const file = join9(hooksDir, "claude-settings.json");
|
|
4100
4208
|
const staging = `${file}.${randomUUID2()}.tmp`;
|
|
4101
4209
|
try {
|
|
4102
4210
|
await mkdir4(hooksDir, { recursive: true });
|
|
@@ -4116,23 +4224,101 @@ async function prepareAgentCommand(command, {
|
|
|
4116
4224
|
);
|
|
4117
4225
|
}
|
|
4118
4226
|
|
|
4227
|
+
// src/sessions/git/start-point.ts
|
|
4228
|
+
var WorktreeBaseBranchNotFoundError = class extends Error {
|
|
4229
|
+
constructor(branch) {
|
|
4230
|
+
super(`base branch "${branch}" was not found (looked for origin/${branch} and ${branch})`);
|
|
4231
|
+
this.name = "WorktreeBaseBranchNotFoundError";
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
async function commitExists2(ref, cwd) {
|
|
4235
|
+
try {
|
|
4236
|
+
await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
|
|
4237
|
+
return true;
|
|
4238
|
+
} catch {
|
|
4239
|
+
return false;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
|
|
4243
|
+
if (baseBranch === void 0) return await findDefaultBranchRef(sourceCwd) ?? void 0;
|
|
4244
|
+
for (const candidate of [`origin/${baseBranch}`, baseBranch]) {
|
|
4245
|
+
if (await commitExists2(candidate, sourceCwd)) return candidate;
|
|
4246
|
+
}
|
|
4247
|
+
throw new WorktreeBaseBranchNotFoundError(baseBranch);
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4119
4250
|
// src/sessions/worktree-setup/copy-files.ts
|
|
4120
|
-
import { constants as
|
|
4251
|
+
import { constants as constants4 } from "fs";
|
|
4121
4252
|
import { copyFile, glob, lstat as lstat3, mkdir as mkdir5, realpath as realpath2 } from "fs/promises";
|
|
4122
|
-
import { dirname as dirname4, isAbsolute as isAbsolute3, join as
|
|
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;
|
|
4123
4256
|
function isInside(root, path) {
|
|
4124
4257
|
const rel = relative(root, path);
|
|
4125
4258
|
return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
|
|
4126
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
|
+
}
|
|
4127
4287
|
async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
|
|
4128
|
-
const result = { copied: [], failed: 0 };
|
|
4288
|
+
const result = { copied: [], failed: 0, skipped: [] };
|
|
4129
4289
|
const patterns = globs.filter(isSafeCopyGlob);
|
|
4130
4290
|
if (patterns.length === 0) return result;
|
|
4131
4291
|
try {
|
|
4132
4292
|
const source = await realpath2(sourceRoot);
|
|
4133
4293
|
const worktree = await realpath2(worktreeRoot);
|
|
4294
|
+
const candidates = [];
|
|
4134
4295
|
for await (const match of glob(patterns, { cwd: source })) {
|
|
4135
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
|
+
}
|
|
4136
4322
|
try {
|
|
4137
4323
|
if (await copyOne(source, worktree, match)) result.copied.push(match);
|
|
4138
4324
|
} catch (error2) {
|
|
@@ -4146,15 +4332,24 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
|
|
|
4146
4332
|
}
|
|
4147
4333
|
return result;
|
|
4148
4334
|
}
|
|
4149
|
-
async function
|
|
4150
|
-
const from =
|
|
4335
|
+
async function isCopyCandidate(source, worktree, match) {
|
|
4336
|
+
const from = join10(source, match);
|
|
4151
4337
|
if (!(await lstat3(from)).isFile()) return false;
|
|
4152
4338
|
if (!isInside(source, await realpath2(from))) return false;
|
|
4153
|
-
|
|
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) {
|
|
4348
|
+
const to = join10(worktree, match);
|
|
4154
4349
|
await mkdir5(dirname4(to), { recursive: true });
|
|
4155
4350
|
if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
|
|
4156
4351
|
try {
|
|
4157
|
-
await copyFile(
|
|
4352
|
+
await copyFile(join10(source, match), to, constants4.COPYFILE_EXCL);
|
|
4158
4353
|
} catch (error2) {
|
|
4159
4354
|
if (error2.code === "EEXIST") return false;
|
|
4160
4355
|
throw error2;
|
|
@@ -4170,16 +4365,18 @@ var InvalidCwdError = class extends Error {
|
|
|
4170
4365
|
}
|
|
4171
4366
|
};
|
|
4172
4367
|
function worktreePathFor(sessionId) {
|
|
4173
|
-
return
|
|
4368
|
+
return join11(homedir7(), ".bondsmith", "worktrees", sessionId);
|
|
4174
4369
|
}
|
|
4175
4370
|
async function createTmuxSession(input) {
|
|
4176
4371
|
const sourceCwd = expandHome(input.cwd);
|
|
4177
4372
|
let cwd = sourceCwd;
|
|
4178
4373
|
let worktreeOf = null;
|
|
4179
4374
|
let setupFailed = false;
|
|
4375
|
+
let skippedFiles = [];
|
|
4180
4376
|
if (input.worktreeBranch) {
|
|
4181
4377
|
const worktreePath = worktreePathFor(input.id);
|
|
4182
|
-
await
|
|
4378
|
+
const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
|
|
4379
|
+
await createWorktree(sourceCwd, worktreePath, input.worktreeBranch, startPoint);
|
|
4183
4380
|
cwd = worktreePath;
|
|
4184
4381
|
worktreeOf = sourceCwd;
|
|
4185
4382
|
if (input.worktreeSetup?.copyGlobs.length) {
|
|
@@ -4189,6 +4386,7 @@ async function createTmuxSession(input) {
|
|
|
4189
4386
|
input.worktreeSetup.copyGlobs
|
|
4190
4387
|
);
|
|
4191
4388
|
setupFailed = copy.failed > 0;
|
|
4389
|
+
skippedFiles = copy.skipped;
|
|
4192
4390
|
}
|
|
4193
4391
|
}
|
|
4194
4392
|
try {
|
|
@@ -4226,6 +4424,15 @@ async function createTmuxSession(input) {
|
|
|
4226
4424
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
|
|
4227
4425
|
}
|
|
4228
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
|
+
}
|
|
4229
4436
|
if (baseline) {
|
|
4230
4437
|
await runTmux([
|
|
4231
4438
|
"set-option",
|
|
@@ -4248,7 +4455,10 @@ async function createTmuxSession(input) {
|
|
|
4248
4455
|
lastActivityAt: null,
|
|
4249
4456
|
changedFileCount: null,
|
|
4250
4457
|
worktreeOf,
|
|
4251
|
-
...setupFailed && { worktreeSetupFailed: true }
|
|
4458
|
+
...setupFailed && { worktreeSetupFailed: true },
|
|
4459
|
+
...skippedFiles.length > 0 && {
|
|
4460
|
+
worktreeSetupSkipped: skippedFiles.slice(0, MAX_SKIPPED_FILES_REPORTED)
|
|
4461
|
+
}
|
|
4252
4462
|
};
|
|
4253
4463
|
return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
|
|
4254
4464
|
}
|
|
@@ -4444,17 +4654,15 @@ ${error2.message}
|
|
|
4444
4654
|
session,
|
|
4445
4655
|
this.baselines.get(session.id)
|
|
4446
4656
|
);
|
|
4657
|
+
const report = agentReports.get(session.tmuxSessionName);
|
|
4447
4658
|
const updated = {
|
|
4448
4659
|
...enriched,
|
|
4449
|
-
agentState: resolveSessionAttention(
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
...agentReports.get(session.tmuxSessionName)?.setupFailed && {
|
|
4456
|
-
worktreeSetupFailed: true
|
|
4457
|
-
}
|
|
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 }
|
|
4458
4666
|
};
|
|
4459
4667
|
if (!snapshotEquals(session, updated)) {
|
|
4460
4668
|
this.sessions.set(session.id, updated);
|
|
@@ -4479,7 +4687,7 @@ ${error2.message}
|
|
|
4479
4687
|
}
|
|
4480
4688
|
};
|
|
4481
4689
|
function snapshotEquals(a, b) {
|
|
4482
|
-
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 ?? []);
|
|
4483
4691
|
}
|
|
4484
4692
|
|
|
4485
4693
|
// src/ws/server.ts
|
|
@@ -4580,7 +4788,8 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
|
|
|
4580
4788
|
hostname: hostname(),
|
|
4581
4789
|
expiresAt: challenge.expiresAt,
|
|
4582
4790
|
protocolVersion: PROTOCOL_VERSION,
|
|
4583
|
-
daemonVersion: DAEMON_VERSION
|
|
4791
|
+
daemonVersion: DAEMON_VERSION,
|
|
4792
|
+
installedAgents: listInstalledAgents()
|
|
4584
4793
|
});
|
|
4585
4794
|
const unauthenticatedTimer = setTimeout(() => {
|
|
4586
4795
|
if (!connectionAuth.isAuthenticated()) socket.close();
|
|
@@ -4645,10 +4854,10 @@ if (isVersionRequest(process.argv.slice(2))) {
|
|
|
4645
4854
|
process.exit(0);
|
|
4646
4855
|
}
|
|
4647
4856
|
function loadRepoRootEnv() {
|
|
4648
|
-
const repoRoot =
|
|
4649
|
-
if (!existsSync(
|
|
4650
|
-
const envLocal =
|
|
4651
|
-
const envDefault =
|
|
4857
|
+
const repoRoot = join12(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
4858
|
+
if (!existsSync(join12(repoRoot, "pnpm-workspace.yaml"))) return;
|
|
4859
|
+
const envLocal = join12(repoRoot, ".env.local");
|
|
4860
|
+
const envDefault = join12(repoRoot, ".env");
|
|
4652
4861
|
loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
|
|
4653
4862
|
}
|
|
4654
4863
|
loadRepoRootEnv();
|
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",
|