@bondsmith/bondsmithd 0.1.12 → 0.1.13
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 +103 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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
|
|
@@ -16,7 +16,15 @@ var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
|
|
|
16
16
|
var INVALID_MESSAGE_ERROR = "invalid message";
|
|
17
17
|
|
|
18
18
|
// ../../packages/local-agent-protocol/src/protocol-version.ts
|
|
19
|
-
var PROTOCOL_VERSION =
|
|
19
|
+
var PROTOCOL_VERSION = 16;
|
|
20
|
+
|
|
21
|
+
// ../../packages/local-agent-protocol/src/branch-name.ts
|
|
22
|
+
var MAX_BRANCH_NAME_LENGTH = 250;
|
|
23
|
+
var BRANCH_CHARACTERS = /^[\p{L}\p{N}._/+#@-]+$/u;
|
|
24
|
+
var REJECTED_SHAPES = /^[-/]|\/\/|\.\.|@\{|^@$|[/.]$|(^|\/)\.|\.lock(\/|$)/;
|
|
25
|
+
function isValidBranchName(name) {
|
|
26
|
+
return name.length > 0 && name.length <= MAX_BRANCH_NAME_LENGTH && BRANCH_CHARACTERS.test(name) && !REJECTED_SHAPES.test(name);
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
// ../../packages/local-agent-protocol/src/worktree-setup.ts
|
|
22
30
|
var MAX_COPY_GLOBS = 50;
|
|
@@ -271,6 +279,7 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
|
|
|
271
279
|
worktreeBranch: z3.string().min(1).max(250).refine((value) => !value.startsWith("-"), {
|
|
272
280
|
message: "branch name can't start with -"
|
|
273
281
|
}).optional(),
|
|
282
|
+
baseBranch: z3.string().max(MAX_BRANCH_NAME_LENGTH).refine(isValidBranchName).optional(),
|
|
274
283
|
worktreeSetup: worktreeSetupSchema.optional(),
|
|
275
284
|
requestId: z3.string().min(1).max(200).optional()
|
|
276
285
|
}),
|
|
@@ -456,7 +465,8 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
|
|
|
456
465
|
expiresAt: z3.number().int().positive(),
|
|
457
466
|
// .catch(undefined): a malformed informational field must never fail the whole handshake message.
|
|
458
467
|
protocolVersion: z3.number().int().positive().optional().catch(void 0),
|
|
459
|
-
daemonVersion: z3.string().optional().catch(void 0)
|
|
468
|
+
daemonVersion: z3.string().optional().catch(void 0),
|
|
469
|
+
installedAgents: z3.array(z3.string().max(40)).max(20).optional().catch(void 0)
|
|
460
470
|
}),
|
|
461
471
|
z3.object({
|
|
462
472
|
type: z3.literal("connection.authenticated"),
|
|
@@ -1542,12 +1552,28 @@ async function commitExists(cwd, sha) {
|
|
|
1542
1552
|
return false;
|
|
1543
1553
|
}
|
|
1544
1554
|
}
|
|
1555
|
+
async function isAncestor(cwd, ancestor, descendant) {
|
|
1556
|
+
try {
|
|
1557
|
+
await runGit(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
1558
|
+
return true;
|
|
1559
|
+
} catch (error2) {
|
|
1560
|
+
if (error2.code === 1) return false;
|
|
1561
|
+
throw error2;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1545
1564
|
async function resolveBaseFor(cwd, baseline) {
|
|
1546
1565
|
if (!baseline) return resolveComparisonBase(cwd);
|
|
1547
1566
|
if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
|
|
1548
1567
|
return resolveComparisonBase(cwd);
|
|
1549
1568
|
}
|
|
1550
|
-
|
|
1569
|
+
const start2 = {
|
|
1570
|
+
sha: baseline.baseSha ?? EMPTY_TREE_SHA,
|
|
1571
|
+
ref: SESSION_START_REF
|
|
1572
|
+
};
|
|
1573
|
+
if (baseline.baseSha === null) return start2;
|
|
1574
|
+
const meeting = await resolveComparisonBase(cwd);
|
|
1575
|
+
if (meeting.ref === null || await isAncestor(cwd, meeting.sha, baseline.baseSha)) return start2;
|
|
1576
|
+
return meeting;
|
|
1551
1577
|
}
|
|
1552
1578
|
async function dropPreexisting(cwd, entries, baseline) {
|
|
1553
1579
|
if (baseline.preexisting.size === 0) return entries;
|
|
@@ -2183,8 +2209,27 @@ var ConnectionAuthState = class {
|
|
|
2183
2209
|
}
|
|
2184
2210
|
};
|
|
2185
2211
|
|
|
2212
|
+
// src/sessions/agents/installed-agents.ts
|
|
2213
|
+
import { accessSync, constants as constants3, statSync } from "fs";
|
|
2214
|
+
import { delimiter, join as join6 } from "path";
|
|
2215
|
+
var KNOWN_AGENTS = ["claude", "codex", "opencode"];
|
|
2216
|
+
function isExecutableFile(path) {
|
|
2217
|
+
try {
|
|
2218
|
+
accessSync(path, constants3.X_OK);
|
|
2219
|
+
return statSync(path).isFile();
|
|
2220
|
+
} catch {
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function listInstalledAgents(env = process.env) {
|
|
2225
|
+
const directories = (env.PATH ?? "").split(delimiter).filter((directory) => directory !== "");
|
|
2226
|
+
return KNOWN_AGENTS.filter(
|
|
2227
|
+
(agent) => directories.some((directory) => isExecutableFile(join6(directory, agent)))
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2186
2231
|
// src/version.ts
|
|
2187
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
2232
|
+
var DAEMON_VERSION = true ? "0.1.13" : "dev";
|
|
2188
2233
|
var VERSION_FLAGS = ["--version", "-v"];
|
|
2189
2234
|
function versionLine() {
|
|
2190
2235
|
return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
@@ -2555,13 +2600,14 @@ async function dispatch(conn, message, {
|
|
|
2555
2600
|
return;
|
|
2556
2601
|
}
|
|
2557
2602
|
case "session.create": {
|
|
2558
|
-
const { title, cwd, command, worktreeBranch, worktreeSetup, requestId } = message;
|
|
2603
|
+
const { title, cwd, command, worktreeBranch, baseBranch, worktreeSetup, requestId } = message;
|
|
2559
2604
|
try {
|
|
2560
2605
|
const session = await sessionManager.create({
|
|
2561
2606
|
title,
|
|
2562
2607
|
cwd,
|
|
2563
2608
|
command,
|
|
2564
2609
|
worktreeBranch,
|
|
2610
|
+
baseBranch,
|
|
2565
2611
|
worktreeSetup
|
|
2566
2612
|
});
|
|
2567
2613
|
broadcast({ type: "session.created", session, requestId });
|
|
@@ -2997,7 +3043,8 @@ function startRelayClient(deps) {
|
|
|
2997
3043
|
hostname: deps.hostname(),
|
|
2998
3044
|
expiresAt: challenge.expiresAt,
|
|
2999
3045
|
protocolVersion: PROTOCOL_VERSION,
|
|
3000
|
-
daemonVersion: DAEMON_VERSION
|
|
3046
|
+
daemonVersion: DAEMON_VERSION,
|
|
3047
|
+
installedAgents: listInstalledAgents()
|
|
3001
3048
|
});
|
|
3002
3049
|
}
|
|
3003
3050
|
function handleFrame(frame) {
|
|
@@ -3137,9 +3184,9 @@ function startRelayClient(deps) {
|
|
|
3137
3184
|
import { generateKeyPairSync, sign as sign2, verify as verify4 } from "crypto";
|
|
3138
3185
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3139
3186
|
import { homedir as homedir4 } from "os";
|
|
3140
|
-
import { dirname as dirname2, join as
|
|
3187
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
3141
3188
|
function defaultCredentialsPath() {
|
|
3142
|
-
return
|
|
3189
|
+
return join7(homedir4(), ".bondsmith", "credentials");
|
|
3143
3190
|
}
|
|
3144
3191
|
async function loadOrCreateDaemonIdentity(credentialsPath = defaultCredentialsPath()) {
|
|
3145
3192
|
const existing = await tryReadIdentity(credentialsPath);
|
|
@@ -3208,9 +3255,9 @@ function createDaemonProofSigner(privateKeyPem) {
|
|
|
3208
3255
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3209
3256
|
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
3210
3257
|
import { homedir as homedir5 } from "os";
|
|
3211
|
-
import { dirname as dirname3, join as
|
|
3258
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
3212
3259
|
function defaultDevTunnelTokenPath() {
|
|
3213
|
-
return
|
|
3260
|
+
return join8(homedir5(), ".bondsmith", "dev-tunnel-token");
|
|
3214
3261
|
}
|
|
3215
3262
|
async function loadOrCreateDevTunnelToken(tokenPath = defaultDevTunnelTokenPath()) {
|
|
3216
3263
|
const existing = await tryReadToken(tokenPath);
|
|
@@ -3834,11 +3881,11 @@ function stderrOf(error2) {
|
|
|
3834
3881
|
const stderr = error2.stderr;
|
|
3835
3882
|
return typeof stderr === "string" ? stderr : "";
|
|
3836
3883
|
}
|
|
3837
|
-
async function createWorktree(sourceCwd, worktreePath, branch) {
|
|
3884
|
+
async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
|
|
3885
|
+
const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
|
|
3886
|
+
if (startPoint) args.push(startPoint);
|
|
3838
3887
|
try {
|
|
3839
|
-
await execFile3("git", [...SAFE_GIT_CONFIG,
|
|
3840
|
-
cwd: sourceCwd
|
|
3841
|
-
});
|
|
3888
|
+
await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
|
|
3842
3889
|
} catch (error2) {
|
|
3843
3890
|
const stderr = stderrOf(error2);
|
|
3844
3891
|
if (stderr.includes("a branch named")) {
|
|
@@ -3955,14 +4002,14 @@ function resolveSessionAttention(report, lastActivityAt, { nowMs, acknowledgedAt
|
|
|
3955
4002
|
// src/sessions/tmux/create.ts
|
|
3956
4003
|
import { access } from "fs/promises";
|
|
3957
4004
|
import { homedir as homedir7 } from "os";
|
|
3958
|
-
import { join as
|
|
4005
|
+
import { join as join11 } from "path";
|
|
3959
4006
|
|
|
3960
4007
|
// src/sessions/agents/claude-hooks.ts
|
|
3961
4008
|
import { execFile as execFile4 } from "child_process";
|
|
3962
4009
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3963
4010
|
import { mkdir as mkdir4, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
3964
4011
|
import { homedir as homedir6 } from "os";
|
|
3965
|
-
import { join as
|
|
4012
|
+
import { join as join9 } from "path";
|
|
3966
4013
|
import { promisify as promisify4 } from "util";
|
|
3967
4014
|
|
|
3968
4015
|
// src/sessions/session-env.ts
|
|
@@ -4044,7 +4091,7 @@ function claudeSettings() {
|
|
|
4044
4091
|
|
|
4045
4092
|
// src/sessions/agents/claude-hooks.ts
|
|
4046
4093
|
var execFileAsync = promisify4(execFile4);
|
|
4047
|
-
var CLAUDE_HOOKS_DIR =
|
|
4094
|
+
var CLAUDE_HOOKS_DIR = join9(homedir6(), ".bondsmith", "hooks");
|
|
4048
4095
|
var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
|
|
4049
4096
|
var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
|
|
4050
4097
|
var CLAUDE_BINARY = "claude";
|
|
@@ -4096,7 +4143,7 @@ async function prepareAgentCommand(command, {
|
|
|
4096
4143
|
if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
|
|
4097
4144
|
if (!isPlainClaudeCommand(command)) return command;
|
|
4098
4145
|
if (!await supportsSettings()) return command;
|
|
4099
|
-
const file =
|
|
4146
|
+
const file = join9(hooksDir, "claude-settings.json");
|
|
4100
4147
|
const staging = `${file}.${randomUUID2()}.tmp`;
|
|
4101
4148
|
try {
|
|
4102
4149
|
await mkdir4(hooksDir, { recursive: true });
|
|
@@ -4116,10 +4163,33 @@ async function prepareAgentCommand(command, {
|
|
|
4116
4163
|
);
|
|
4117
4164
|
}
|
|
4118
4165
|
|
|
4166
|
+
// src/sessions/git/start-point.ts
|
|
4167
|
+
var WorktreeBaseBranchNotFoundError = class extends Error {
|
|
4168
|
+
constructor(branch) {
|
|
4169
|
+
super(`base branch "${branch}" was not found (looked for origin/${branch} and ${branch})`);
|
|
4170
|
+
this.name = "WorktreeBaseBranchNotFoundError";
|
|
4171
|
+
}
|
|
4172
|
+
};
|
|
4173
|
+
async function commitExists2(ref, cwd) {
|
|
4174
|
+
try {
|
|
4175
|
+
await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
|
|
4176
|
+
return true;
|
|
4177
|
+
} catch {
|
|
4178
|
+
return false;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
|
|
4182
|
+
if (baseBranch === void 0) return await findDefaultBranchRef(sourceCwd) ?? void 0;
|
|
4183
|
+
for (const candidate of [`origin/${baseBranch}`, baseBranch]) {
|
|
4184
|
+
if (await commitExists2(candidate, sourceCwd)) return candidate;
|
|
4185
|
+
}
|
|
4186
|
+
throw new WorktreeBaseBranchNotFoundError(baseBranch);
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4119
4189
|
// src/sessions/worktree-setup/copy-files.ts
|
|
4120
|
-
import { constants as
|
|
4190
|
+
import { constants as constants4 } from "fs";
|
|
4121
4191
|
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
|
|
4192
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative, sep as sep2 } from "path";
|
|
4123
4193
|
function isInside(root, path) {
|
|
4124
4194
|
const rel = relative(root, path);
|
|
4125
4195
|
return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
|
|
@@ -4147,14 +4217,14 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
|
|
|
4147
4217
|
return result;
|
|
4148
4218
|
}
|
|
4149
4219
|
async function copyOne(source, worktree, match) {
|
|
4150
|
-
const from =
|
|
4220
|
+
const from = join10(source, match);
|
|
4151
4221
|
if (!(await lstat3(from)).isFile()) return false;
|
|
4152
4222
|
if (!isInside(source, await realpath2(from))) return false;
|
|
4153
|
-
const to =
|
|
4223
|
+
const to = join10(worktree, match);
|
|
4154
4224
|
await mkdir5(dirname4(to), { recursive: true });
|
|
4155
4225
|
if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
|
|
4156
4226
|
try {
|
|
4157
|
-
await copyFile(from, to,
|
|
4227
|
+
await copyFile(from, to, constants4.COPYFILE_EXCL);
|
|
4158
4228
|
} catch (error2) {
|
|
4159
4229
|
if (error2.code === "EEXIST") return false;
|
|
4160
4230
|
throw error2;
|
|
@@ -4170,7 +4240,7 @@ var InvalidCwdError = class extends Error {
|
|
|
4170
4240
|
}
|
|
4171
4241
|
};
|
|
4172
4242
|
function worktreePathFor(sessionId) {
|
|
4173
|
-
return
|
|
4243
|
+
return join11(homedir7(), ".bondsmith", "worktrees", sessionId);
|
|
4174
4244
|
}
|
|
4175
4245
|
async function createTmuxSession(input) {
|
|
4176
4246
|
const sourceCwd = expandHome(input.cwd);
|
|
@@ -4179,7 +4249,8 @@ async function createTmuxSession(input) {
|
|
|
4179
4249
|
let setupFailed = false;
|
|
4180
4250
|
if (input.worktreeBranch) {
|
|
4181
4251
|
const worktreePath = worktreePathFor(input.id);
|
|
4182
|
-
await
|
|
4252
|
+
const startPoint = await resolveWorktreeStartPoint(sourceCwd, input.baseBranch);
|
|
4253
|
+
await createWorktree(sourceCwd, worktreePath, input.worktreeBranch, startPoint);
|
|
4183
4254
|
cwd = worktreePath;
|
|
4184
4255
|
worktreeOf = sourceCwd;
|
|
4185
4256
|
if (input.worktreeSetup?.copyGlobs.length) {
|
|
@@ -4580,7 +4651,8 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
|
|
|
4580
4651
|
hostname: hostname(),
|
|
4581
4652
|
expiresAt: challenge.expiresAt,
|
|
4582
4653
|
protocolVersion: PROTOCOL_VERSION,
|
|
4583
|
-
daemonVersion: DAEMON_VERSION
|
|
4654
|
+
daemonVersion: DAEMON_VERSION,
|
|
4655
|
+
installedAgents: listInstalledAgents()
|
|
4584
4656
|
});
|
|
4585
4657
|
const unauthenticatedTimer = setTimeout(() => {
|
|
4586
4658
|
if (!connectionAuth.isAuthenticated()) socket.close();
|
|
@@ -4645,10 +4717,10 @@ if (isVersionRequest(process.argv.slice(2))) {
|
|
|
4645
4717
|
process.exit(0);
|
|
4646
4718
|
}
|
|
4647
4719
|
function loadRepoRootEnv() {
|
|
4648
|
-
const repoRoot =
|
|
4649
|
-
if (!existsSync(
|
|
4650
|
-
const envLocal =
|
|
4651
|
-
const envDefault =
|
|
4720
|
+
const repoRoot = join12(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
4721
|
+
if (!existsSync(join12(repoRoot, "pnpm-workspace.yaml"))) return;
|
|
4722
|
+
const envLocal = join12(repoRoot, ".env.local");
|
|
4723
|
+
const envDefault = join12(repoRoot, ".env");
|
|
4652
4724
|
loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
|
|
4653
4725
|
}
|
|
4654
4726
|
loadRepoRootEnv();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondsmith/bondsmithd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|