@buildautomaton/cli 0.1.69 → 0.1.70

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
@@ -31064,7 +31064,7 @@ var {
31064
31064
  } = import_index.default;
31065
31065
 
31066
31066
  // src/cli-version.ts
31067
- var CLI_VERSION = "0.1.69".length > 0 ? "0.1.69" : "0.0.0-dev";
31067
+ var CLI_VERSION = "0.1.70".length > 0 ? "0.1.70" : "0.0.0-dev";
31068
31068
 
31069
31069
  // src/cli/defaults.ts
31070
31070
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
@@ -34902,6 +34902,18 @@ function getAgentModelFromAgentConfig(config2) {
34902
34902
  return t !== "" ? t : null;
34903
34903
  }
34904
34904
 
34905
+ // ../types/src/overview/heatmap/constants.ts
34906
+ var HOUR_MS = 36e5;
34907
+ var DAY_MS = 24 * HOUR_MS;
34908
+
34909
+ // ../types/src/bridges/agents/types.ts
34910
+ var INSTALLABLE_BRIDGE_AGENTS = [
34911
+ { value: "claude-code", label: "Anthropic Claude Code", tokenLabel: "Anthropic API key", tokenEnvVar: "ANTHROPIC_API_KEY" },
34912
+ { value: "codex-acp", label: "Codex", tokenLabel: "OpenAI API key", tokenEnvVar: "OPENAI_API_KEY" },
34913
+ { value: "cursor-cli", label: "Cursor CLI agent", tokenLabel: "Cursor API key", tokenEnvVar: "CURSOR_API_KEY" },
34914
+ { value: "opencode", label: "OpenCode agent", tokenLabel: "Provider API key", tokenEnvVar: "OPENCODE_API_KEY" }
34915
+ ];
34916
+
34905
34917
  // src/paths/session-layout-paths.ts
34906
34918
  import * as path26 from "node:path";
34907
34919
  function resolveIsolatedSessionParentPathFromCheckouts(worktreePaths) {
@@ -51489,6 +51501,7 @@ var API_TO_BRIDGE_MESSAGE_TYPES = [
51489
51501
  "code_nav",
51490
51502
  "skill_layout_request",
51491
51503
  "install_skills",
51504
+ "install_agent",
51492
51505
  "refresh_local_skills",
51493
51506
  "bridge_git_context_request",
51494
51507
  "list_repo_branches_request"
@@ -51564,6 +51577,100 @@ var handleBridgeHeartbeatAck = (msg, deps) => {
51564
51577
  deps.onBridgeHeartbeatAck?.(Math.trunc(raw));
51565
51578
  };
51566
51579
 
51580
+ // src/routing/dispatch/connection.ts
51581
+ function dispatchBridgeConnectionMessage(msg, deps) {
51582
+ switch (msg.type) {
51583
+ case "auth_token":
51584
+ handleAuthToken(msg, deps);
51585
+ break;
51586
+ case "bridge_identified":
51587
+ handleBridgeIdentified(msg, deps);
51588
+ break;
51589
+ case "ha":
51590
+ handleBridgeHeartbeatAck(msg, deps);
51591
+ break;
51592
+ }
51593
+ }
51594
+
51595
+ // src/routing/handlers/preview-environment-control.ts
51596
+ var handlePreviewEnvironmentControl = (msg, deps) => {
51597
+ let wire;
51598
+ try {
51599
+ wire = deps.e2ee ? deps.e2ee.decryptMessage(msg) : msg;
51600
+ } catch (e) {
51601
+ deps.log(`[E2EE] Could not decrypt preview environment command: ${e instanceof Error ? e.message : String(e)}`);
51602
+ return;
51603
+ }
51604
+ const environmentId = typeof wire.environmentId === "string" ? wire.environmentId : "";
51605
+ const action = wire.action === "start" || wire.action === "stop" ? wire.action : null;
51606
+ if (!environmentId || !action) return;
51607
+ deps.previewEnvironmentManager?.handleControl(environmentId, action);
51608
+ };
51609
+
51610
+ // src/routing/handlers/preview-environments-config.ts
51611
+ var VALID_PORT = (p) => typeof p === "number" && Number.isInteger(p) && p > 0 && p < 65536;
51612
+ var handlePreviewEnvironmentsConfig = (msg, deps) => {
51613
+ const previewEnvironments = msg.previewEnvironments;
51614
+ const proxyPorts = Array.isArray(msg.proxyPorts) ? msg.proxyPorts.filter(VALID_PORT) : void 0;
51615
+ setImmediate(() => {
51616
+ deps.previewEnvironmentManager?.applyConfig(previewEnvironments ?? []);
51617
+ const environmentIds = parsePreviewEnvironmentDefs(previewEnvironments ?? []).map((d) => d.environmentId);
51618
+ void deps.previewWorktreeManager?.syncConfiguredEnvironments(environmentIds);
51619
+ if (proxyPorts !== void 0) {
51620
+ deps.updateFirehoseProxyPorts?.(proxyPorts);
51621
+ }
51622
+ });
51623
+ };
51624
+
51625
+ // src/routing/handlers/send-deploy-session-to-preview-result.ts
51626
+ function sendDeploySessionToPreviewResult(ws, id, payload) {
51627
+ if (!ws) return;
51628
+ sendWsMessage(ws, { type: "deploy_session_to_preview_result", id, ...payload });
51629
+ }
51630
+
51631
+ // src/routing/handlers/deploy-session-to-preview.ts
51632
+ var handleDeploySessionToPreviewMessage = (msg, deps) => {
51633
+ if (typeof msg.id !== "string") return;
51634
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
51635
+ const environmentId = typeof msg.environmentId === "string" ? msg.environmentId : "";
51636
+ if (!sessionId || !environmentId) return;
51637
+ void (async () => {
51638
+ const ws = deps.getWs();
51639
+ const reply = (payload) => sendDeploySessionToPreviewResult(ws, msg.id, payload);
51640
+ try {
51641
+ if (!deps.previewWorktreeManager) {
51642
+ reply({ ok: false, error: "Preview worktree manager unavailable" });
51643
+ return;
51644
+ }
51645
+ const result = await deploySessionToPreviewEnvironment({
51646
+ sessionWorktreeManager: deps.sessionWorktreeManager,
51647
+ previewWorktreeManager: deps.previewWorktreeManager,
51648
+ sessionId,
51649
+ environmentId,
51650
+ log: deps.log
51651
+ });
51652
+ reply(result);
51653
+ } catch (e) {
51654
+ reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
51655
+ }
51656
+ })();
51657
+ };
51658
+
51659
+ // src/routing/dispatch/preview.ts
51660
+ function dispatchBridgePreviewMessage(msg, deps) {
51661
+ switch (msg.type) {
51662
+ case "preview_environments_config":
51663
+ handlePreviewEnvironmentsConfig(msg, deps);
51664
+ break;
51665
+ case "preview_environment_control":
51666
+ handlePreviewEnvironmentControl(msg, deps);
51667
+ break;
51668
+ case "deploy_session_to_preview":
51669
+ handleDeploySessionToPreviewMessage(msg, deps);
51670
+ break;
51671
+ }
51672
+ }
51673
+
51567
51674
  // src/agents/acp/from-bridge/handle-bridge-agent-config.ts
51568
51675
  function handleBridgeAgentConfig(msg, { acpManager }) {
51569
51676
  if (!Array.isArray(msg.agents) || msg.agents.length === 0) return;
@@ -52202,367 +52309,505 @@ var handleSessionRequestResponseMessage = (msg, deps) => {
52202
52309
  handleBridgeSessionRequestResponse(msg, deps);
52203
52310
  };
52204
52311
 
52205
- // src/skills/preview.ts
52206
- import { spawn as spawn10 } from "node:child_process";
52207
- var PREVIEW_API_BASE_PATH = "/__preview";
52208
- var PREVIEW_SECRET_HEADER = "X-Preview-Secret";
52209
- var DEFAULT_PORT = 3e3;
52210
- var DEFAULT_COMMAND = "npm run preview";
52211
- var PREVIEW_COMMAND_ENV = "BUILDAUTOMATON_PREVIEW_COMMAND";
52212
- var PREVIEW_PORT_ENV = "BUILDAUTOMATON_PREVIEW_PORT";
52213
- var previewProcess = null;
52214
- var previewPort = DEFAULT_PORT;
52215
- var previewSecret = "";
52216
- function getPreviewCommand() {
52217
- return process.env[PREVIEW_COMMAND_ENV]?.trim() || DEFAULT_COMMAND;
52218
- }
52219
- function randomSecret() {
52220
- const bytes = new Uint8Array(32);
52221
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
52222
- crypto.getRandomValues(bytes);
52312
+ // src/routing/handlers/git/handle-session-git-commit.ts
52313
+ async function handleSessionGitCommitAction(deps, sessionId, msg, reply) {
52314
+ const branch = typeof msg.branch === "string" ? msg.branch : "";
52315
+ const message = typeof msg.message === "string" ? msg.message : "";
52316
+ const pushAfterCommit = msg.pushAfterCommit === true;
52317
+ if (!branch.trim() || !message.trim()) {
52318
+ reply({ ok: false, error: "branch and message are required for commit" });
52319
+ return;
52223
52320
  }
52224
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
52225
- }
52226
- async function requestPreviewApi(port, secret, method, path84, body) {
52227
- const url2 = `http://127.0.0.1:${port}${path84}`;
52228
- const headers = {
52229
- [PREVIEW_SECRET_HEADER]: secret,
52230
- "Content-Type": "application/json"
52231
- };
52232
- const res = await fetch(url2, {
52233
- method,
52234
- headers,
52235
- body: body !== void 0 ? JSON.stringify(body) : void 0
52321
+ const commitRes = await deps.sessionWorktreeManager.commitSession({
52322
+ sessionId,
52323
+ branch: branch.trim(),
52324
+ message: message.trim(),
52325
+ push: pushAfterCommit
52236
52326
  });
52237
- const data = await res.json().catch(() => ({}));
52238
- if (!res.ok) {
52239
- throw new Error(data?.error ?? `Preview API ${method} ${path84}: ${res.status}`);
52327
+ if (!commitRes.ok) {
52328
+ reply({ ok: false, error: commitRes.error ?? "Commit failed" });
52329
+ return;
52240
52330
  }
52241
- return data;
52331
+ const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52332
+ reply({
52333
+ ok: true,
52334
+ hasUncommittedChanges: st.hasUncommittedChanges,
52335
+ hasUnpushedCommits: st.hasUnpushedCommits,
52336
+ uncommittedFileCount: st.uncommittedFileCount
52337
+ });
52242
52338
  }
52243
- var OPERATIONS = [
52244
- {
52245
- id: "start",
52246
- description: "Start the preview server process (runs the configured command with PORT and PREVIEW_SECRET).",
52247
- params: [
52248
- { name: "port", description: "Port for the preview server", type: "number", required: false }
52249
- ]
52250
- },
52251
- {
52252
- id: "stop",
52253
- description: "Stop the preview server via POST /__preview/stop (graceful shutdown).",
52254
- params: []
52255
- },
52256
- {
52257
- id: "status",
52258
- description: "Return status from GET /__preview/status (running, url).",
52259
- params: []
52339
+
52340
+ // src/routing/handlers/git/session-git-changes-params.ts
52341
+ function readString(value) {
52342
+ return typeof value === "string" ? value.trim() : "";
52343
+ }
52344
+ function readChangesObject(msg) {
52345
+ const changes = msg.changes;
52346
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return null;
52347
+ return changes;
52348
+ }
52349
+ function parseSessionGitChangesParams(msg) {
52350
+ const nested = readChangesObject(msg);
52351
+ const file2 = nested?.file && typeof nested.file === "object" && !Array.isArray(nested.file) ? nested.file : null;
52352
+ const repoRelPath = readString(nested?.repoRelPath) || readString(msg.changesRepoRelPath);
52353
+ const viewRaw = nested?.view ?? msg.changesView;
52354
+ const view = viewRaw === "commit" ? "commit" : "working";
52355
+ const commitSha = readString(nested?.commitSha) || readString(msg.changesCommitSha);
52356
+ const recentCommitsLimit = nested?.recentCommitsLimit ?? msg.changesRecentCommitsLimit;
52357
+ return {
52358
+ repoRelPath,
52359
+ view,
52360
+ commitSha,
52361
+ recentCommitsLimit: typeof recentCommitsLimit === "number" || typeof recentCommitsLimit === "string" ? recentCommitsLimit : void 0,
52362
+ fileWorkspaceRelPath: readString(file2?.workspaceRelPath),
52363
+ fileChange: readString(file2?.change),
52364
+ fileMovedFromWorkspaceRelPath: readString(file2?.movedFromWorkspaceRelPath)
52365
+ };
52366
+ }
52367
+
52368
+ // src/routing/handlers/git/handle-session-git-get-change-file-patch.ts
52369
+ async function handleSessionGitGetChangeFilePatchAction(deps, msg, reply) {
52370
+ const changes = parseSessionGitChangesParams(msg);
52371
+ const { repoRelPath: repoRel, view, commitSha, fileWorkspaceRelPath, fileChange, fileMovedFromWorkspaceRelPath } = changes;
52372
+ const change = parseWorkingTreeChangeKind(fileChange);
52373
+ if (!repoRel || !fileWorkspaceRelPath || !change) {
52374
+ reply({
52375
+ ok: false,
52376
+ error: "changes.repoRelPath, changes.file.workspaceRelPath, and changes.file.change are required"
52377
+ });
52378
+ return;
52260
52379
  }
52261
- ];
52262
- var previewSkill = {
52263
- id: "preview",
52264
- name: "Preview",
52265
- description: "Start and manage a local preview server that implements the BuildAutomaton Preview Server API. Configure the command with BUILDAUTOMATON_PREVIEW_COMMAND (default: npm run preview). The server receives PORT and PREVIEW_SECRET and must expose /__preview/status and /__preview/stop.",
52266
- operations: OPERATIONS,
52267
- async execute(operationId, params) {
52268
- const command = getPreviewCommand();
52269
- switch (operationId) {
52270
- case "start": {
52271
- if (previewProcess) {
52272
- return {
52273
- ok: false,
52274
- message: "Preview server already running",
52275
- url: `http://localhost:${previewPort}`
52276
- };
52277
- }
52278
- const port = params.port ?? (Number(process.env[PREVIEW_PORT_ENV]) || DEFAULT_PORT);
52279
- previewPort = port;
52280
- previewSecret = randomSecret();
52281
- const isWindows = process.platform === "win32";
52282
- const parts = command.split(/\s+/);
52283
- const exe = parts[0];
52284
- const args = parts.slice(1);
52285
- previewProcess = spawn10(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
52286
- cwd: getBridgeRoot(),
52287
- stdio: ["ignore", "pipe", "pipe"],
52288
- env: {
52289
- ...process.env,
52290
- PORT: String(port),
52291
- PREVIEW_SECRET: previewSecret
52292
- }
52293
- });
52294
- previewProcess.stdout?.on("data", (d) => process.stdout.write(d));
52295
- previewProcess.stderr?.on("data", (d) => process.stderr.write(d));
52296
- previewProcess.on("exit", (code) => {
52297
- previewProcess = null;
52298
- if (code !== null && code !== 0) {
52299
- process.stderr.write(`Preview server exited with code ${code}
52300
- `);
52301
- }
52302
- });
52303
- return {
52304
- ok: true,
52305
- message: `Preview server starting (${command}, port=${port})`,
52306
- url: `http://localhost:${port}`,
52307
- port
52308
- };
52309
- }
52310
- case "stop": {
52311
- if (!previewProcess) {
52312
- return { ok: true, message: "No preview server was running" };
52313
- }
52314
- if (!previewSecret) {
52315
- previewProcess.kill("SIGTERM");
52316
- previewProcess = null;
52317
- return { ok: true, message: "Preview process stopped (no secret)" };
52318
- }
52319
- try {
52320
- await requestPreviewApi(
52321
- previewPort,
52322
- previewSecret,
52323
- "POST",
52324
- `${PREVIEW_API_BASE_PATH}/stop`
52325
- );
52326
- } catch (e) {
52327
- previewProcess.kill("SIGTERM");
52328
- }
52329
- previewProcess = null;
52330
- previewSecret = "";
52331
- return { ok: true, message: "Preview server stop requested" };
52332
- }
52333
- case "status": {
52334
- if (!previewProcess || !previewSecret) {
52335
- return {
52336
- running: previewProcess != null,
52337
- url: previewProcess ? `http://localhost:${previewPort}` : void 0,
52338
- message: previewProcess ? `Process running at http://localhost:${previewPort}` : "No preview server running"
52339
- };
52340
- }
52341
- try {
52342
- const status = await requestPreviewApi(
52343
- previewPort,
52344
- previewSecret,
52345
- "GET",
52346
- `${PREVIEW_API_BASE_PATH}/status`
52347
- );
52348
- return {
52349
- running: status.status === "running",
52350
- status: status.status,
52351
- url: status.url ?? `http://localhost:${previewPort}`,
52352
- message: status.message ?? (status.status === "running" ? "Preview server running" : "Stopping")
52353
- };
52354
- } catch {
52355
- return {
52356
- running: previewProcess != null,
52357
- url: `http://localhost:${previewPort}`,
52358
- message: "Process running but status endpoint unreachable (server may not implement Preview Server API yet)"
52359
- };
52360
- }
52361
- }
52362
- default:
52363
- throw new Error(`Unknown operation: ${operationId}`);
52364
- }
52380
+ if (view === "commit" && !commitSha) {
52381
+ reply({ ok: false, error: "changes.commitSha is required for commit file patch" });
52382
+ return;
52365
52383
  }
52366
- };
52384
+ const patch = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeFilePatch(msg.sessionId, {
52385
+ repoRelPath: repoRel,
52386
+ workspaceRelPath: fileWorkspaceRelPath,
52387
+ change,
52388
+ movedFromWorkspaceRelPath: fileMovedFromWorkspaceRelPath || null,
52389
+ basis: view === "commit" ? { kind: "commit", sha: commitSha } : { kind: "working" }
52390
+ });
52391
+ reply(
52392
+ {
52393
+ ok: true,
52394
+ patchContent: patch.patchContent ?? null,
52395
+ totalLines: patch.totalLines
52396
+ },
52397
+ ["patchContent"]
52398
+ );
52399
+ }
52367
52400
 
52368
- // src/skills/index.ts
52369
- var skills = [previewSkill];
52370
- function getSkill(id) {
52371
- return skills.find((s) => s.id === id);
52401
+ // src/routing/handlers/git/coalesce-list-changes.ts
52402
+ var listChangesInflight = /* @__PURE__ */ new Map();
52403
+ function normalizeListChangesRepoPathRelativeToWorkspaceRoot(repoRelPath) {
52404
+ if (!repoRelPath?.trim()) return "";
52405
+ return normalizeRepoPathRelativeToWorkspaceRoot(repoRelPath.trim());
52372
52406
  }
52373
- async function callSkill(skillId, operationId, params) {
52374
- const skill = getSkill(skillId);
52375
- if (!skill) throw new Error(`Skill not found: ${skillId}`);
52376
- return skill.execute(operationId, params);
52407
+ function listChangesInflightKey(sessionId, opts) {
52408
+ const basis = opts?.basis;
52409
+ return [
52410
+ sessionId,
52411
+ normalizeListChangesRepoPathRelativeToWorkspaceRoot(opts?.repoRelPath),
52412
+ basis?.kind === "commit" ? `commit:${basis.sha}` : "working",
52413
+ String(opts?.recentCommitsLimit ?? "")
52414
+ ].join("|");
52377
52415
  }
52378
-
52379
- // src/skills/handle-skill-call.ts
52380
- function handleSkillCall(msg, socket, log2) {
52381
- callSkill(msg.skillId, msg.operationId, msg.params ?? {}).then((result) => {
52382
- sendWsMessage(socket, { type: "skill_result", id: msg.id, result });
52383
- }).catch((err) => {
52384
- sendWsMessage(socket, { type: "skill_result", id: msg.id, error: String(err) });
52385
- log2(`[Bridge service] Skill invocation failed (${msg.skillId}/${msg.operationId}): ${err}`);
52416
+ function getSessionWorkingTreeChangeDetailsCoalesced(sessionWorktreeManager, sessionId, opts) {
52417
+ const key = listChangesInflightKey(sessionId, opts);
52418
+ const existing = listChangesInflight.get(key);
52419
+ if (existing) return existing;
52420
+ const promise2 = sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts).finally(() => {
52421
+ if (listChangesInflight.get(key) === promise2) listChangesInflight.delete(key);
52386
52422
  });
52423
+ listChangesInflight.set(key, promise2);
52424
+ return promise2;
52387
52425
  }
52388
52426
 
52389
- // src/routing/handlers/skill-call.ts
52390
- var handleSkillCallMessage = (msg, { getWs, log: log2 }) => {
52391
- const skillId = typeof msg.skillId === "string" ? msg.skillId : "";
52392
- const operationId = typeof msg.operationId === "string" ? msg.operationId : "";
52393
- if (!skillId || !operationId) return;
52394
- const socket = getWs();
52395
- if (!socket) return;
52396
- handleSkillCall(
52397
- { id: msg.id, skillId, operationId, params: msg.params },
52398
- socket,
52399
- log2
52427
+ // src/routing/handlers/git/handle-session-git-list-changes.ts
52428
+ async function handleSessionGitListChangesAction(deps, msg, reply) {
52429
+ const changes = parseSessionGitChangesParams(msg);
52430
+ const { repoRelPath: repoRel, view, commitSha, recentCommitsLimit } = changes;
52431
+ if (view === "commit") {
52432
+ if (!repoRel || !commitSha) {
52433
+ reply({
52434
+ ok: false,
52435
+ error: "changes.repoRelPath and changes.commitSha are required for commit view"
52436
+ });
52437
+ return;
52438
+ }
52439
+ }
52440
+ const opts = repoRel && view === "commit" && commitSha ? {
52441
+ repoRelPath: repoRel,
52442
+ basis: { kind: "commit", sha: commitSha },
52443
+ recentCommitsLimit
52444
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
52445
+ const repos = await getSessionWorkingTreeChangeDetailsCoalesced(
52446
+ deps.sessionWorktreeManager,
52447
+ msg.sessionId,
52448
+ opts
52400
52449
  );
52401
- };
52402
-
52403
- // src/files/browser/send-file-browser-message.ts
52404
- function sendFileBrowserMessage(socket, e2ee, payload) {
52405
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
52450
+ reply({ ok: true, repos }, ["repos"]);
52406
52451
  }
52407
52452
 
52408
- // src/files/browser/handle-file-browser-list.ts
52409
- async function handleFileBrowserList(socket, e2ee, id, reqPath, sessionParentPath, gitScope) {
52410
- const result = await executeFileBrowserList({ reqPath, sessionParentPath, gitScope });
52411
- if ("error" in result) {
52412
- sendWsMessage(socket, { type: "file_browser_response", id, error: result.error });
52453
+ // src/routing/handlers/git/handle-session-git-push.ts
52454
+ async function handleSessionGitPushAction(deps, sessionId, reply) {
52455
+ const pushRes = await deps.sessionWorktreeManager.pushSessionUpstream(sessionId);
52456
+ if (!pushRes.ok) {
52457
+ reply({ ok: false, error: pushRes.error ?? "Push failed" });
52413
52458
  return;
52414
52459
  }
52415
- sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id, entries: result.entries });
52460
+ const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52461
+ reply({
52462
+ ok: true,
52463
+ hasUncommittedChanges: st.hasUncommittedChanges,
52464
+ hasUnpushedCommits: st.hasUnpushedCommits,
52465
+ uncommittedFileCount: st.uncommittedFileCount
52466
+ });
52416
52467
  }
52417
52468
 
52418
- // src/files/browser/handle-file-browser-read.ts
52419
- async function handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope) {
52420
- const result = await executeFileBrowserRead({ msg, reqPath, sessionParentPath, gitScope });
52421
- if ("error" in result) {
52422
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
52423
- return;
52469
+ // src/routing/handlers/git/handle-session-git-status.ts
52470
+ async function handleSessionGitStatusAction(deps, sessionId, reply) {
52471
+ const r = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52472
+ reply({
52473
+ ok: true,
52474
+ hasUncommittedChanges: r.hasUncommittedChanges,
52475
+ hasUnpushedCommits: r.hasUnpushedCommits,
52476
+ uncommittedFileCount: r.uncommittedFileCount
52477
+ });
52478
+ }
52479
+
52480
+ // src/routing/handlers/git/send-session-git-result.ts
52481
+ function sendSessionGitResult(ws, id, payload, e2ee, encryptedFields = []) {
52482
+ if (!ws) return;
52483
+ const message = { type: "session_git_result", id, ...payload };
52484
+ let wire = message;
52485
+ if (e2ee && encryptedFields.length > 0) {
52486
+ wire = e2ee.encryptFields(message, encryptedFields);
52424
52487
  }
52425
- const payload = {
52426
- type: "file_browser_response",
52427
- id: msg.id,
52428
- content: result.content,
52429
- totalLines: result.totalLines,
52430
- size: result.size
52431
- };
52432
- if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
52433
- if (result.mimeType != null) payload.mimeType = result.mimeType;
52434
- if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
52435
- sendFileBrowserMessage(socket, e2ee, payload);
52488
+ sendWsMessage(ws, wire);
52436
52489
  }
52437
52490
 
52438
- // src/files/browser/index.ts
52439
- init_in_flight();
52440
- function handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager) {
52441
- beginFileBrowserRequest();
52491
+ // src/routing/handlers/git/session-git-request.ts
52492
+ var handleSessionGitRequestMessage = (msg, deps) => {
52493
+ if (typeof msg.id !== "string") return;
52494
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52495
+ const action = msg.action;
52496
+ if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes" && action !== "file_diff")
52497
+ return;
52442
52498
  void (async () => {
52499
+ const ws = deps.getWs();
52500
+ const reply = (payload, encryptedFields = []) => sendSessionGitResult(ws, msg.id, payload, deps.e2ee, encryptedFields);
52443
52501
  try {
52444
- const reqPath = msg.path.replace(/^\/+/, "") || ".";
52445
- const sessionParentPath = sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
52446
- const gitScope = resolveGitBranchScope(msg);
52447
- if (msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0 && !gitScope) {
52448
- sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: "Invalid repository path" });
52502
+ if (action === "status") {
52503
+ await handleSessionGitStatusAction(deps, sessionId, reply);
52449
52504
  return;
52450
52505
  }
52451
- if (msg.op === "read") {
52452
- await handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope);
52453
- } else {
52454
- await handleFileBrowserList(socket, e2ee, msg.id, reqPath, sessionParentPath, gitScope);
52506
+ if (action === "list_changes") {
52507
+ await handleSessionGitListChangesAction(deps, { ...msg, sessionId }, reply);
52508
+ return;
52455
52509
  }
52456
- } finally {
52457
- endFileBrowserRequest();
52510
+ if (action === "file_diff") {
52511
+ await handleSessionGitGetChangeFilePatchAction(deps, { ...msg, sessionId }, reply);
52512
+ return;
52513
+ }
52514
+ if (action === "push") {
52515
+ await handleSessionGitPushAction(deps, sessionId, reply);
52516
+ return;
52517
+ }
52518
+ await handleSessionGitCommitAction(deps, sessionId, msg, reply);
52519
+ } catch (e) {
52520
+ reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
52458
52521
  }
52459
52522
  })();
52460
- }
52523
+ };
52461
52524
 
52462
- // src/files/handle-file-browser-search.ts
52463
- init_yield_to_event_loop();
52464
- import path80 from "node:path";
52465
- var SEARCH_LIMIT = 100;
52466
- function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
52467
- void (async () => {
52468
- await yieldToEventLoop();
52469
- const q = typeof msg.q === "string" ? msg.q : "";
52470
- const sessionParentPath = path80.resolve(
52471
- sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
52472
- );
52473
- if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
52474
- triggerFileIndexBuild(sessionParentPath);
52475
- const payload2 = {
52476
- type: "file_browser_search_response",
52477
- id: msg.id,
52478
- paths: [],
52479
- indexReady: false
52480
- };
52481
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
52525
+ // src/routing/handlers/rename-session-branch.ts
52526
+ var handleRenameSessionBranchMessage = (msg, deps) => {
52527
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52528
+ const newBranch = typeof msg.newBranch === "string" ? msg.newBranch : "";
52529
+ if (!sessionId || !newBranch) return;
52530
+ void deps.sessionWorktreeManager.renameSessionBranch(sessionId, newBranch);
52531
+ };
52532
+
52533
+ // src/routing/handlers/session-archived.ts
52534
+ var handleSessionArchivedMessage = (msg, deps) => {
52535
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52536
+ if (!sessionId) return;
52537
+ void deps.sessionWorktreeManager.removeSessionWorktrees(sessionId);
52538
+ };
52539
+
52540
+ // src/routing/handlers/session-discarded.ts
52541
+ var handleSessionDiscardedMessage = (msg, deps) => {
52542
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52543
+ if (!sessionId) return;
52544
+ void deps.sessionWorktreeManager.removeSessionWorktrees(sessionId);
52545
+ };
52546
+
52547
+ // src/routing/handlers/revert-turn-snapshot.ts
52548
+ import * as fs60 from "node:fs";
52549
+ var handleRevertTurnSnapshotMessage = (msg, deps) => {
52550
+ const id = typeof msg.id === "string" ? msg.id : "";
52551
+ const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52552
+ const turnId = typeof msg.turnId === "string" ? msg.turnId : "";
52553
+ if (!id || !sessionId || !turnId) return;
52554
+ const { getWs, log: log2, sessionWorktreeManager } = deps;
52555
+ void (async () => {
52556
+ const s = getWs();
52557
+ if (!s) return;
52558
+ const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
52559
+ const file2 = snapshotFilePath(agentBase, turnId);
52560
+ try {
52561
+ await fs60.promises.access(file2, fs60.constants.F_OK);
52562
+ } catch {
52563
+ sendWsMessage(s, {
52564
+ type: "revert_turn_snapshot_result",
52565
+ id,
52566
+ ok: false,
52567
+ error: "No snapshot found for this turn (git state may be unavailable)."
52568
+ });
52482
52569
  return;
52483
52570
  }
52484
- const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
52485
- const payload = {
52486
- type: "file_browser_search_response",
52487
- id: msg.id,
52488
- paths: results,
52489
- indexReady: true
52490
- };
52491
- sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
52492
- })();
52493
- }
52494
- function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
52495
- setImmediate(() => {
52496
- void ensureFileIndexAsync(sessionParentPath).catch((e) => {
52497
- console.error("[file-index] Background build failed:", e);
52571
+ const res = await applyPreTurnSnapshot(file2, log2);
52572
+ sendWsMessage(s, {
52573
+ type: "revert_turn_snapshot_result",
52574
+ id,
52575
+ ok: res.ok,
52576
+ ...res.error ? { error: res.error } : {}
52498
52577
  });
52499
- });
52578
+ })();
52579
+ };
52580
+
52581
+ // src/routing/dispatch/session.ts
52582
+ function dispatchBridgeSessionMessage(msg, deps) {
52583
+ switch (msg.type) {
52584
+ case "agent_config":
52585
+ handleAgentConfigMessage(msg, deps);
52586
+ break;
52587
+ case "prompt_queue_state":
52588
+ handlePromptQueueStateMessage(msg, deps);
52589
+ break;
52590
+ case "prompt":
52591
+ handlePromptMessage(msg, deps);
52592
+ break;
52593
+ case "session_git_request":
52594
+ handleSessionGitRequestMessage(msg, deps);
52595
+ break;
52596
+ case "rename_session_branch":
52597
+ handleRenameSessionBranchMessage(msg, deps);
52598
+ break;
52599
+ case "session_archived":
52600
+ handleSessionArchivedMessage(msg, deps);
52601
+ break;
52602
+ case "session_discarded":
52603
+ handleSessionDiscardedMessage(msg, deps);
52604
+ break;
52605
+ case "revert_turn_snapshot":
52606
+ handleRevertTurnSnapshotMessage(msg, deps);
52607
+ break;
52608
+ case "cursor_request_response":
52609
+ handleSessionRequestResponseMessage(msg, deps);
52610
+ break;
52611
+ }
52500
52612
  }
52501
52613
 
52502
- // src/routing/handlers/file-browser-messages.ts
52503
- function handleFileBrowserRequestMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
52504
- if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
52505
- const socket = getWs();
52506
- if (!socket) return;
52507
- handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager);
52614
+ // src/skills/preview.ts
52615
+ import { spawn as spawn10 } from "node:child_process";
52616
+ var PREVIEW_API_BASE_PATH = "/__preview";
52617
+ var PREVIEW_SECRET_HEADER = "X-Preview-Secret";
52618
+ var DEFAULT_PORT = 3e3;
52619
+ var DEFAULT_COMMAND = "npm run preview";
52620
+ var PREVIEW_COMMAND_ENV = "BUILDAUTOMATON_PREVIEW_COMMAND";
52621
+ var PREVIEW_PORT_ENV = "BUILDAUTOMATON_PREVIEW_PORT";
52622
+ var previewProcess = null;
52623
+ var previewPort = DEFAULT_PORT;
52624
+ var previewSecret = "";
52625
+ function getPreviewCommand() {
52626
+ return process.env[PREVIEW_COMMAND_ENV]?.trim() || DEFAULT_COMMAND;
52508
52627
  }
52509
- function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
52510
- if (typeof msg.id !== "string") return;
52511
- const socket = getWs();
52512
- if (!socket) return;
52513
- handleFileBrowserSearch(
52514
- msg,
52515
- socket,
52516
- e2ee,
52517
- sessionWorktreeManager
52518
- );
52628
+ function randomSecret() {
52629
+ const bytes = new Uint8Array(32);
52630
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
52631
+ crypto.getRandomValues(bytes);
52632
+ }
52633
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
52519
52634
  }
52520
-
52521
- // src/code-nav/handlers/send-code-nav-response.ts
52522
- var ENCRYPTED_FIELDS = ["definition", "definitions", "references", "symbols", "confidentTarget"];
52523
- function sendCodeNavResponse(socket, e2ee, payload) {
52524
- const fieldsToEncrypt = ENCRYPTED_FIELDS.filter((field) => field in payload && payload[field] !== void 0);
52525
- sendWsMessage(
52526
- socket,
52527
- e2ee && fieldsToEncrypt.length > 0 ? e2ee.encryptFields(payload, [...fieldsToEncrypt]) : payload
52528
- );
52635
+ async function requestPreviewApi(port, secret, method, path84, body) {
52636
+ const url2 = `http://127.0.0.1:${port}${path84}`;
52637
+ const headers = {
52638
+ [PREVIEW_SECRET_HEADER]: secret,
52639
+ "Content-Type": "application/json"
52640
+ };
52641
+ const res = await fetch(url2, {
52642
+ method,
52643
+ headers,
52644
+ body: body !== void 0 ? JSON.stringify(body) : void 0
52645
+ });
52646
+ const data = await res.json().catch(() => ({}));
52647
+ if (!res.ok) {
52648
+ throw new Error(data?.error ?? `Preview API ${method} ${path84}: ${res.status}`);
52649
+ }
52650
+ return data;
52529
52651
  }
52652
+ var OPERATIONS = [
52653
+ {
52654
+ id: "start",
52655
+ description: "Start the preview server process (runs the configured command with PORT and PREVIEW_SECRET).",
52656
+ params: [
52657
+ { name: "port", description: "Port for the preview server", type: "number", required: false }
52658
+ ]
52659
+ },
52660
+ {
52661
+ id: "stop",
52662
+ description: "Stop the preview server via POST /__preview/stop (graceful shutdown).",
52663
+ params: []
52664
+ },
52665
+ {
52666
+ id: "status",
52667
+ description: "Return status from GET /__preview/status (running, url).",
52668
+ params: []
52669
+ }
52670
+ ];
52671
+ var previewSkill = {
52672
+ id: "preview",
52673
+ name: "Preview",
52674
+ description: "Start and manage a local preview server that implements the BuildAutomaton Preview Server API. Configure the command with BUILDAUTOMATON_PREVIEW_COMMAND (default: npm run preview). The server receives PORT and PREVIEW_SECRET and must expose /__preview/status and /__preview/stop.",
52675
+ operations: OPERATIONS,
52676
+ async execute(operationId, params) {
52677
+ const command = getPreviewCommand();
52678
+ switch (operationId) {
52679
+ case "start": {
52680
+ if (previewProcess) {
52681
+ return {
52682
+ ok: false,
52683
+ message: "Preview server already running",
52684
+ url: `http://localhost:${previewPort}`
52685
+ };
52686
+ }
52687
+ const port = params.port ?? (Number(process.env[PREVIEW_PORT_ENV]) || DEFAULT_PORT);
52688
+ previewPort = port;
52689
+ previewSecret = randomSecret();
52690
+ const isWindows = process.platform === "win32";
52691
+ const parts = command.split(/\s+/);
52692
+ const exe = parts[0];
52693
+ const args = parts.slice(1);
52694
+ previewProcess = spawn10(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
52695
+ cwd: getBridgeRoot(),
52696
+ stdio: ["ignore", "pipe", "pipe"],
52697
+ env: {
52698
+ ...process.env,
52699
+ PORT: String(port),
52700
+ PREVIEW_SECRET: previewSecret
52701
+ }
52702
+ });
52703
+ previewProcess.stdout?.on("data", (d) => process.stdout.write(d));
52704
+ previewProcess.stderr?.on("data", (d) => process.stderr.write(d));
52705
+ previewProcess.on("exit", (code) => {
52706
+ previewProcess = null;
52707
+ if (code !== null && code !== 0) {
52708
+ process.stderr.write(`Preview server exited with code ${code}
52709
+ `);
52710
+ }
52711
+ });
52712
+ return {
52713
+ ok: true,
52714
+ message: `Preview server starting (${command}, port=${port})`,
52715
+ url: `http://localhost:${port}`,
52716
+ port
52717
+ };
52718
+ }
52719
+ case "stop": {
52720
+ if (!previewProcess) {
52721
+ return { ok: true, message: "No preview server was running" };
52722
+ }
52723
+ if (!previewSecret) {
52724
+ previewProcess.kill("SIGTERM");
52725
+ previewProcess = null;
52726
+ return { ok: true, message: "Preview process stopped (no secret)" };
52727
+ }
52728
+ try {
52729
+ await requestPreviewApi(
52730
+ previewPort,
52731
+ previewSecret,
52732
+ "POST",
52733
+ `${PREVIEW_API_BASE_PATH}/stop`
52734
+ );
52735
+ } catch (e) {
52736
+ previewProcess.kill("SIGTERM");
52737
+ }
52738
+ previewProcess = null;
52739
+ previewSecret = "";
52740
+ return { ok: true, message: "Preview server stop requested" };
52741
+ }
52742
+ case "status": {
52743
+ if (!previewProcess || !previewSecret) {
52744
+ return {
52745
+ running: previewProcess != null,
52746
+ url: previewProcess ? `http://localhost:${previewPort}` : void 0,
52747
+ message: previewProcess ? `Process running at http://localhost:${previewPort}` : "No preview server running"
52748
+ };
52749
+ }
52750
+ try {
52751
+ const status = await requestPreviewApi(
52752
+ previewPort,
52753
+ previewSecret,
52754
+ "GET",
52755
+ `${PREVIEW_API_BASE_PATH}/status`
52756
+ );
52757
+ return {
52758
+ running: status.status === "running",
52759
+ status: status.status,
52760
+ url: status.url ?? `http://localhost:${previewPort}`,
52761
+ message: status.message ?? (status.status === "running" ? "Preview server running" : "Stopping")
52762
+ };
52763
+ } catch {
52764
+ return {
52765
+ running: previewProcess != null,
52766
+ url: `http://localhost:${previewPort}`,
52767
+ message: "Process running but status endpoint unreachable (server may not implement Preview Server API yet)"
52768
+ };
52769
+ }
52770
+ }
52771
+ default:
52772
+ throw new Error(`Unknown operation: ${operationId}`);
52773
+ }
52774
+ }
52775
+ };
52530
52776
 
52531
- // src/code-nav/handlers/handle-code-nav-request.ts
52532
- function handleCodeNavRequest(msg, socket, e2ee, sessionWorktreeManager) {
52533
- void (async () => {
52534
- const body = await executeCodeNavRequest(msg, sessionWorktreeManager);
52535
- sendCodeNavResponse(socket, e2ee, {
52536
- type: "code_nav_response",
52537
- id: msg.id,
52538
- ...body
52539
- });
52540
- })();
52777
+ // src/skills/index.ts
52778
+ var skills = [previewSkill];
52779
+ function getSkill(id) {
52780
+ return skills.find((s) => s.id === id);
52781
+ }
52782
+ async function callSkill(skillId, operationId, params) {
52783
+ const skill = getSkill(skillId);
52784
+ if (!skill) throw new Error(`Skill not found: ${skillId}`);
52785
+ return skill.execute(operationId, params);
52541
52786
  }
52542
52787
 
52543
- // src/code-nav/handlers/trigger-symbol-index-build.ts
52544
- init_normalize_resolved_path();
52788
+ // src/skills/handle-skill-call.ts
52789
+ function handleSkillCall(msg, socket, log2) {
52790
+ callSkill(msg.skillId, msg.operationId, msg.params ?? {}).then((result) => {
52791
+ sendWsMessage(socket, { type: "skill_result", id: msg.id, result });
52792
+ }).catch((err) => {
52793
+ sendWsMessage(socket, { type: "skill_result", id: msg.id, error: String(err) });
52794
+ log2(`[Bridge service] Skill invocation failed (${msg.skillId}/${msg.operationId}): ${err}`);
52795
+ });
52796
+ }
52545
52797
 
52546
- // src/routing/handlers/code-nav-messages.ts
52547
- function handleFileBrowserCodeNavMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
52548
- if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
52798
+ // src/routing/handlers/skill-call.ts
52799
+ var handleSkillCallMessage = (msg, { getWs, log: log2 }) => {
52800
+ const skillId = typeof msg.skillId === "string" ? msg.skillId : "";
52801
+ const operationId = typeof msg.operationId === "string" ? msg.operationId : "";
52802
+ if (!skillId || !operationId) return;
52549
52803
  const socket = getWs();
52550
52804
  if (!socket) return;
52551
- handleCodeNavRequest(
52552
- {
52553
- id: msg.id,
52554
- type: "code_nav",
52555
- op: msg.op === "references" ? "references" : msg.op === "symbols" ? "symbols" : msg.op === "definitions" ? "definitions" : "definition",
52556
- path: msg.path,
52557
- line: typeof msg.line === "number" ? msg.line : 1,
52558
- column: typeof msg.column === "number" ? msg.column : 0,
52559
- sessionId: msg.sessionId
52560
- },
52805
+ handleSkillCall(
52806
+ { id: msg.id, skillId, operationId, params: msg.params },
52561
52807
  socket,
52562
- e2ee,
52563
- sessionWorktreeManager
52808
+ log2
52564
52809
  );
52565
- }
52810
+ };
52566
52811
 
52567
52812
  // src/routing/handlers/skill-layout-request.ts
52568
52813
  function handleSkillLayoutRequest(msg, deps) {
@@ -52585,413 +52830,409 @@ function isValidRemoteSkillInstallItem(item) {
52585
52830
 
52586
52831
  // src/skills/install/install-remote-skills-async.ts
52587
52832
  init_yield_to_event_loop();
52588
- import fs60 from "node:fs";
52589
- import path81 from "node:path";
52833
+ import fs61 from "node:fs";
52834
+ import path80 from "node:path";
52590
52835
 
52591
52836
  // src/skills/install/constants.ts
52592
52837
  var INSTALL_SKILLS_YIELD_EVERY = 16;
52593
52838
 
52594
52839
  // src/skills/install/install-remote-skills-async.ts
52595
52840
  async function writeInstallFileAsync(skillDir, f, filesWritten) {
52596
- if (typeof f.path !== "string" || !f.text && !f.base64) return;
52597
- if (++filesWritten.count % INSTALL_SKILLS_YIELD_EVERY === 0) {
52598
- await yieldToEventLoop();
52599
- }
52600
- const dest = path81.join(skillDir, f.path);
52601
- await fs60.promises.mkdir(path81.dirname(dest), { recursive: true });
52602
- if (f.text !== void 0) {
52603
- await fs60.promises.writeFile(dest, f.text, "utf8");
52604
- } else if (f.base64) {
52605
- await fs60.promises.writeFile(dest, Buffer.from(f.base64, "base64"));
52606
- }
52607
- }
52608
- async function installRemoteSkillsAsync(cwd, targetDir, items) {
52609
- const installed2 = [];
52610
- if (!Array.isArray(items)) {
52611
- return { success: false, error: "Invalid items" };
52612
- }
52613
- const filesWritten = { count: 0 };
52614
- try {
52615
- for (const item of items) {
52616
- if (!isValidRemoteSkillInstallItem(item)) continue;
52617
- const skillDir = path81.join(cwd, targetDir, item.skillName);
52618
- for (const f of item.files) {
52619
- await writeInstallFileAsync(skillDir, f, filesWritten);
52620
- }
52621
- installed2.push({
52622
- sourceId: item.sourceId,
52623
- skillName: item.skillName,
52624
- versionHash: item.versionHash
52625
- });
52626
- }
52627
- return { success: true, installed: installed2 };
52628
- } catch (e) {
52629
- return { success: false, error: e instanceof Error ? e.message : String(e) };
52630
- }
52631
- }
52632
-
52633
- // src/routing/handlers/install-skills.ts
52634
- var handleInstallSkillsMessage = (msg, deps) => {
52635
- const id = typeof msg.id === "string" ? msg.id : "";
52636
- const targetDir = typeof msg.targetDir === "string" && msg.targetDir.trim() ? msg.targetDir.trim() : ".agents/skills";
52637
- const rawItems = msg.items;
52638
- const cwd = getBridgeRoot();
52639
- void (async () => {
52640
- const socket = deps.getWs();
52641
- const result = await installRemoteSkillsAsync(cwd, targetDir, rawItems);
52642
- if (!result.success) {
52643
- const err = result.error ?? "Invalid items";
52644
- deps.log(`[Bridge service] Install skills failed: ${err}`);
52645
- if (socket) {
52646
- sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
52647
- }
52648
- return;
52649
- }
52650
- if (socket) {
52651
- sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
52652
- }
52653
- })();
52654
- };
52655
-
52656
- // src/routing/handlers/refresh-local-skills.ts
52657
- var handleRefreshLocalSkills = (_msg, deps) => {
52658
- setImmediate(() => {
52659
- deps.sendLocalSkillsReport?.();
52660
- });
52661
- };
52662
-
52663
- // src/routing/handlers/git/handle-session-git-commit.ts
52664
- async function handleSessionGitCommitAction(deps, sessionId, msg, reply) {
52665
- const branch = typeof msg.branch === "string" ? msg.branch : "";
52666
- const message = typeof msg.message === "string" ? msg.message : "";
52667
- const pushAfterCommit = msg.pushAfterCommit === true;
52668
- if (!branch.trim() || !message.trim()) {
52669
- reply({ ok: false, error: "branch and message are required for commit" });
52670
- return;
52671
- }
52672
- const commitRes = await deps.sessionWorktreeManager.commitSession({
52673
- sessionId,
52674
- branch: branch.trim(),
52675
- message: message.trim(),
52676
- push: pushAfterCommit
52677
- });
52678
- if (!commitRes.ok) {
52679
- reply({ ok: false, error: commitRes.error ?? "Commit failed" });
52680
- return;
52681
- }
52682
- const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52683
- reply({
52684
- ok: true,
52685
- hasUncommittedChanges: st.hasUncommittedChanges,
52686
- hasUnpushedCommits: st.hasUnpushedCommits,
52687
- uncommittedFileCount: st.uncommittedFileCount
52688
- });
52689
- }
52690
-
52691
- // src/routing/handlers/git/session-git-changes-params.ts
52692
- function readString(value) {
52693
- return typeof value === "string" ? value.trim() : "";
52694
- }
52695
- function readChangesObject(msg) {
52696
- const changes = msg.changes;
52697
- if (!changes || typeof changes !== "object" || Array.isArray(changes)) return null;
52698
- return changes;
52699
- }
52700
- function parseSessionGitChangesParams(msg) {
52701
- const nested = readChangesObject(msg);
52702
- const file2 = nested?.file && typeof nested.file === "object" && !Array.isArray(nested.file) ? nested.file : null;
52703
- const repoRelPath = readString(nested?.repoRelPath) || readString(msg.changesRepoRelPath);
52704
- const viewRaw = nested?.view ?? msg.changesView;
52705
- const view = viewRaw === "commit" ? "commit" : "working";
52706
- const commitSha = readString(nested?.commitSha) || readString(msg.changesCommitSha);
52707
- const recentCommitsLimit = nested?.recentCommitsLimit ?? msg.changesRecentCommitsLimit;
52708
- return {
52709
- repoRelPath,
52710
- view,
52711
- commitSha,
52712
- recentCommitsLimit: typeof recentCommitsLimit === "number" || typeof recentCommitsLimit === "string" ? recentCommitsLimit : void 0,
52713
- fileWorkspaceRelPath: readString(file2?.workspaceRelPath),
52714
- fileChange: readString(file2?.change),
52715
- fileMovedFromWorkspaceRelPath: readString(file2?.movedFromWorkspaceRelPath)
52716
- };
52717
- }
52718
-
52719
- // src/routing/handlers/git/handle-session-git-get-change-file-patch.ts
52720
- async function handleSessionGitGetChangeFilePatchAction(deps, msg, reply) {
52721
- const changes = parseSessionGitChangesParams(msg);
52722
- const { repoRelPath: repoRel, view, commitSha, fileWorkspaceRelPath, fileChange, fileMovedFromWorkspaceRelPath } = changes;
52723
- const change = parseWorkingTreeChangeKind(fileChange);
52724
- if (!repoRel || !fileWorkspaceRelPath || !change) {
52725
- reply({
52726
- ok: false,
52727
- error: "changes.repoRelPath, changes.file.workspaceRelPath, and changes.file.change are required"
52728
- });
52729
- return;
52730
- }
52731
- if (view === "commit" && !commitSha) {
52732
- reply({ ok: false, error: "changes.commitSha is required for commit file patch" });
52733
- return;
52734
- }
52735
- const patch = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeFilePatch(msg.sessionId, {
52736
- repoRelPath: repoRel,
52737
- workspaceRelPath: fileWorkspaceRelPath,
52738
- change,
52739
- movedFromWorkspaceRelPath: fileMovedFromWorkspaceRelPath || null,
52740
- basis: view === "commit" ? { kind: "commit", sha: commitSha } : { kind: "working" }
52741
- });
52742
- reply(
52743
- {
52744
- ok: true,
52745
- patchContent: patch.patchContent ?? null,
52746
- totalLines: patch.totalLines
52747
- },
52748
- ["patchContent"]
52749
- );
52750
- }
52751
-
52752
- // src/routing/handlers/git/coalesce-list-changes.ts
52753
- var listChangesInflight = /* @__PURE__ */ new Map();
52754
- function normalizeListChangesRepoPathRelativeToWorkspaceRoot(repoRelPath) {
52755
- if (!repoRelPath?.trim()) return "";
52756
- return normalizeRepoPathRelativeToWorkspaceRoot(repoRelPath.trim());
52757
- }
52758
- function listChangesInflightKey(sessionId, opts) {
52759
- const basis = opts?.basis;
52760
- return [
52761
- sessionId,
52762
- normalizeListChangesRepoPathRelativeToWorkspaceRoot(opts?.repoRelPath),
52763
- basis?.kind === "commit" ? `commit:${basis.sha}` : "working",
52764
- String(opts?.recentCommitsLimit ?? "")
52765
- ].join("|");
52841
+ if (typeof f.path !== "string" || !f.text && !f.base64) return;
52842
+ if (++filesWritten.count % INSTALL_SKILLS_YIELD_EVERY === 0) {
52843
+ await yieldToEventLoop();
52844
+ }
52845
+ const dest = path80.join(skillDir, f.path);
52846
+ await fs61.promises.mkdir(path80.dirname(dest), { recursive: true });
52847
+ if (f.text !== void 0) {
52848
+ await fs61.promises.writeFile(dest, f.text, "utf8");
52849
+ } else if (f.base64) {
52850
+ await fs61.promises.writeFile(dest, Buffer.from(f.base64, "base64"));
52851
+ }
52766
52852
  }
52767
- function getSessionWorkingTreeChangeDetailsCoalesced(sessionWorktreeManager, sessionId, opts) {
52768
- const key = listChangesInflightKey(sessionId, opts);
52769
- const existing = listChangesInflight.get(key);
52770
- if (existing) return existing;
52771
- const promise2 = sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts).finally(() => {
52772
- if (listChangesInflight.get(key) === promise2) listChangesInflight.delete(key);
52773
- });
52774
- listChangesInflight.set(key, promise2);
52775
- return promise2;
52853
+ async function installRemoteSkillsAsync(cwd, targetDir, items) {
52854
+ const installed2 = [];
52855
+ if (!Array.isArray(items)) {
52856
+ return { success: false, error: "Invalid items" };
52857
+ }
52858
+ const filesWritten = { count: 0 };
52859
+ try {
52860
+ for (const item of items) {
52861
+ if (!isValidRemoteSkillInstallItem(item)) continue;
52862
+ const skillDir = path80.join(cwd, targetDir, item.skillName);
52863
+ for (const f of item.files) {
52864
+ await writeInstallFileAsync(skillDir, f, filesWritten);
52865
+ }
52866
+ installed2.push({
52867
+ sourceId: item.sourceId,
52868
+ skillName: item.skillName,
52869
+ versionHash: item.versionHash
52870
+ });
52871
+ }
52872
+ return { success: true, installed: installed2 };
52873
+ } catch (e) {
52874
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
52875
+ }
52776
52876
  }
52777
52877
 
52778
- // src/routing/handlers/git/handle-session-git-list-changes.ts
52779
- async function handleSessionGitListChangesAction(deps, msg, reply) {
52780
- const changes = parseSessionGitChangesParams(msg);
52781
- const { repoRelPath: repoRel, view, commitSha, recentCommitsLimit } = changes;
52782
- if (view === "commit") {
52783
- if (!repoRel || !commitSha) {
52784
- reply({
52785
- ok: false,
52786
- error: "changes.repoRelPath and changes.commitSha are required for commit view"
52787
- });
52878
+ // src/routing/handlers/install-skills.ts
52879
+ var handleInstallSkillsMessage = (msg, deps) => {
52880
+ const id = typeof msg.id === "string" ? msg.id : "";
52881
+ const targetDir = typeof msg.targetDir === "string" && msg.targetDir.trim() ? msg.targetDir.trim() : ".agents/skills";
52882
+ const rawItems = msg.items;
52883
+ const cwd = getBridgeRoot();
52884
+ void (async () => {
52885
+ const socket = deps.getWs();
52886
+ const result = await installRemoteSkillsAsync(cwd, targetDir, rawItems);
52887
+ if (!result.success) {
52888
+ const err = result.error ?? "Invalid items";
52889
+ deps.log(`[Bridge service] Install skills failed: ${err}`);
52890
+ if (socket) {
52891
+ sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
52892
+ }
52788
52893
  return;
52789
52894
  }
52790
- }
52791
- const opts = repoRel && view === "commit" && commitSha ? {
52792
- repoRelPath: repoRel,
52793
- basis: { kind: "commit", sha: commitSha },
52794
- recentCommitsLimit
52795
- } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
52796
- const repos = await getSessionWorkingTreeChangeDetailsCoalesced(
52797
- deps.sessionWorktreeManager,
52798
- msg.sessionId,
52799
- opts
52800
- );
52801
- reply({ ok: true, repos }, ["repos"]);
52895
+ if (socket) {
52896
+ sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
52897
+ }
52898
+ })();
52899
+ };
52900
+
52901
+ // src/agents/install/commands/run-npm-global-install.ts
52902
+ import { execFile as execFile9 } from "node:child_process";
52903
+ import { promisify as promisify10 } from "node:util";
52904
+ var execFileAsync8 = promisify10(execFile9);
52905
+ async function runNpmGlobalInstall(packageName, env, timeoutMs = 3e5) {
52906
+ await execFileAsync8("npm", ["install", "-g", packageName], { timeout: timeoutMs, env });
52802
52907
  }
52803
52908
 
52804
- // src/routing/handlers/git/handle-session-git-push.ts
52805
- async function handleSessionGitPushAction(deps, sessionId, reply) {
52806
- const pushRes = await deps.sessionWorktreeManager.pushSessionUpstream(sessionId);
52807
- if (!pushRes.ok) {
52808
- reply({ ok: false, error: pushRes.error ?? "Push failed" });
52809
- return;
52909
+ // src/agents/install/commands/claude-code.ts
52910
+ var claudeCodeInstallCommand = {
52911
+ agentType: "claude-code",
52912
+ detectCommand: "claude",
52913
+ async install(ctx) {
52914
+ ctx.onProgress?.("Installing Anthropic Claude Code");
52915
+ await runNpmGlobalInstall("@anthropic-ai/claude-code", {
52916
+ ...ctx.env,
52917
+ ANTHROPIC_API_KEY: ctx.authToken
52918
+ });
52810
52919
  }
52811
- const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52812
- reply({
52813
- ok: true,
52814
- hasUncommittedChanges: st.hasUncommittedChanges,
52815
- hasUnpushedCommits: st.hasUnpushedCommits,
52816
- uncommittedFileCount: st.uncommittedFileCount
52817
- });
52818
- }
52920
+ };
52819
52921
 
52820
- // src/routing/handlers/git/handle-session-git-status.ts
52821
- async function handleSessionGitStatusAction(deps, sessionId, reply) {
52822
- const r = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
52823
- reply({
52824
- ok: true,
52825
- hasUncommittedChanges: r.hasUncommittedChanges,
52826
- hasUnpushedCommits: r.hasUnpushedCommits,
52827
- uncommittedFileCount: r.uncommittedFileCount
52828
- });
52829
- }
52922
+ // src/agents/install/commands/codex-acp.ts
52923
+ var codexAcpInstallCommand = {
52924
+ agentType: "codex-acp",
52925
+ detectCommand: "codex",
52926
+ async install(ctx) {
52927
+ ctx.onProgress?.("Installing Codex");
52928
+ await runNpmGlobalInstall("@openai/codex", {
52929
+ ...ctx.env,
52930
+ OPENAI_API_KEY: ctx.authToken
52931
+ });
52932
+ }
52933
+ };
52830
52934
 
52831
- // src/routing/handlers/git/send-session-git-result.ts
52832
- function sendSessionGitResult(ws, id, payload, e2ee, encryptedFields = []) {
52833
- if (!ws) return;
52834
- const message = { type: "session_git_result", id, ...payload };
52835
- let wire = message;
52836
- if (e2ee && encryptedFields.length > 0) {
52837
- wire = e2ee.encryptFields(message, encryptedFields);
52935
+ // src/agents/install/commands/cursor-cli.ts
52936
+ import { execFile as execFile10 } from "node:child_process";
52937
+ import { promisify as promisify11 } from "node:util";
52938
+ var execFileAsync9 = promisify11(execFile10);
52939
+ var cursorCliInstallCommand = {
52940
+ agentType: "cursor-cli",
52941
+ detectCommand: "agent",
52942
+ async install(ctx) {
52943
+ ctx.onProgress?.("Installing Cursor CLI");
52944
+ await execFileAsync9("bash", ["-lc", "curl -fsSL https://cursor.com/install | bash"], {
52945
+ timeout: 3e5,
52946
+ env: { ...ctx.env, CURSOR_API_KEY: ctx.authToken }
52947
+ });
52838
52948
  }
52839
- sendWsMessage(ws, wire);
52949
+ };
52950
+
52951
+ // src/agents/install/commands/opencode.ts
52952
+ var opencodeInstallCommand = {
52953
+ agentType: "opencode",
52954
+ detectCommand: "opencode",
52955
+ async install(ctx) {
52956
+ ctx.onProgress?.("Installing OpenCode");
52957
+ await runNpmGlobalInstall("opencode-ai", {
52958
+ ...ctx.env,
52959
+ OPENCODE_API_KEY: ctx.authToken
52960
+ });
52961
+ }
52962
+ };
52963
+
52964
+ // src/agents/install/commands/index.ts
52965
+ var COMMANDS = [
52966
+ claudeCodeInstallCommand,
52967
+ codexAcpInstallCommand,
52968
+ cursorCliInstallCommand,
52969
+ opencodeInstallCommand
52970
+ ];
52971
+ var byType = new Map(COMMANDS.map((c) => [c.agentType, c]));
52972
+ function getAgentInstallCommand(agentType) {
52973
+ return byType.get(agentType);
52974
+ }
52975
+
52976
+ // src/agents/install/install-local-agent.ts
52977
+ async function installLocalAgentOnBridge(params) {
52978
+ const spec = INSTALLABLE_BRIDGE_AGENTS.find((a) => a.value === params.agentType);
52979
+ if (!spec) return { success: false, error: `Unsupported agent type: ${params.agentType}` };
52980
+ const command = getAgentInstallCommand(params.agentType);
52981
+ if (!command) return { success: false, error: `No install command for ${params.agentType}` };
52982
+ params.onProgress?.(`Configuring ${spec.label} credentials`);
52983
+ try {
52984
+ await command.install({
52985
+ authToken: params.authToken,
52986
+ onProgress: params.onProgress,
52987
+ env: { ...process.env, [spec.tokenEnvVar]: params.authToken }
52988
+ });
52989
+ } catch (e) {
52990
+ const msg = e instanceof Error ? e.message : String(e);
52991
+ return { success: false, error: msg };
52992
+ }
52993
+ const found = await isCommandOnPath(command.detectCommand);
52994
+ if (!found) {
52995
+ return { success: false, error: `${command.detectCommand} not found on PATH after install` };
52996
+ }
52997
+ return { success: true };
52840
52998
  }
52841
52999
 
52842
- // src/routing/handlers/git/session-git-request.ts
52843
- var handleSessionGitRequestMessage = (msg, deps) => {
52844
- if (typeof msg.id !== "string") return;
52845
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52846
- const action = msg.action;
52847
- if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes" && action !== "file_diff")
52848
- return;
53000
+ // src/routing/handlers/install-agent.ts
53001
+ var handleInstallAgentMessage = (msg, deps) => {
53002
+ const processId = typeof msg.processId === "string" ? msg.processId : "";
53003
+ const agentId = typeof msg.agentId === "string" ? msg.agentId : "";
53004
+ const tokenId = typeof msg.tokenId === "string" ? msg.tokenId : "";
53005
+ const agentType = typeof msg.agentType === "string" ? msg.agentType : "";
53006
+ const authToken = typeof msg.authToken === "string" ? msg.authToken : "";
53007
+ if (!processId || !agentId || !tokenId || !agentType || !authToken) return;
52849
53008
  void (async () => {
52850
- const ws = deps.getWs();
52851
- const reply = (payload, encryptedFields = []) => sendSessionGitResult(ws, msg.id, payload, deps.e2ee, encryptedFields);
52852
- try {
52853
- if (action === "status") {
52854
- await handleSessionGitStatusAction(deps, sessionId, reply);
52855
- return;
52856
- }
52857
- if (action === "list_changes") {
52858
- await handleSessionGitListChangesAction(deps, { ...msg, sessionId }, reply);
52859
- return;
52860
- }
52861
- if (action === "file_diff") {
52862
- await handleSessionGitGetChangeFilePatchAction(deps, { ...msg, sessionId }, reply);
52863
- return;
52864
- }
52865
- if (action === "push") {
52866
- await handleSessionGitPushAction(deps, sessionId, reply);
52867
- return;
53009
+ const socket = deps.getWs();
53010
+ const report = (step, message) => {
53011
+ if (socket) {
53012
+ sendWsMessage(socket, {
53013
+ type: "install_agent_progress",
53014
+ processId,
53015
+ agentId,
53016
+ tokenId,
53017
+ agentType,
53018
+ step,
53019
+ message
53020
+ });
52868
53021
  }
52869
- await handleSessionGitCommitAction(deps, sessionId, msg, reply);
52870
- } catch (e) {
52871
- reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
53022
+ };
53023
+ const result = await installLocalAgentOnBridge({
53024
+ agentType,
53025
+ authToken,
53026
+ onProgress: (message) => report("agent_install_package", message)
53027
+ });
53028
+ if (socket) {
53029
+ sendWsMessage(socket, {
53030
+ type: "install_agent_result",
53031
+ processId,
53032
+ agentId,
53033
+ tokenId,
53034
+ agentType,
53035
+ success: result.success,
53036
+ error: result.error
53037
+ });
53038
+ }
53039
+ if (!result.success) {
53040
+ deps.log(`[Bridge service] Install agent failed: ${result.error ?? "unknown"}`);
52872
53041
  }
52873
53042
  })();
52874
53043
  };
52875
53044
 
52876
- // src/routing/handlers/rename-session-branch.ts
52877
- var handleRenameSessionBranchMessage = (msg, deps) => {
52878
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52879
- const newBranch = typeof msg.newBranch === "string" ? msg.newBranch : "";
52880
- if (!sessionId || !newBranch) return;
52881
- void deps.sessionWorktreeManager.renameSessionBranch(sessionId, newBranch);
53045
+ // src/routing/handlers/refresh-local-skills.ts
53046
+ var handleRefreshLocalSkills = (_msg, deps) => {
53047
+ setImmediate(() => {
53048
+ deps.sendLocalSkillsReport?.();
53049
+ });
52882
53050
  };
52883
53051
 
52884
- // src/routing/handlers/session-archived.ts
52885
- var handleSessionArchivedMessage = (msg, deps) => {
52886
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52887
- if (!sessionId) return;
52888
- void deps.sessionWorktreeManager.removeSessionWorktrees(sessionId);
52889
- };
53052
+ // src/routing/dispatch/skills.ts
53053
+ function dispatchBridgeSkillsMessage(msg, deps) {
53054
+ switch (msg.type) {
53055
+ case "skill_call":
53056
+ handleSkillCallMessage(msg, deps);
53057
+ break;
53058
+ case "skill_layout_request":
53059
+ handleSkillLayoutRequest(msg, deps);
53060
+ break;
53061
+ case "install_skills":
53062
+ handleInstallSkillsMessage(msg, deps);
53063
+ break;
53064
+ case "install_agent":
53065
+ handleInstallAgentMessage(msg, deps);
53066
+ break;
53067
+ case "refresh_local_skills":
53068
+ handleRefreshLocalSkills(msg, deps);
53069
+ break;
53070
+ }
53071
+ }
52890
53072
 
52891
- // src/routing/handlers/session-discarded.ts
52892
- var handleSessionDiscardedMessage = (msg, deps) => {
52893
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52894
- if (!sessionId) return;
52895
- void deps.sessionWorktreeManager.removeSessionWorktrees(sessionId);
52896
- };
53073
+ // src/files/browser/send-file-browser-message.ts
53074
+ function sendFileBrowserMessage(socket, e2ee, payload) {
53075
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["entries", "content", "totalLines", "size", "lineOffset"]) : payload);
53076
+ }
52897
53077
 
52898
- // src/routing/handlers/revert-turn-snapshot.ts
52899
- import * as fs61 from "node:fs";
52900
- var handleRevertTurnSnapshotMessage = (msg, deps) => {
52901
- const id = typeof msg.id === "string" ? msg.id : "";
52902
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52903
- const turnId = typeof msg.turnId === "string" ? msg.turnId : "";
52904
- if (!id || !sessionId || !turnId) return;
52905
- const { getWs, log: log2, sessionWorktreeManager } = deps;
53078
+ // src/files/browser/handle-file-browser-list.ts
53079
+ async function handleFileBrowserList(socket, e2ee, id, reqPath, sessionParentPath, gitScope) {
53080
+ const result = await executeFileBrowserList({ reqPath, sessionParentPath, gitScope });
53081
+ if ("error" in result) {
53082
+ sendWsMessage(socket, { type: "file_browser_response", id, error: result.error });
53083
+ return;
53084
+ }
53085
+ sendFileBrowserMessage(socket, e2ee, { type: "file_browser_response", id, entries: result.entries });
53086
+ }
53087
+
53088
+ // src/files/browser/handle-file-browser-read.ts
53089
+ async function handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope) {
53090
+ const result = await executeFileBrowserRead({ msg, reqPath, sessionParentPath, gitScope });
53091
+ if ("error" in result) {
53092
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: result.error });
53093
+ return;
53094
+ }
53095
+ const payload = {
53096
+ type: "file_browser_response",
53097
+ id: msg.id,
53098
+ content: result.content,
53099
+ totalLines: result.totalLines,
53100
+ size: result.size
53101
+ };
53102
+ if (result.lineOffset != null) payload.lineOffset = result.lineOffset;
53103
+ if (result.mimeType != null) payload.mimeType = result.mimeType;
53104
+ if (result.resolvedPath != null) payload.resolvedPath = result.resolvedPath;
53105
+ sendFileBrowserMessage(socket, e2ee, payload);
53106
+ }
53107
+
53108
+ // src/files/browser/index.ts
53109
+ init_in_flight();
53110
+ function handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager) {
53111
+ beginFileBrowserRequest();
52906
53112
  void (async () => {
52907
- const s = getWs();
52908
- if (!s) return;
52909
- const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
52910
- const file2 = snapshotFilePath(agentBase, turnId);
52911
53113
  try {
52912
- await fs61.promises.access(file2, fs61.constants.F_OK);
52913
- } catch {
52914
- sendWsMessage(s, {
52915
- type: "revert_turn_snapshot_result",
52916
- id,
52917
- ok: false,
52918
- error: "No snapshot found for this turn (git state may be unavailable)."
52919
- });
52920
- return;
53114
+ const reqPath = msg.path.replace(/^\/+/, "") || ".";
53115
+ const sessionParentPath = sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
53116
+ const gitScope = resolveGitBranchScope(msg);
53117
+ if (msg.source === "git_branch" && typeof msg.repoRelPath === "string" && typeof msg.branch === "string" && msg.branch.trim().length > 0 && !gitScope) {
53118
+ sendWsMessage(socket, { type: "file_browser_response", id: msg.id, error: "Invalid repository path" });
53119
+ return;
53120
+ }
53121
+ if (msg.op === "read") {
53122
+ await handleFileBrowserRead(socket, e2ee, msg, reqPath, sessionParentPath, gitScope);
53123
+ } else {
53124
+ await handleFileBrowserList(socket, e2ee, msg.id, reqPath, sessionParentPath, gitScope);
53125
+ }
53126
+ } finally {
53127
+ endFileBrowserRequest();
52921
53128
  }
52922
- const res = await applyPreTurnSnapshot(file2, log2);
52923
- sendWsMessage(s, {
52924
- type: "revert_turn_snapshot_result",
52925
- id,
52926
- ok: res.ok,
52927
- ...res.error ? { error: res.error } : {}
52928
- });
52929
53129
  })();
52930
- };
52931
-
52932
- // src/routing/handlers/preview-environment-control.ts
52933
- var handlePreviewEnvironmentControl = (msg, deps) => {
52934
- let wire;
52935
- try {
52936
- wire = deps.e2ee ? deps.e2ee.decryptMessage(msg) : msg;
52937
- } catch (e) {
52938
- deps.log(`[E2EE] Could not decrypt preview environment command: ${e instanceof Error ? e.message : String(e)}`);
52939
- return;
52940
- }
52941
- const environmentId = typeof wire.environmentId === "string" ? wire.environmentId : "";
52942
- const action = wire.action === "start" || wire.action === "stop" ? wire.action : null;
52943
- if (!environmentId || !action) return;
52944
- deps.previewEnvironmentManager?.handleControl(environmentId, action);
52945
- };
53130
+ }
52946
53131
 
52947
- // src/routing/handlers/preview-environments-config.ts
52948
- var VALID_PORT = (p) => typeof p === "number" && Number.isInteger(p) && p > 0 && p < 65536;
52949
- var handlePreviewEnvironmentsConfig = (msg, deps) => {
52950
- const previewEnvironments = msg.previewEnvironments;
52951
- const proxyPorts = Array.isArray(msg.proxyPorts) ? msg.proxyPorts.filter(VALID_PORT) : void 0;
52952
- setImmediate(() => {
52953
- deps.previewEnvironmentManager?.applyConfig(previewEnvironments ?? []);
52954
- const environmentIds = parsePreviewEnvironmentDefs(previewEnvironments ?? []).map((d) => d.environmentId);
52955
- void deps.previewWorktreeManager?.syncConfiguredEnvironments(environmentIds);
52956
- if (proxyPorts !== void 0) {
52957
- deps.updateFirehoseProxyPorts?.(proxyPorts);
53132
+ // src/files/handle-file-browser-search.ts
53133
+ init_yield_to_event_loop();
53134
+ import path81 from "node:path";
53135
+ var SEARCH_LIMIT = 100;
53136
+ function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
53137
+ void (async () => {
53138
+ await yieldToEventLoop();
53139
+ const q = typeof msg.q === "string" ? msg.q : "";
53140
+ const sessionParentPath = path81.resolve(
53141
+ sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
53142
+ );
53143
+ if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
53144
+ triggerFileIndexBuild(sessionParentPath);
53145
+ const payload2 = {
53146
+ type: "file_browser_search_response",
53147
+ id: msg.id,
53148
+ paths: [],
53149
+ indexReady: false
53150
+ };
53151
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload2, ["paths"]) : payload2);
53152
+ return;
52958
53153
  }
53154
+ const results = await searchBridgeFilePathsAsync(sessionParentPath, q, SEARCH_LIMIT);
53155
+ const payload = {
53156
+ type: "file_browser_search_response",
53157
+ id: msg.id,
53158
+ paths: results,
53159
+ indexReady: true
53160
+ };
53161
+ sendWsMessage(socket, e2ee ? e2ee.encryptFields(payload, ["paths"]) : payload);
53162
+ })();
53163
+ }
53164
+ function triggerFileIndexBuild(sessionParentPath = getBridgeRoot()) {
53165
+ setImmediate(() => {
53166
+ void ensureFileIndexAsync(sessionParentPath).catch((e) => {
53167
+ console.error("[file-index] Background build failed:", e);
53168
+ });
52959
53169
  });
52960
- };
52961
-
52962
- // src/routing/handlers/send-deploy-session-to-preview-result.ts
52963
- function sendDeploySessionToPreviewResult(ws, id, payload) {
52964
- if (!ws) return;
52965
- sendWsMessage(ws, { type: "deploy_session_to_preview_result", id, ...payload });
52966
53170
  }
52967
53171
 
52968
- // src/routing/handlers/deploy-session-to-preview.ts
52969
- var handleDeploySessionToPreviewMessage = (msg, deps) => {
53172
+ // src/routing/handlers/file-browser-messages.ts
53173
+ function handleFileBrowserRequestMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
53174
+ if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
53175
+ const socket = getWs();
53176
+ if (!socket) return;
53177
+ handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager);
53178
+ }
53179
+ function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
52970
53180
  if (typeof msg.id !== "string") return;
52971
- const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
52972
- const environmentId = typeof msg.environmentId === "string" ? msg.environmentId : "";
52973
- if (!sessionId || !environmentId) return;
53181
+ const socket = getWs();
53182
+ if (!socket) return;
53183
+ handleFileBrowserSearch(
53184
+ msg,
53185
+ socket,
53186
+ e2ee,
53187
+ sessionWorktreeManager
53188
+ );
53189
+ }
53190
+
53191
+ // src/code-nav/handlers/send-code-nav-response.ts
53192
+ var ENCRYPTED_FIELDS = ["definition", "definitions", "references", "symbols", "confidentTarget"];
53193
+ function sendCodeNavResponse(socket, e2ee, payload) {
53194
+ const fieldsToEncrypt = ENCRYPTED_FIELDS.filter((field) => field in payload && payload[field] !== void 0);
53195
+ sendWsMessage(
53196
+ socket,
53197
+ e2ee && fieldsToEncrypt.length > 0 ? e2ee.encryptFields(payload, [...fieldsToEncrypt]) : payload
53198
+ );
53199
+ }
53200
+
53201
+ // src/code-nav/handlers/handle-code-nav-request.ts
53202
+ function handleCodeNavRequest(msg, socket, e2ee, sessionWorktreeManager) {
52974
53203
  void (async () => {
52975
- const ws = deps.getWs();
52976
- const reply = (payload) => sendDeploySessionToPreviewResult(ws, msg.id, payload);
52977
- try {
52978
- if (!deps.previewWorktreeManager) {
52979
- reply({ ok: false, error: "Preview worktree manager unavailable" });
52980
- return;
52981
- }
52982
- const result = await deploySessionToPreviewEnvironment({
52983
- sessionWorktreeManager: deps.sessionWorktreeManager,
52984
- previewWorktreeManager: deps.previewWorktreeManager,
52985
- sessionId,
52986
- environmentId,
52987
- log: deps.log
52988
- });
52989
- reply(result);
52990
- } catch (e) {
52991
- reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
52992
- }
53204
+ const body = await executeCodeNavRequest(msg, sessionWorktreeManager);
53205
+ sendCodeNavResponse(socket, e2ee, {
53206
+ type: "code_nav_response",
53207
+ id: msg.id,
53208
+ ...body
53209
+ });
52993
53210
  })();
52994
- };
53211
+ }
53212
+
53213
+ // src/code-nav/handlers/trigger-symbol-index-build.ts
53214
+ init_normalize_resolved_path();
53215
+
53216
+ // src/routing/handlers/code-nav-messages.ts
53217
+ function handleFileBrowserCodeNavMessage(msg, { getWs, e2ee, sessionWorktreeManager }) {
53218
+ if (typeof msg.id !== "string" || typeof msg.path !== "string") return;
53219
+ const socket = getWs();
53220
+ if (!socket) return;
53221
+ handleCodeNavRequest(
53222
+ {
53223
+ id: msg.id,
53224
+ type: "code_nav",
53225
+ op: msg.op === "references" ? "references" : msg.op === "symbols" ? "symbols" : msg.op === "definitions" ? "definitions" : "definition",
53226
+ path: msg.path,
53227
+ line: typeof msg.line === "number" ? msg.line : 1,
53228
+ column: typeof msg.column === "number" ? msg.column : 0,
53229
+ sessionId: msg.sessionId
53230
+ },
53231
+ socket,
53232
+ e2ee,
53233
+ sessionWorktreeManager
53234
+ );
53235
+ }
52995
53236
 
52996
53237
  // src/git/bridge-git-context.ts
52997
53238
  import * as path82 from "node:path";
@@ -53138,80 +53379,64 @@ function handleListRepoBranchesRequestMessage(msg, getWs) {
53138
53379
  })();
53139
53380
  }
53140
53381
 
53141
- // src/routing/dispatch-bridge-message.ts
53382
+ // src/routing/dispatch/file-browser.ts
53383
+ function dispatchBridgeFileBrowserMessage(msg, deps) {
53384
+ switch (msg.type) {
53385
+ case "file_browser_request":
53386
+ handleFileBrowserRequestMessage(msg, deps);
53387
+ break;
53388
+ case "file_browser_search":
53389
+ handleFileBrowserSearchMessage(msg, deps);
53390
+ break;
53391
+ case "code_nav":
53392
+ handleFileBrowserCodeNavMessage(msg, deps);
53393
+ break;
53394
+ case "bridge_git_context_request":
53395
+ handleBridgeGitContextRequestMessage(msg, deps.getWs);
53396
+ break;
53397
+ case "list_repo_branches_request":
53398
+ handleListRepoBranchesRequestMessage(msg, deps.getWs);
53399
+ break;
53400
+ }
53401
+ }
53402
+
53403
+ // src/routing/dispatch/index.ts
53142
53404
  function dispatchBridgeMessage(msg, deps) {
53143
53405
  switch (msg.type) {
53144
53406
  case "auth_token":
53145
- handleAuthToken(msg, deps);
53146
- break;
53147
53407
  case "bridge_identified":
53148
- handleBridgeIdentified(msg, deps);
53149
- break;
53150
53408
  case "ha":
53151
- handleBridgeHeartbeatAck(msg, deps);
53409
+ dispatchBridgeConnectionMessage(msg, deps);
53152
53410
  break;
53153
53411
  case "preview_environments_config":
53154
- handlePreviewEnvironmentsConfig(msg, deps);
53155
- break;
53156
53412
  case "preview_environment_control":
53157
- handlePreviewEnvironmentControl(msg, deps);
53158
- break;
53159
53413
  case "deploy_session_to_preview":
53160
- handleDeploySessionToPreviewMessage(msg, deps);
53414
+ dispatchBridgePreviewMessage(msg, deps);
53161
53415
  break;
53162
53416
  case "agent_config":
53163
- handleAgentConfigMessage(msg, deps);
53164
- break;
53165
53417
  case "prompt_queue_state":
53166
- handlePromptQueueStateMessage(msg, deps);
53167
- break;
53168
53418
  case "prompt":
53169
- handlePromptMessage(msg, deps);
53170
- break;
53171
53419
  case "session_git_request":
53172
- handleSessionGitRequestMessage(msg, deps);
53173
- break;
53174
53420
  case "rename_session_branch":
53175
- handleRenameSessionBranchMessage(msg, deps);
53176
- break;
53177
53421
  case "session_archived":
53178
- handleSessionArchivedMessage(msg, deps);
53179
- break;
53180
53422
  case "session_discarded":
53181
- handleSessionDiscardedMessage(msg, deps);
53182
- break;
53183
53423
  case "revert_turn_snapshot":
53184
- handleRevertTurnSnapshotMessage(msg, deps);
53185
- break;
53186
53424
  case "cursor_request_response":
53187
- handleSessionRequestResponseMessage(msg, deps);
53425
+ dispatchBridgeSessionMessage(msg, deps);
53188
53426
  break;
53189
53427
  case "skill_call":
53190
- handleSkillCallMessage(msg, deps);
53191
- break;
53192
- case "file_browser_request":
53193
- handleFileBrowserRequestMessage(msg, deps);
53194
- break;
53195
- case "file_browser_search":
53196
- handleFileBrowserSearchMessage(msg, deps);
53197
- break;
53198
- case "code_nav":
53199
- handleFileBrowserCodeNavMessage(msg, deps);
53200
- break;
53201
53428
  case "skill_layout_request":
53202
- handleSkillLayoutRequest(msg, deps);
53203
- break;
53204
53429
  case "install_skills":
53205
- handleInstallSkillsMessage(msg, deps);
53206
- break;
53430
+ case "install_agent":
53207
53431
  case "refresh_local_skills":
53208
- handleRefreshLocalSkills(msg, deps);
53432
+ dispatchBridgeSkillsMessage(msg, deps);
53209
53433
  break;
53434
+ case "file_browser_request":
53435
+ case "file_browser_search":
53436
+ case "code_nav":
53210
53437
  case "bridge_git_context_request":
53211
- handleBridgeGitContextRequestMessage(msg, deps.getWs);
53212
- break;
53213
53438
  case "list_repo_branches_request":
53214
- handleListRepoBranchesRequestMessage(msg, deps.getWs);
53439
+ dispatchBridgeFileBrowserMessage(msg, deps);
53215
53440
  break;
53216
53441
  }
53217
53442
  }