@junctionpanel/server 0.1.28 → 0.1.31

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.
Files changed (63) hide show
  1. package/dist/server/client/daemon-client.d.ts +42 -5
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +85 -3
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +7 -0
  6. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  8. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +8 -0
  9. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  10. package/dist/server/server/agent/providers/codex-app-server-agent.js +244 -135
  11. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  12. package/dist/server/server/agent/providers/gemini-agent.d.ts +4 -1
  13. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  14. package/dist/server/server/agent/providers/gemini-agent.js +36 -8
  15. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  16. package/dist/server/server/agent/providers/image-attachments.d.ts +8 -0
  17. package/dist/server/server/agent/providers/image-attachments.d.ts.map +1 -0
  18. package/dist/server/server/agent/providers/image-attachments.js +47 -0
  19. package/dist/server/server/agent/providers/image-attachments.js.map +1 -0
  20. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +3 -0
  21. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  22. package/dist/server/server/daemon-doctor.d.ts +39 -0
  23. package/dist/server/server/daemon-doctor.d.ts.map +1 -0
  24. package/dist/server/server/daemon-doctor.js +260 -0
  25. package/dist/server/server/daemon-doctor.js.map +1 -0
  26. package/dist/server/server/daemon-provider-settings.d.ts +42 -0
  27. package/dist/server/server/daemon-provider-settings.d.ts.map +1 -0
  28. package/dist/server/server/daemon-provider-settings.js +207 -0
  29. package/dist/server/server/daemon-provider-settings.js.map +1 -0
  30. package/dist/server/server/file-explorer/service.d.ts +4 -2
  31. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  32. package/dist/server/server/file-explorer/service.js +104 -2
  33. package/dist/server/server/file-explorer/service.js.map +1 -1
  34. package/dist/server/server/persisted-config.d.ts +24 -24
  35. package/dist/server/server/session.d.ts +10 -1
  36. package/dist/server/server/session.d.ts.map +1 -1
  37. package/dist/server/server/session.js +439 -62
  38. package/dist/server/server/session.js.map +1 -1
  39. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  40. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  41. package/dist/server/server/worktree-bootstrap.js +4 -0
  42. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  43. package/dist/server/shared/messages.d.ts +4245 -34
  44. package/dist/server/shared/messages.d.ts.map +1 -1
  45. package/dist/server/shared/messages.js +167 -0
  46. package/dist/server/shared/messages.js.map +1 -1
  47. package/dist/server/utils/checkout-git.d.ts +23 -4
  48. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  49. package/dist/server/utils/checkout-git.js +298 -79
  50. package/dist/server/utils/checkout-git.js.map +1 -1
  51. package/dist/server/utils/directory-suggestions.d.ts +4 -0
  52. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  53. package/dist/server/utils/directory-suggestions.js +83 -5
  54. package/dist/server/utils/directory-suggestions.js.map +1 -1
  55. package/dist/server/utils/workspace-ref-files.d.ts +31 -0
  56. package/dist/server/utils/workspace-ref-files.d.ts.map +1 -0
  57. package/dist/server/utils/workspace-ref-files.js +207 -0
  58. package/dist/server/utils/workspace-ref-files.js.map +1 -0
  59. package/dist/server/utils/worktree.d.ts +6 -3
  60. package/dist/server/utils/worktree.d.ts.map +1 -1
  61. package/dist/server/utils/worktree.js +46 -45
  62. package/dist/server/utils/worktree.js.map +1 -1
  63. package/package.json +2 -2
@@ -1,15 +1,16 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import { watch } from 'node:fs';
2
+ import { existsSync, watch } from 'node:fs';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
- import { resolve, sep, basename, dirname } from 'path';
6
- import { homedir } from 'node:os';
5
+ import { resolve, sep, basename, dirname, parse as parsePath } from 'path';
6
+ import { homedir, hostname } from 'node:os';
7
7
  import { z } from 'zod';
8
8
  import { serializeAgentStreamEvent, } from './messages.js';
9
9
  import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
10
10
  import { buildConfigOverrides, buildSessionConfig, extractTimestamps, extractTimelineSnapshot, } from './persistence-hooks.js';
11
11
  import { experimental_createMCPClient } from 'ai';
12
12
  import { buildProviderRegistry } from './agent/provider-registry.js';
13
+ import { applyProviderEnv, } from './agent/provider-launch-config.js';
13
14
  import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
14
15
  import { resolveEffectiveThinkingOptionId, toAgentPayload } from './agent/agent-projections.js';
15
16
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
@@ -20,14 +21,18 @@ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from
20
21
  import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, restoreInRepoWorktree, } from '../utils/worktree.js';
21
22
  import { readJunctionWorktreeMetadata } from '../utils/worktree-metadata.js';
22
23
  import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
23
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
24
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, listGitRemotes, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
24
25
  import { getProjectIcon } from '../utils/project-icon.js';
25
26
  import { expandTilde } from '../utils/path.js';
26
- import { searchHomeDirectories, searchWorkspaceEntries, searchGitRepositories, checkIsGitRepo } from '../utils/directory-suggestions.js';
27
+ import { searchHomeDirectories, searchWorkspaceEntries, searchWorkspaceEntriesAtGitRef, searchGitRepositories, checkIsGitRepo, } from '../utils/directory-suggestions.js';
27
28
  import { cloneRepository } from '../utils/git-clone.js';
28
29
  import { initRepository } from '../utils/git-init.js';
29
30
  import { resolveClientMessageId } from './client-message-id.js';
30
31
  import { deriveProjectGroupingKey, deriveProjectGroupingName } from '../shared/project-grouping.js';
32
+ import { resolveDaemonVersion } from './daemon-version.js';
33
+ import { runDaemonDoctor } from './daemon-doctor.js';
34
+ import { MANAGED_DAEMON_PROVIDERS, autoRouteProviderExecutable, loadDaemonProviderSettings, saveDaemonProviderExecutablePath, } from './daemon-provider-settings.js';
35
+ import { loadPersistedConfig } from './persisted-config.js';
31
36
  const execAsync = promisify(exec);
32
37
  const READ_ONLY_GIT_ENV = {
33
38
  ...process.env,
@@ -50,6 +55,7 @@ class SessionRequestError extends Error {
50
55
  }
51
56
  }
52
57
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
58
+ const SAFE_GIT_REMOTE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
53
59
  function coerceAgentProvider(logger, value, agentId) {
54
60
  if (isValidAgentProvider(value)) {
55
61
  return value;
@@ -77,6 +83,19 @@ function toAgentPersistenceHandle(logger, handle) {
77
83
  metadata: handle.metadata,
78
84
  };
79
85
  }
86
+ function normalizeAgentRunOptions(runOptions) {
87
+ if (!runOptions) {
88
+ return undefined;
89
+ }
90
+ return {
91
+ ...(runOptions.outputSchema !== undefined ? { outputSchema: runOptions.outputSchema } : {}),
92
+ ...(runOptions.resumeFrom ? { resumeFrom: runOptions.resumeFrom } : {}),
93
+ ...(runOptions.maxThinkingTokens !== undefined
94
+ ? { maxThinkingTokens: runOptions.maxThinkingTokens }
95
+ : {}),
96
+ ...(runOptions.extra ? { extra: runOptions.extra } : {}),
97
+ };
98
+ }
80
99
  /**
81
100
  * Session represents a single connected client session.
82
101
  * It owns all state management, orchestration logic, and message processing.
@@ -800,9 +819,15 @@ export class Session {
800
819
  case 'branch_suggestions_request':
801
820
  await this.handleBranchSuggestionsRequest(msg);
802
821
  break;
822
+ case 'git_remotes_request':
823
+ await this.handleGitRemotesRequest(msg);
824
+ break;
803
825
  case 'directory_suggestions_request':
804
826
  await this.handleDirectorySuggestionsRequest(msg);
805
827
  break;
828
+ case 'workspace_file_suggestions_request':
829
+ await this.handleWorkspaceFileSuggestionsRequest(msg);
830
+ break;
806
831
  case 'git_clone_request':
807
832
  await this.handleGitCloneRequest(msg);
808
833
  break;
@@ -863,6 +888,18 @@ export class Session {
863
888
  case 'list_available_providers_request':
864
889
  await this.handleListAvailableProvidersRequest(msg);
865
890
  break;
891
+ case 'run_daemon_doctor_request':
892
+ await this.handleRunDaemonDoctorRequest(msg);
893
+ break;
894
+ case 'get_daemon_provider_settings_request':
895
+ await this.handleGetDaemonProviderSettingsRequest(msg);
896
+ break;
897
+ case 'update_daemon_provider_settings_request':
898
+ await this.handleUpdateDaemonProviderSettingsRequest(msg);
899
+ break;
900
+ case 'auto_route_provider_request':
901
+ await this.handleAutoRouteProviderRequest(msg);
902
+ break;
866
903
  case 'clear_agent_attention':
867
904
  await this.handleClearAgentAttention(msg.agentId);
868
905
  break;
@@ -1150,19 +1187,43 @@ export class Session {
1150
1187
  if (!record) {
1151
1188
  throw new Error(`Agent not found: ${agentId}`);
1152
1189
  }
1190
+ const allRecords = await this.agentStorage.list();
1191
+ const siblingRecords = record.archivedWorktree?.cleanupState === 'deleted' && record.archivedWorktree
1192
+ ? allRecords.filter((candidate) => {
1193
+ if (candidate.id === record.id || !candidate.archivedAt) {
1194
+ return false;
1195
+ }
1196
+ return candidate.cwd === record.cwd;
1197
+ })
1198
+ : [];
1153
1199
  let nextRecord = {
1154
1200
  ...record,
1155
1201
  archivedAt: null,
1156
1202
  };
1157
1203
  let restoredWorktree = null;
1158
1204
  if (record.archivedWorktree?.cleanupState === 'deleted') {
1159
- restoredWorktree = await restoreInRepoWorktree({
1160
- repoRoot: record.archivedWorktree.repoRoot,
1161
- baseBranch: record.archivedWorktree.baseBranch,
1162
- branchName: record.archivedWorktree.branchName,
1163
- worktreeSlug: record.archivedWorktree.worktreeSlug,
1164
- runSetup: false,
1165
- });
1205
+ try {
1206
+ restoredWorktree = await restoreInRepoWorktree({
1207
+ repoRoot: record.archivedWorktree.repoRoot,
1208
+ baseBranch: record.archivedWorktree.baseBranch,
1209
+ branchName: record.archivedWorktree.branchName,
1210
+ worktreeSlug: record.archivedWorktree.worktreeSlug,
1211
+ runSetup: false,
1212
+ });
1213
+ }
1214
+ catch (error) {
1215
+ const message = error instanceof Error ? error.message : String(error);
1216
+ const restoredPath = record.archivedWorktree.originalCwd;
1217
+ if (!message.includes('Worktree path already exists') || !existsSync(restoredPath)) {
1218
+ throw error;
1219
+ }
1220
+ restoredWorktree = {
1221
+ branchName: record.archivedWorktree.branchName,
1222
+ worktreePath: restoredPath,
1223
+ baseBranch: record.archivedWorktree.baseBranch,
1224
+ workspaceName: basename(restoredPath),
1225
+ };
1226
+ }
1166
1227
  nextRecord = {
1167
1228
  ...nextRecord,
1168
1229
  cwd: restoredWorktree.worktreePath,
@@ -1174,13 +1235,34 @@ export class Session {
1174
1235
  },
1175
1236
  };
1176
1237
  }
1177
- await this.agentStorage.upsert(nextRecord);
1178
- const liveAgent = this.agentManager.getAgent(agentId);
1179
- if (liveAgent) {
1180
- this.agentManager.notifyAgentState(agentId);
1181
- }
1182
- else {
1183
- await this.forwardStoredAgentRecordUpdate(nextRecord);
1238
+ const recordsToRestore = [record, ...siblingRecords].map((candidate) => {
1239
+ const nextArchivedWorktree = restoredWorktree && candidate.archivedWorktree
1240
+ ? {
1241
+ ...candidate.archivedWorktree,
1242
+ originalCwd: restoredWorktree.worktreePath,
1243
+ cleanupState: 'active',
1244
+ cleanedUpAt: null,
1245
+ }
1246
+ : candidate.archivedWorktree;
1247
+ return {
1248
+ ...candidate,
1249
+ archivedAt: null,
1250
+ cwd: restoredWorktree ? restoredWorktree.worktreePath : candidate.cwd,
1251
+ archivedWorktree: nextArchivedWorktree,
1252
+ };
1253
+ });
1254
+ for (const restoredRecord of recordsToRestore) {
1255
+ await this.agentStorage.upsert(restoredRecord);
1256
+ const liveAgent = this.agentManager.getAgent(restoredRecord.id);
1257
+ if (liveAgent) {
1258
+ this.agentManager.notifyAgentState(restoredRecord.id);
1259
+ }
1260
+ else {
1261
+ await this.forwardStoredAgentRecordUpdate(restoredRecord);
1262
+ }
1263
+ if (restoredRecord.id === agentId) {
1264
+ nextRecord = restoredRecord;
1265
+ }
1184
1266
  }
1185
1267
  this.emit({
1186
1268
  type: 'agent_unarchived',
@@ -1363,7 +1445,7 @@ export class Session {
1363
1445
  * Handle create agent request
1364
1446
  */
1365
1447
  async handleCreateAgentRequest(msg) {
1366
- const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, bootstrapSetupOverride, images, labels, } = msg;
1448
+ const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, initialRunOptions, git, bootstrapSetupOverride, generalPreferencesApplied, images, labels, } = msg;
1367
1449
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1368
1450
  try {
1369
1451
  const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
@@ -1387,21 +1469,27 @@ export class Session {
1387
1469
  },
1388
1470
  });
1389
1471
  }
1390
- const trimmedPrompt = initialPrompt?.trim();
1472
+ const trimmedPrompt = initialPrompt?.trim() ?? '';
1473
+ const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
1391
1474
  const runInitialPrompt = async () => {
1392
- if (!trimmedPrompt) {
1475
+ if (!hasInitialMessage) {
1393
1476
  return;
1394
1477
  }
1395
- scheduleAgentMetadataGeneration({
1396
- agentManager: this.agentManager,
1397
- agentId: snapshot.id,
1398
- cwd: snapshot.cwd,
1399
- initialPrompt: trimmedPrompt,
1400
- explicitTitle: snapshot.config.title,
1401
- junctionHome: this.junctionHome,
1402
- logger: this.sessionLogger,
1403
- });
1404
- await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined);
1478
+ if (trimmedPrompt.length > 0) {
1479
+ scheduleAgentMetadataGeneration({
1480
+ agentManager: this.agentManager,
1481
+ agentId: snapshot.id,
1482
+ cwd: snapshot.cwd,
1483
+ initialPrompt: trimmedPrompt,
1484
+ explicitTitle: snapshot.config.title,
1485
+ junctionHome: this.junctionHome,
1486
+ logger: this.sessionLogger,
1487
+ });
1488
+ }
1489
+ await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, normalizeAgentRunOptions({
1490
+ ...(outputSchema ? { outputSchema } : {}),
1491
+ ...(initialRunOptions ?? {}),
1492
+ }));
1405
1493
  };
1406
1494
  const handleInitialPromptError = (promptError) => {
1407
1495
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
@@ -1415,7 +1503,7 @@ export class Session {
1415
1503
  },
1416
1504
  });
1417
1505
  };
1418
- if (trimmedPrompt && !worktreeConfig) {
1506
+ if (hasInitialMessage && !worktreeConfig) {
1419
1507
  void runInitialPrompt().catch(handleInitialPromptError);
1420
1508
  }
1421
1509
  if (worktreeConfig) {
@@ -1423,6 +1511,7 @@ export class Session {
1423
1511
  agentId: snapshot.id,
1424
1512
  worktree: worktreeConfig,
1425
1513
  setupOverride: bootstrapSetupOverride,
1514
+ generalPreferencesApplied,
1426
1515
  terminalManager: this.terminalManager,
1427
1516
  appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1428
1517
  agentManager: this.agentManager,
@@ -1435,7 +1524,7 @@ export class Session {
1435
1524
  item,
1436
1525
  }),
1437
1526
  onSetupSettled: async (result) => {
1438
- if (!trimmedPrompt || result.setupStatus === 'failed') {
1527
+ if (!hasInitialMessage || result.setupStatus === 'failed') {
1439
1528
  return;
1440
1529
  }
1441
1530
  await runInitialPrompt().catch(handleInitialPromptError);
@@ -1587,7 +1676,7 @@ export class Session {
1587
1676
  this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1588
1677
  }
1589
1678
  }
1590
- async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
1679
+ async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, labels) {
1591
1680
  const cwd = expandTilde(config.cwd);
1592
1681
  const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
1593
1682
  let repoRoot;
@@ -1608,7 +1697,26 @@ export class Session {
1608
1697
  catch {
1609
1698
  throw new Error('Selected project must be a git repository. Junction always creates a new worktree in .junction/.');
1610
1699
  }
1611
- const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot));
1700
+ if (!normalized) {
1701
+ const ownership = await isJunctionOwnedWorktreeCwd(cwd, {
1702
+ junctionHome: this.junctionHome,
1703
+ });
1704
+ if (ownership.allowed) {
1705
+ const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(cwd, {
1706
+ junctionHome: this.junctionHome,
1707
+ });
1708
+ const workspaceName = labels?.['junction:workspace']
1709
+ ?? (resolvedWorktree ? basename(resolvedWorktree.worktreePath) : basename(cwd));
1710
+ return {
1711
+ sessionConfig: {
1712
+ ...config,
1713
+ cwd,
1714
+ },
1715
+ autoWorkspaceName: workspaceName,
1716
+ };
1717
+ }
1718
+ }
1719
+ const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot, { remoteName: normalized?.remoteName }));
1612
1720
  if (!baseBranch) {
1613
1721
  throw new Error('Unable to determine a base branch for worktree creation');
1614
1722
  }
@@ -1616,6 +1724,7 @@ export class Session {
1616
1724
  const createdWorktree = await createInRepoWorktree({
1617
1725
  repoRoot,
1618
1726
  baseBranch,
1727
+ remoteName: normalized?.remoteName,
1619
1728
  runSetup: false,
1620
1729
  });
1621
1730
  return {
@@ -1684,6 +1793,153 @@ export class Session {
1684
1793
  });
1685
1794
  }
1686
1795
  }
1796
+ resolveDaemonVersionSafe() {
1797
+ try {
1798
+ return resolveDaemonVersion(import.meta.url);
1799
+ }
1800
+ catch {
1801
+ return null;
1802
+ }
1803
+ }
1804
+ toDaemonMetadata(input) {
1805
+ const homeDir = input?.homeDir ?? homedir();
1806
+ const rootDir = input?.rootDir ?? parsePath(homeDir).root;
1807
+ return {
1808
+ hostname: hostname(),
1809
+ version: this.resolveDaemonVersionSafe(),
1810
+ platform: process.platform,
1811
+ homeDir,
1812
+ rootDir,
1813
+ };
1814
+ }
1815
+ async handleRunDaemonDoctorRequest(msg) {
1816
+ try {
1817
+ const result = await runDaemonDoctor(this.junctionHome, this.sessionLogger);
1818
+ this.emit({
1819
+ type: 'run_daemon_doctor_response',
1820
+ payload: {
1821
+ daemon: result.daemon,
1822
+ summary: result.summary,
1823
+ checks: result.checks,
1824
+ ranAt: result.ranAt,
1825
+ error: null,
1826
+ requestId: msg.requestId,
1827
+ },
1828
+ });
1829
+ }
1830
+ catch (error) {
1831
+ this.sessionLogger.error({ err: error }, 'Failed to run daemon doctor');
1832
+ const snapshot = loadDaemonProviderSettings(this.junctionHome);
1833
+ this.emit({
1834
+ type: 'run_daemon_doctor_response',
1835
+ payload: {
1836
+ daemon: this.toDaemonMetadata(snapshot),
1837
+ summary: 'fail',
1838
+ checks: [],
1839
+ ranAt: new Date().toISOString(),
1840
+ error: error instanceof Error ? error.message : String(error),
1841
+ requestId: msg.requestId,
1842
+ },
1843
+ });
1844
+ }
1845
+ }
1846
+ async handleGetDaemonProviderSettingsRequest(msg) {
1847
+ try {
1848
+ const snapshot = loadDaemonProviderSettings(this.junctionHome);
1849
+ this.emit({
1850
+ type: 'get_daemon_provider_settings_response',
1851
+ payload: {
1852
+ daemon: this.toDaemonMetadata(snapshot),
1853
+ providers: MANAGED_DAEMON_PROVIDERS.map((provider) => snapshot.providers[provider]),
1854
+ error: null,
1855
+ requestId: msg.requestId,
1856
+ },
1857
+ });
1858
+ }
1859
+ catch (error) {
1860
+ this.sessionLogger.error({ err: error }, 'Failed to load daemon provider settings');
1861
+ this.emit({
1862
+ type: 'get_daemon_provider_settings_response',
1863
+ payload: {
1864
+ daemon: this.toDaemonMetadata(),
1865
+ providers: [],
1866
+ error: error instanceof Error ? error.message : String(error),
1867
+ requestId: msg.requestId,
1868
+ },
1869
+ });
1870
+ }
1871
+ }
1872
+ async handleUpdateDaemonProviderSettingsRequest(msg) {
1873
+ try {
1874
+ const snapshot = saveDaemonProviderExecutablePath({
1875
+ junctionHome: this.junctionHome,
1876
+ provider: msg.provider,
1877
+ executablePath: msg.executablePath,
1878
+ });
1879
+ this.emit({
1880
+ type: 'update_daemon_provider_settings_response',
1881
+ payload: {
1882
+ daemon: this.toDaemonMetadata(snapshot),
1883
+ provider: snapshot.providers[msg.provider],
1884
+ error: null,
1885
+ requestId: msg.requestId,
1886
+ },
1887
+ });
1888
+ await this.handleRestartServerRequest(msg.requestId, 'settings_update');
1889
+ }
1890
+ catch (error) {
1891
+ this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to update daemon provider settings');
1892
+ this.emit({
1893
+ type: 'update_daemon_provider_settings_response',
1894
+ payload: {
1895
+ daemon: this.toDaemonMetadata(),
1896
+ provider: null,
1897
+ error: error instanceof Error ? error.message : String(error),
1898
+ requestId: msg.requestId,
1899
+ },
1900
+ });
1901
+ }
1902
+ }
1903
+ async handleAutoRouteProviderRequest(msg) {
1904
+ try {
1905
+ const config = loadPersistedConfig(this.junctionHome);
1906
+ const env = applyProviderEnv(process.env, config.agents?.providers?.[msg.provider]);
1907
+ const executablePath = autoRouteProviderExecutable(msg.provider, {
1908
+ env,
1909
+ platform: process.platform,
1910
+ });
1911
+ if (!executablePath) {
1912
+ throw new SessionRequestError('provider_not_found', `Could not automatically locate ${msg.provider} on this daemon.`);
1913
+ }
1914
+ const snapshot = saveDaemonProviderExecutablePath({
1915
+ junctionHome: this.junctionHome,
1916
+ provider: msg.provider,
1917
+ executablePath,
1918
+ });
1919
+ this.emit({
1920
+ type: 'auto_route_provider_response',
1921
+ payload: {
1922
+ daemon: this.toDaemonMetadata(snapshot),
1923
+ provider: snapshot.providers[msg.provider],
1924
+ error: null,
1925
+ requestId: msg.requestId,
1926
+ },
1927
+ });
1928
+ await this.handleRestartServerRequest(msg.requestId, 'settings_update');
1929
+ }
1930
+ catch (error) {
1931
+ this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to auto-route provider executable');
1932
+ this.emit({
1933
+ type: 'auto_route_provider_response',
1934
+ payload: {
1935
+ daemon: this.toDaemonMetadata(),
1936
+ provider: null,
1937
+ error: error instanceof Error ? error.message : String(error),
1938
+ requestId: msg.requestId,
1939
+ },
1940
+ });
1941
+ }
1942
+ }
1687
1943
  normalizeGitOptions(gitOptions, legacyWorktreeName) {
1688
1944
  const fallbackOptions = legacyWorktreeName
1689
1945
  ? {
@@ -1698,6 +1954,7 @@ export class Session {
1698
1954
  return null;
1699
1955
  }
1700
1956
  const baseBranch = merged.baseBranch?.trim() || undefined;
1957
+ const remoteName = merged.remoteName?.trim() || undefined;
1701
1958
  const createWorktree = Boolean(merged.createWorktree);
1702
1959
  const createNewBranch = Boolean(merged.createNewBranch);
1703
1960
  const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
@@ -1710,6 +1967,9 @@ export class Session {
1710
1967
  if (baseBranch) {
1711
1968
  this.assertSafeGitRef(baseBranch, 'base branch');
1712
1969
  }
1970
+ if (remoteName) {
1971
+ this.assertSafeRemoteName(remoteName);
1972
+ }
1713
1973
  if (createWorktree && !baseBranch) {
1714
1974
  throw new Error('Base branch is required when creating a worktree');
1715
1975
  }
@@ -1733,6 +1993,7 @@ export class Session {
1733
1993
  }
1734
1994
  return {
1735
1995
  baseBranch,
1996
+ remoteName,
1736
1997
  createNewBranch,
1737
1998
  newBranchName: normalizedBranchName,
1738
1999
  createWorktree,
@@ -1744,6 +2005,11 @@ export class Session {
1744
2005
  throw new Error(`Invalid ${label}: ${ref}`);
1745
2006
  }
1746
2007
  }
2008
+ assertSafeRemoteName(remoteName) {
2009
+ if (!SAFE_GIT_REMOTE_NAME_PATTERN.test(remoteName)) {
2010
+ throw new Error(`Invalid remote name: ${remoteName}`);
2011
+ }
2012
+ }
1747
2013
  toCheckoutError(error) {
1748
2014
  if (error instanceof NotGitRepoError) {
1749
2015
  return { code: 'NOT_GIT_REPO', message: error.message };
@@ -2201,6 +2467,10 @@ export class Session {
2201
2467
  const { cwd, branchName, requestId } = msg;
2202
2468
  try {
2203
2469
  const resolvedCwd = expandTilde(cwd);
2470
+ const remoteName = msg.remoteName?.trim() || undefined;
2471
+ if (remoteName) {
2472
+ this.assertSafeRemoteName(remoteName);
2473
+ }
2204
2474
  // Try local branch first
2205
2475
  try {
2206
2476
  await execAsync(`git rev-parse --verify ${branchName}`, {
@@ -2222,26 +2492,45 @@ export class Session {
2222
2492
  catch {
2223
2493
  // Local branch doesn't exist, try remote
2224
2494
  }
2225
- // Try remote branch (origin/{branchName})
2226
- try {
2227
- await execAsync(`git rev-parse --verify origin/${branchName}`, {
2228
- cwd: resolvedCwd,
2229
- env: READ_ONLY_GIT_ENV,
2230
- });
2231
- this.emit({
2232
- type: 'validate_branch_response',
2233
- payload: {
2234
- exists: true,
2235
- resolvedRef: `origin/${branchName}`,
2236
- isRemote: true,
2237
- error: null,
2238
- requestId,
2239
- },
2240
- });
2241
- return;
2242
- }
2243
- catch {
2244
- // Remote branch doesn't exist either
2495
+ const { stdout: remoteStdout } = await execAsync('git remote', {
2496
+ cwd: resolvedCwd,
2497
+ env: READ_ONLY_GIT_ENV,
2498
+ });
2499
+ const configuredRemotes = remoteStdout
2500
+ .split('\n')
2501
+ .map((line) => line.trim())
2502
+ .filter((line) => line.length > 0)
2503
+ .sort((left, right) => left.localeCompare(right));
2504
+ const remoteCandidates = remoteName
2505
+ ? [remoteName, 'origin', ...configuredRemotes]
2506
+ : ['origin', ...configuredRemotes];
2507
+ const seen = new Set();
2508
+ for (const candidateRemote of remoteCandidates) {
2509
+ const trimmedRemote = candidateRemote.trim();
2510
+ if (!trimmedRemote || seen.has(trimmedRemote)) {
2511
+ continue;
2512
+ }
2513
+ seen.add(trimmedRemote);
2514
+ try {
2515
+ await execAsync(`git rev-parse --verify ${trimmedRemote}/${branchName}`, {
2516
+ cwd: resolvedCwd,
2517
+ env: READ_ONLY_GIT_ENV,
2518
+ });
2519
+ this.emit({
2520
+ type: 'validate_branch_response',
2521
+ payload: {
2522
+ exists: true,
2523
+ resolvedRef: `${trimmedRemote}/${branchName}`,
2524
+ isRemote: true,
2525
+ error: null,
2526
+ requestId,
2527
+ },
2528
+ });
2529
+ return;
2530
+ }
2531
+ catch {
2532
+ // try next remote
2533
+ }
2245
2534
  }
2246
2535
  // Branch not found anywhere
2247
2536
  this.emit({
@@ -2272,7 +2561,11 @@ export class Session {
2272
2561
  const { cwd, query, limit, requestId } = msg;
2273
2562
  try {
2274
2563
  const resolvedCwd = expandTilde(cwd);
2275
- const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2564
+ const branches = await listBranchSuggestions(resolvedCwd, {
2565
+ query,
2566
+ limit,
2567
+ remoteName: msg.remoteName,
2568
+ });
2276
2569
  this.emit({
2277
2570
  type: 'branch_suggestions_response',
2278
2571
  payload: {
@@ -2293,6 +2586,30 @@ export class Session {
2293
2586
  });
2294
2587
  }
2295
2588
  }
2589
+ async handleGitRemotesRequest(msg) {
2590
+ const { cwd, requestId } = msg;
2591
+ try {
2592
+ const remotes = await listGitRemotes(expandTilde(cwd));
2593
+ this.emit({
2594
+ type: 'git_remotes_response',
2595
+ payload: {
2596
+ remotes,
2597
+ error: null,
2598
+ requestId,
2599
+ },
2600
+ });
2601
+ }
2602
+ catch (error) {
2603
+ this.emit({
2604
+ type: 'git_remotes_response',
2605
+ payload: {
2606
+ remotes: [],
2607
+ error: error instanceof Error ? error.message : String(error),
2608
+ requestId,
2609
+ },
2610
+ });
2611
+ }
2612
+ }
2296
2613
  async handleDirectorySuggestionsRequest(msg) {
2297
2614
  const { query, limit, requestId, cwd, includeFiles, includeDirectories, onlyGitRepos } = msg;
2298
2615
  try {
@@ -2351,6 +2668,55 @@ export class Session {
2351
2668
  });
2352
2669
  }
2353
2670
  }
2671
+ async handleWorkspaceFileSuggestionsRequest(msg) {
2672
+ const { cwd, query, limit, requestId, includeDirectories, includeFiles, ref } = msg;
2673
+ try {
2674
+ const workspaceCwd = expandTilde(cwd);
2675
+ if (ref) {
2676
+ this.assertSafeGitRef(ref, 'workspace file ref');
2677
+ }
2678
+ const entries = ref
2679
+ ? await searchWorkspaceEntriesAtGitRef({
2680
+ cwd: workspaceCwd,
2681
+ ref,
2682
+ query,
2683
+ limit,
2684
+ includeDirectories,
2685
+ includeFiles,
2686
+ })
2687
+ : await searchWorkspaceEntries({
2688
+ cwd: workspaceCwd,
2689
+ query,
2690
+ limit,
2691
+ includeDirectories,
2692
+ includeFiles,
2693
+ });
2694
+ this.emit({
2695
+ type: 'workspace_file_suggestions_response',
2696
+ payload: {
2697
+ cwd,
2698
+ query,
2699
+ ref: ref ?? null,
2700
+ entries,
2701
+ error: null,
2702
+ requestId,
2703
+ },
2704
+ });
2705
+ }
2706
+ catch (error) {
2707
+ this.emit({
2708
+ type: 'workspace_file_suggestions_response',
2709
+ payload: {
2710
+ cwd,
2711
+ query,
2712
+ ref: ref ?? null,
2713
+ entries: [],
2714
+ error: error instanceof Error ? error.message : String(error),
2715
+ requestId,
2716
+ },
2717
+ });
2718
+ }
2719
+ }
2354
2720
  async handleGitCloneRequest(msg) {
2355
2721
  const { url, targetDirectory, requestId } = msg;
2356
2722
  try {
@@ -2792,6 +3158,7 @@ export class Session {
2792
3158
  }
2793
3159
  await mergeFromBase(cwd, {
2794
3160
  baseRef: msg.baseRef,
3161
+ remoteName: msg.remoteName,
2795
3162
  requireCleanTarget: msg.requireCleanTarget ?? true,
2796
3163
  });
2797
3164
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
@@ -2820,7 +3187,7 @@ export class Session {
2820
3187
  async handleCheckoutPushRequest(msg) {
2821
3188
  const { cwd, requestId } = msg;
2822
3189
  try {
2823
- await pushCurrentBranch(cwd);
3190
+ await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
2824
3191
  this.emit({
2825
3192
  type: 'checkout_push_response',
2826
3193
  payload: {
@@ -2859,6 +3226,7 @@ export class Session {
2859
3226
  title,
2860
3227
  body,
2861
3228
  base: msg.baseRef,
3229
+ remoteName: msg.remoteName,
2862
3230
  });
2863
3231
  this.emit({
2864
3232
  type: 'checkout_pr_create_response',
@@ -2887,7 +3255,7 @@ export class Session {
2887
3255
  async handleCheckoutPrStatusRequest(msg) {
2888
3256
  const { cwd, requestId } = msg;
2889
3257
  try {
2890
- const prStatus = await getPullRequestStatus(cwd);
3258
+ const prStatus = await getPullRequestStatus(cwd, { remoteName: msg.remoteName });
2891
3259
  this.emit({
2892
3260
  type: 'checkout_pr_status_response',
2893
3261
  payload: {
@@ -2915,7 +3283,7 @@ export class Session {
2915
3283
  async handleCheckoutPrFailureLogsRequest(msg) {
2916
3284
  const { cwd, requestId } = msg;
2917
3285
  try {
2918
- const result = await getPullRequestFailureLogs(cwd);
3286
+ const result = await getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName });
2919
3287
  this.emit({
2920
3288
  type: 'checkout_pr_failure_logs_response',
2921
3289
  payload: {
@@ -2945,6 +3313,7 @@ export class Session {
2945
3313
  try {
2946
3314
  await mergePullRequest(cwd, {
2947
3315
  method: msg.method ?? 'squash',
3316
+ remoteName: msg.remoteName,
2948
3317
  });
2949
3318
  this.emit({
2950
3319
  type: 'checkout_pr_merge_response',
@@ -3295,19 +3664,24 @@ export class Session {
3295
3664
  * Handle read-only file explorer requests scoped to a workspace cwd
3296
3665
  */
3297
3666
  async handleWorkspaceFileExplorerRequest(request) {
3298
- const { cwd, path: requestedPath = '.', mode, requestId } = request;
3667
+ const { cwd, path: requestedPath = '.', mode, requestId, ref } = request;
3299
3668
  try {
3300
3669
  const root = expandTilde(cwd);
3670
+ if (ref) {
3671
+ this.assertSafeGitRef(ref, 'workspace file ref');
3672
+ }
3301
3673
  if (mode === 'list') {
3302
3674
  const directory = await listDirectoryEntries({
3303
3675
  root,
3304
3676
  relativePath: requestedPath,
3677
+ ref,
3305
3678
  });
3306
3679
  this.emit({
3307
3680
  type: 'workspace_file_explorer_response',
3308
3681
  payload: {
3309
3682
  cwd,
3310
3683
  path: directory.path,
3684
+ ref: ref ?? null,
3311
3685
  mode,
3312
3686
  directory,
3313
3687
  file: null,
@@ -3320,12 +3694,14 @@ export class Session {
3320
3694
  const file = await readExplorerFile({
3321
3695
  root,
3322
3696
  relativePath: requestedPath,
3697
+ ref,
3323
3698
  });
3324
3699
  this.emit({
3325
3700
  type: 'workspace_file_explorer_response',
3326
3701
  payload: {
3327
3702
  cwd,
3328
3703
  path: file.path,
3704
+ ref: ref ?? null,
3329
3705
  mode,
3330
3706
  directory: null,
3331
3707
  file,
@@ -3342,6 +3718,7 @@ export class Session {
3342
3718
  payload: {
3343
3719
  cwd,
3344
3720
  path: requestedPath,
3721
+ ref: ref ?? null,
3345
3722
  mode,
3346
3723
  directory: null,
3347
3724
  file: null,
@@ -4028,7 +4405,7 @@ export class Session {
4028
4405
  this.sessionLogger.error({ err: error, agentId }, 'Failed to record user message for send_agent_message_request');
4029
4406
  }
4030
4407
  const prompt = this.buildAgentPrompt(msg.text, msg.images);
4031
- const started = this.startAgentStream(agentId, prompt);
4408
+ const started = this.startAgentStream(agentId, prompt, normalizeAgentRunOptions(msg.runOptions));
4032
4409
  if (!started.ok) {
4033
4410
  this.emit({
4034
4411
  type: 'send_agent_message_response',