@band-ai/sdk 0.4.1 → 0.4.3

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.
@@ -5778,79 +5778,165 @@ function extractAssistantText3(event) {
5778
5778
  return blocks.map((block) => block.type === "text" ? block.text ?? "" : "").filter((text) => text.length > 0).join("\n");
5779
5779
  }
5780
5780
 
5781
- // src/adapters/acp/ACPClientAdapter.ts
5782
- import { spawn as spawn2 } from "child_process";
5783
- import { createConnection } from "net";
5784
- import { Duplex, Readable, Writable } from "stream";
5785
-
5786
- // src/adapters/acp/types.ts
5787
- var DEFAULT_ACP_SERVER_MODES = [
5788
- {
5789
- id: "default",
5790
- name: "Default",
5791
- description: "General-purpose chat mode"
5792
- },
5793
- {
5794
- id: "code",
5795
- name: "Code",
5796
- description: "Route prompts toward coding peers when available"
5781
+ // src/adapters/acp/sessionConfigReconciliation.ts
5782
+ var FAILURE_CODE_SESSION_CONFIG = "session_config";
5783
+ var MISSING_CONFIG_OPTIONS_REASON = "missing_config_options";
5784
+ var AcpSessionConfigTimeoutError = class extends Error {
5785
+ };
5786
+ var AcpSessionConfigError = class extends Error {
5787
+ provider;
5788
+ sessionId;
5789
+ optionId;
5790
+ selectedValue;
5791
+ acpCode;
5792
+ detail;
5793
+ timedOut;
5794
+ constructor(input) {
5795
+ super(input.message, input.cause !== void 0 ? { cause: input.cause } : void 0);
5796
+ this.name = "AcpSessionConfigError";
5797
+ this.provider = input.provider;
5798
+ this.sessionId = input.sessionId;
5799
+ this.optionId = input.optionId;
5800
+ this.selectedValue = input.selectedValue;
5801
+ this.acpCode = input.acpCode;
5802
+ this.detail = input.detail;
5803
+ this.timedOut = input.timedOut ?? false;
5804
+ }
5805
+ toAgentFailure() {
5806
+ return agentFailure(
5807
+ this.provider,
5808
+ this.message,
5809
+ this.acpCode !== void 0 ? String(this.acpCode) : FAILURE_CODE_SESSION_CONFIG,
5810
+ {
5811
+ sessionId: this.sessionId,
5812
+ optionId: this.optionId,
5813
+ selectedValue: this.selectedValue,
5814
+ detail: this.detail
5815
+ }
5816
+ );
5797
5817
  }
5798
- ];
5799
- function createPendingPrompt(sessionId) {
5800
- let markDone = () => void 0;
5801
- const done = new Promise((resolve) => {
5802
- markDone = resolve;
5803
- });
5804
- return {
5805
- sessionId,
5806
- done,
5807
- markDone,
5808
- terminalMessageSeen: false,
5809
- completionTimer: null
5810
- };
5811
- }
5812
- function choosePermissionOption(options) {
5813
- if (options.length === 0) {
5814
- return null;
5818
+ };
5819
+ async function applySessionConfigSelections(input) {
5820
+ let catalog = input.catalog;
5821
+ for (const { configId, value: selectedValue } of sessionConfigSelectionEntries(input.selections)) {
5822
+ if (selectedValue === void 0) {
5823
+ continue;
5824
+ }
5825
+ const option = catalog.find((entry) => entry?.id === configId);
5826
+ if (!option || !isSessionConfigSelect(option)) {
5827
+ throw new AcpSessionConfigError({
5828
+ provider: input.provider,
5829
+ sessionId: input.sessionId,
5830
+ optionId: configId,
5831
+ selectedValue,
5832
+ message: `Session config option "${configId}" is not available after prior selections.`
5833
+ });
5834
+ }
5835
+ if (selectedValue === option.currentValue) {
5836
+ continue;
5837
+ }
5838
+ const availableValues = flattenConfigSelectOptions(option.options).map((entry) => entry.value);
5839
+ if (!availableValues.includes(selectedValue)) {
5840
+ throw new AcpSessionConfigError({
5841
+ provider: input.provider,
5842
+ sessionId: input.sessionId,
5843
+ optionId: configId,
5844
+ selectedValue,
5845
+ message: `Session config value "${selectedValue}" is not advertised for option "${configId}".`,
5846
+ detail: { availableValues }
5847
+ });
5848
+ }
5849
+ const timeoutMessage = `setSessionConfigOption did not respond within ${input.timeoutMs}ms`;
5850
+ try {
5851
+ const response = await withTimeout(
5852
+ input.setOption({ sessionId: input.sessionId, configId, value: selectedValue }),
5853
+ input.timeoutMs,
5854
+ () => new AcpSessionConfigTimeoutError(timeoutMessage)
5855
+ );
5856
+ if (!Array.isArray(response?.configOptions)) {
5857
+ throw new AcpSessionConfigError({
5858
+ provider: input.provider,
5859
+ sessionId: input.sessionId,
5860
+ optionId: configId,
5861
+ selectedValue,
5862
+ message: `Session config option "${configId}" response did not include a refreshed catalog.`,
5863
+ detail: { reason: MISSING_CONFIG_OPTIONS_REASON }
5864
+ });
5865
+ }
5866
+ catalog = response.configOptions;
5867
+ } catch (error) {
5868
+ if (error instanceof AcpSessionConfigError) {
5869
+ throw error;
5870
+ }
5871
+ const acpError = asAcpJsonRpcError(error);
5872
+ throw new AcpSessionConfigError({
5873
+ provider: input.provider,
5874
+ sessionId: input.sessionId,
5875
+ optionId: configId,
5876
+ selectedValue,
5877
+ acpCode: acpError?.code,
5878
+ detail: acpError?.data,
5879
+ message: acpError?.message ?? asErrorMessage(error),
5880
+ cause: error,
5881
+ timedOut: error instanceof AcpSessionConfigTimeoutError
5882
+ });
5883
+ }
5815
5884
  }
5816
- return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options[0];
5885
+ return { catalog };
5817
5886
  }
5818
- function asJsonSafe(value) {
5819
- if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5820
- return value;
5887
+ function sessionConfigSelectionEntries(selections) {
5888
+ if (isOrderedSessionConfigSelections(selections)) {
5889
+ return selections;
5821
5890
  }
5822
- if (Array.isArray(value)) {
5823
- return value.map((item) => asJsonSafe(item));
5891
+ return Object.keys(selections).map((configId) => ({ configId, value: selections[configId] }));
5892
+ }
5893
+ function isOrderedSessionConfigSelections(selections) {
5894
+ return Array.isArray(selections);
5895
+ }
5896
+ function isSessionConfigSelect(option) {
5897
+ return option?.type === "select";
5898
+ }
5899
+ function flattenConfigSelectOptions(options) {
5900
+ if (!Array.isArray(options)) {
5901
+ return [];
5824
5902
  }
5825
- if (typeof value === "object") {
5826
- if ("model_dump" in value && typeof value.model_dump === "function") {
5827
- return asJsonSafe(value.model_dump());
5903
+ return options.flatMap((entry) => {
5904
+ if (!asOptionalRecord(entry)) {
5905
+ return [];
5828
5906
  }
5829
- if ("toJSON" in value && typeof value.toJSON === "function") {
5830
- return asJsonSafe(value.toJSON());
5907
+ if ("group" in entry) {
5908
+ return Array.isArray(entry.options) ? entry.options : [];
5831
5909
  }
5832
- return Object.fromEntries(
5833
- Object.entries(value).map(([key, item]) => [key, asJsonSafe(item)])
5834
- );
5835
- }
5836
- return String(value);
5910
+ return [entry];
5911
+ });
5837
5912
  }
5838
- function normalizeMcpServers(mcpServers) {
5839
- if (!mcpServers) {
5840
- return [];
5913
+ function isAcpErrorResponse(error) {
5914
+ return typeof error === "object" && error !== null && typeof error.code === "number" && typeof error.message === "string";
5915
+ }
5916
+ function asAcpJsonRpcError(error) {
5917
+ if (isAcpErrorResponse(error)) {
5918
+ return error;
5841
5919
  }
5842
- return mcpServers.map((server) => asJsonSafe(server)).filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
5920
+ const nested = asOptionalRecord(error)?.error;
5921
+ return isAcpErrorResponse(nested) ? nested : void 0;
5843
5922
  }
5844
5923
 
5924
+ // src/adapters/acp/ACPClientAdapter.ts
5925
+ import { spawn as spawn2 } from "child_process";
5926
+ import { createConnection } from "net";
5927
+ import { Duplex, Readable, Writable } from "stream";
5928
+
5845
5929
  // src/adapters/acp/client.ts
5846
5930
  var BandACPClient = class {
5847
5931
  sessionChunks = /* @__PURE__ */ new Map();
5848
5932
  permissionHandler;
5933
+ extensionHandler;
5849
5934
  // The handler is connection-scoped and required at construction, so it is
5850
5935
  // already in place before the agent process is spawned: there is no window
5851
5936
  // in which a `session/request_permission` has nowhere to go.
5852
- constructor(permissionHandler) {
5937
+ constructor(permissionHandler, extensionHandler) {
5853
5938
  this.permissionHandler = permissionHandler;
5939
+ this.extensionHandler = extensionHandler;
5854
5940
  }
5855
5941
  beginSession(sessionId) {
5856
5942
  this.sessionChunks.set(sessionId, []);
@@ -5895,62 +5981,26 @@ var BandACPClient = class {
5895
5981
  return chunks;
5896
5982
  }
5897
5983
  async extMethod(method, params) {
5898
- if (method === "cursor/ask_question") {
5899
- const options = Array.isArray(params.options) ? params.options : [];
5900
- const selected = choosePermissionOption(
5901
- options.filter((option) => !!option && typeof option === "object")
5902
- );
5903
- if (!selected) {
5904
- return {
5905
- outcome: {
5906
- type: "cancelled"
5907
- }
5908
- };
5909
- }
5910
- return {
5911
- outcome: {
5912
- type: "selected",
5913
- optionId: selected.optionId
5914
- }
5915
- };
5916
- }
5917
- if (method === "cursor/create_plan") {
5918
- return {
5919
- outcome: {
5920
- type: "approved"
5921
- }
5922
- };
5923
- }
5924
- return {};
5984
+ const result = await this.extensionHandler?.extMethod?.(
5985
+ method,
5986
+ params,
5987
+ { sessionId: sessionIdFrom(params) }
5988
+ );
5989
+ return result ?? {};
5925
5990
  }
5926
5991
  async extNotification(method, params) {
5927
- const sessionId = toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
5928
- if (!sessionId) {
5929
- return;
5930
- }
5931
- if (method === "cursor/update_todos") {
5932
- const todos = Array.isArray(params.todos) ? params.todos : [];
5933
- const lines = todos.filter((todo) => !!todo && typeof todo === "object").map((todo) => `- [${todo.completed === true ? "x" : " "}] ${String(todo.content ?? "")}`).filter((line) => line.trim().length > 0);
5934
- if (lines.length > 0) {
5935
- this.appendChunk(sessionId, {
5936
- chunkType: "plan",
5937
- content: lines.join("\n"),
5938
- metadata: {},
5939
- streamed: false
5940
- });
5941
- }
5992
+ const sessionId = sessionIdFrom(params);
5993
+ const chunks = await this.extensionHandler?.extNotification?.(
5994
+ method,
5995
+ params,
5996
+ { sessionId }
5997
+ );
5998
+ const targetSessionId = sessionId ?? this.extensionHandler?.extensionSessionId?.() ?? null;
5999
+ if (!targetSessionId || !chunks) {
5942
6000
  return;
5943
6001
  }
5944
- if (method === "cursor/task") {
5945
- const result = toOptionalString(params.result);
5946
- if (result) {
5947
- this.appendChunk(sessionId, {
5948
- chunkType: "text",
5949
- content: `[Task completed] ${result}`,
5950
- metadata: {},
5951
- streamed: false
5952
- });
5953
- }
6002
+ for (const chunk of chunks) {
6003
+ this.appendChunk(targetSessionId, chunk);
5954
6004
  }
5955
6005
  }
5956
6006
  appendChunk(sessionId, chunk) {
@@ -6075,6 +6125,68 @@ function extractTextFromContent(content) {
6075
6125
  function toOptionalString(value) {
6076
6126
  return typeof value === "string" && value.length > 0 ? value : null;
6077
6127
  }
6128
+ function sessionIdFrom(params) {
6129
+ return toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
6130
+ }
6131
+
6132
+ // src/adapters/acp/types.ts
6133
+ var DEFAULT_ACP_SERVER_MODES = [
6134
+ {
6135
+ id: "default",
6136
+ name: "Default",
6137
+ description: "General-purpose chat mode"
6138
+ },
6139
+ {
6140
+ id: "code",
6141
+ name: "Code",
6142
+ description: "Route prompts toward coding peers when available"
6143
+ }
6144
+ ];
6145
+ function createPendingPrompt(sessionId) {
6146
+ let markDone = () => void 0;
6147
+ const done = new Promise((resolve) => {
6148
+ markDone = resolve;
6149
+ });
6150
+ return {
6151
+ sessionId,
6152
+ done,
6153
+ markDone,
6154
+ terminalMessageSeen: false,
6155
+ completionTimer: null
6156
+ };
6157
+ }
6158
+ function choosePermissionOption(options) {
6159
+ if (options.length === 0) {
6160
+ return null;
6161
+ }
6162
+ return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options[0];
6163
+ }
6164
+ function asJsonSafe(value) {
6165
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6166
+ return value;
6167
+ }
6168
+ if (Array.isArray(value)) {
6169
+ return value.map((item) => asJsonSafe(item));
6170
+ }
6171
+ if (typeof value === "object") {
6172
+ if ("model_dump" in value && typeof value.model_dump === "function") {
6173
+ return asJsonSafe(value.model_dump());
6174
+ }
6175
+ if ("toJSON" in value && typeof value.toJSON === "function") {
6176
+ return asJsonSafe(value.toJSON());
6177
+ }
6178
+ return Object.fromEntries(
6179
+ Object.entries(value).map(([key, item]) => [key, asJsonSafe(item)])
6180
+ );
6181
+ }
6182
+ return String(value);
6183
+ }
6184
+ function normalizeMcpServers(mcpServers) {
6185
+ if (!mcpServers) {
6186
+ return [];
6187
+ }
6188
+ return mcpServers.map((server) => asJsonSafe(server)).filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
6189
+ }
6078
6190
 
6079
6191
  // src/adapters/acp/loader.ts
6080
6192
  var acpModule = new LazyAsyncValue({
@@ -6088,6 +6200,13 @@ var acpModule = new LazyAsyncValue({
6088
6200
  });
6089
6201
 
6090
6202
  // src/adapters/acp/ACPClientAdapter.ts
6203
+ function createConnectionRetirement() {
6204
+ let reject = () => void 0;
6205
+ const promise = new Promise((_resolve, rejectPromise) => {
6206
+ reject = rejectPromise;
6207
+ });
6208
+ return { promise, reject };
6209
+ }
6091
6210
  var DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 6e4;
6092
6211
  var DEFAULT_TURN_TIMEOUT_MS = 60 * 6e4;
6093
6212
  var SET_SESSION_CONFIG_TIMEOUT_MS = 1e4;
@@ -6113,6 +6232,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6113
6232
  additionalMcpTools;
6114
6233
  clientCapabilities;
6115
6234
  connectionFactory;
6235
+ extensionHandler;
6116
6236
  tcpEndpoint;
6117
6237
  // The value's `generation` is the connection generation the session was
6118
6238
  // last established/restored against. `client` is the exact BandACPClient
@@ -6145,6 +6265,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6145
6265
  permissionTimeoutMs;
6146
6266
  turnTimeoutMs;
6147
6267
  logger;
6268
+ customSection;
6148
6269
  backend = null;
6149
6270
  backendPromise = null;
6150
6271
  client = null;
@@ -6155,6 +6276,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6155
6276
  started = false;
6156
6277
  systemPrompt = "";
6157
6278
  spawnPromise = null;
6279
+ connectionRetirements = /* @__PURE__ */ new WeakMap();
6158
6280
  // Bumped by `stop()` and on every successful spawn install. Cleanup/timeout
6159
6281
  // and permission maps key by this plus session id so a stale generation
6160
6282
  // cannot alias a same-id session on a newer connection.
@@ -6175,12 +6297,14 @@ var ACPClientAdapter = class extends SimpleAdapter {
6175
6297
  this.additionalMcpTools = [...options.additionalMcpTools ?? []];
6176
6298
  this.clientCapabilities = options.clientCapabilities;
6177
6299
  this.connectionFactory = options.connectionFactory;
6300
+ this.extensionHandler = options.extensionHandler;
6178
6301
  this.tcpEndpoint = tcpEndpoint;
6179
6302
  this.resolvePermission = options.resolvePermission;
6180
6303
  this.resolveSessionMode = options.resolveSessionMode;
6181
6304
  this.resolveSessionModel = options.resolveSessionModel;
6182
6305
  this.resolveSessionConfig = options.resolveSessionConfig;
6183
6306
  this.logger = resolveLogger(options.logger);
6307
+ this.customSection = options.customSection;
6184
6308
  this.permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS;
6185
6309
  if ((this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel || this.resolveSessionConfig) && (!Number.isFinite(this.permissionTimeoutMs) || this.permissionTimeoutMs <= 0)) {
6186
6310
  throw new ValidationError(`permissionTimeoutMs must be a positive finite number, got ${options.permissionTimeoutMs}`);
@@ -6212,7 +6336,8 @@ var ACPClientAdapter = class extends SimpleAdapter {
6212
6336
  this.systemPrompt = renderSystemPrompt({
6213
6337
  agentName,
6214
6338
  agentDescription,
6215
- includeBaseInstructions: false
6339
+ includeBaseInstructions: false,
6340
+ customSection: this.customSection
6216
6341
  });
6217
6342
  await this.ensureConnection();
6218
6343
  }
@@ -6228,6 +6353,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6228
6353
  let client = null;
6229
6354
  let sessionId;
6230
6355
  let generation = 0;
6356
+ await this.onAcpTurnStarted(message, tools, context);
6231
6357
  try {
6232
6358
  const ensured = await this.ensureConnection();
6233
6359
  connection = ensured.connection;
@@ -6238,6 +6364,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6238
6364
  }
6239
6365
  sessionId = await this.getOrCreateSession(context.roomId, connection, generation, client);
6240
6366
  const sessionKey = this.sessionKey(generation, sessionId);
6367
+ await this.onAcpSessionReady(message, tools, context, sessionId);
6241
6368
  client.beginSession(sessionId);
6242
6369
  const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
6243
6370
  const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
@@ -6245,13 +6372,17 @@ var ACPClientAdapter = class extends SimpleAdapter {
6245
6372
 
6246
6373
  ${messageWithContext}`;
6247
6374
  this.bootstrappedSessions.add(sessionKey);
6248
- const response = await withTimeout(connection.prompt({
6249
- sessionId,
6250
- prompt: [{
6251
- type: "text",
6252
- text: promptText
6253
- }]
6254
- }), this.turnTimeoutMs, () => new AcpTurnTimeoutError());
6375
+ const response = await withTimeout(
6376
+ this.raceAgainstConnectionRetirement(connection, connection.prompt({
6377
+ sessionId,
6378
+ prompt: [{
6379
+ type: "text",
6380
+ text: promptText
6381
+ }]
6382
+ })),
6383
+ this.turnTimeoutMs,
6384
+ () => new AcpTurnTimeoutError()
6385
+ );
6255
6386
  await this.flushChunks({
6256
6387
  client,
6257
6388
  tools,
@@ -6292,15 +6423,28 @@ ${messageWithContext}`;
6292
6423
  }
6293
6424
  }
6294
6425
  }
6295
- const acpError = asAcpJsonRpcError(error);
6426
+ const configError = error instanceof AcpSessionConfigError ? error : void 0;
6427
+ const acpError = configError ? void 0 : asAcpJsonRpcError(error);
6296
6428
  await reportTurnFailure(
6297
6429
  tools,
6298
- isTimeout ? agentFailure(this.provider, "ACP turn timed out.", FAILURE_CODE_TIMEOUT) : acpError ? agentFailure(this.provider, acpError.message, String(acpError.code), acpError.data) : agentFailure(this.provider, asErrorMessage(error)),
6430
+ isTimeout ? agentFailure(this.provider, "ACP turn timed out.", FAILURE_CODE_TIMEOUT) : configError ? configError.toAgentFailure() : acpError ? agentFailure(this.provider, acpError.message, String(acpError.code), acpError.data) : agentFailure(this.provider, asErrorMessage(error)),
6299
6431
  this.logger,
6300
- { roomId: context.roomId, sessionId }
6432
+ {
6433
+ roomId: context.roomId,
6434
+ sessionId: configError?.sessionId ?? sessionId,
6435
+ ...configError ? { optionId: configError.optionId, selectedValue: configError.selectedValue } : {}
6436
+ }
6301
6437
  );
6438
+ } finally {
6439
+ await this.onAcpTurnFinished(message, tools, context);
6302
6440
  }
6303
6441
  }
6442
+ async onAcpTurnStarted(_message, _tools, _context) {
6443
+ }
6444
+ async onAcpTurnFinished(_message, _tools, _context) {
6445
+ }
6446
+ async onAcpSessionReady(_message, _tools, _context, _sessionId) {
6447
+ }
6304
6448
  // Best-effort: tells the agent to stop working on a turn Band has already
6305
6449
  // given up waiting for (the ACP client has no way to force it), evicts the
6306
6450
  // session so the room's next turn re-establishes rather than reuses it
@@ -6316,17 +6460,12 @@ ${messageWithContext}`;
6316
6460
  // would risk blocking this room's turn lock forever on the very process
6317
6461
  // that just proved it can hang.
6318
6462
  async abandonTimedOutTurn(connection, sessionId, generation) {
6319
- const key = this.sessionKey(generation, sessionId);
6320
- this.activeSessions.delete(key);
6321
- this.abandonedSessions.add(key);
6322
- const owner = [...this.roomToSession.entries()].find(([, value]) => value.sessionId === sessionId && value.generation === generation);
6323
- if (owner) {
6324
- this.unlinkOwner(owner[0], owner[1]);
6325
- }
6326
- abandon(
6327
- () => connection.cancel({ sessionId }),
6328
- (error) => this.safeWarn("acp_client.cancel_failed", { sessionId, error: asErrorMessage(error) })
6329
- );
6463
+ this.evictAbandonedSession(sessionId, generation, connection, () => {
6464
+ const owner = [...this.roomToSession.entries()].find(([, value]) => value.sessionId === sessionId && value.generation === generation);
6465
+ if (owner) {
6466
+ this.unlinkOwner(owner[0], owner[1]);
6467
+ }
6468
+ });
6330
6469
  }
6331
6470
  // A per-room async mutex: `fn` for a given `roomId` never overlaps another
6332
6471
  // call for that same room, while different rooms stay fully concurrent.
@@ -6452,6 +6591,11 @@ ${messageWithContext}`;
6452
6591
  void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
6453
6592
  return Promise.race([operation, closedRejection]);
6454
6593
  }
6594
+ raceAgainstConnectionRetirement(connection, operation) {
6595
+ const retirement = this.connectionRetirements.get(connection) ?? createConnectionRetirement();
6596
+ this.connectionRetirements.set(connection, retirement);
6597
+ return Promise.race([operation, retirement.promise]);
6598
+ }
6455
6599
  unlinkRoom(roomId) {
6456
6600
  const owner = this.roomToSession.get(roomId);
6457
6601
  if (owner) {
@@ -6470,6 +6614,9 @@ ${messageWithContext}`;
6470
6614
  sessionKey(generation, sessionId) {
6471
6615
  return `${generation}:${sessionId}`;
6472
6616
  }
6617
+ roomIdForSession(sessionId) {
6618
+ return [...this.roomToSession.entries()].find(([, owner]) => owner.sessionId === sessionId)?.[0];
6619
+ }
6473
6620
  async ensureConnection() {
6474
6621
  if (this.connection && !this.connection.signal.aborted) {
6475
6622
  return { connection: this.connection, generation: this.connectionGeneration };
@@ -6506,7 +6653,10 @@ ${messageWithContext}`;
6506
6653
  throw new Error(CONNECTION_ATTEMPT_SUPERSEDED_ERROR);
6507
6654
  }
6508
6655
  const owner = { generation: -1 };
6509
- const client = new BandACPClient((params) => this.routePermissionRequest(params, owner.generation));
6656
+ const client = new BandACPClient(
6657
+ (params) => this.routePermissionRequest(params, owner.generation),
6658
+ this.extensionHandler
6659
+ );
6510
6660
  handle = await (this.connectionFactory ? this.connectionFactory(client, {
6511
6661
  command: this.command,
6512
6662
  cwd: this.cwd,
@@ -6596,7 +6746,7 @@ ${messageWithContext}`;
6596
6746
  this.activeSessions.add(restoredKey);
6597
6747
  this.bootstrappedSessions.add(restoredKey);
6598
6748
  await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
6599
- await this.configureSessionConfig(roomId, existingSessionId, restored.configOptions, connection);
6749
+ await this.configureSessionConfig(roomId, existingSessionId, restored.configOptions, connection, connectionGeneration, client);
6600
6750
  if (!this.resolveSessionConfig) {
6601
6751
  await this.configureSessionModel(roomId, existingSessionId, restored.configOptions, connection);
6602
6752
  }
@@ -6610,13 +6760,13 @@ ${messageWithContext}`;
6610
6760
  this.linkOrAbandon(roomId, created.sessionId, generation, connectionGeneration, client);
6611
6761
  this.activeSessions.add(this.sessionKey(connectionGeneration, created.sessionId));
6612
6762
  await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
6613
- await this.configureSessionConfig(roomId, created.sessionId, created.configOptions, connection);
6763
+ await this.configureSessionConfig(roomId, created.sessionId, created.configOptions, connection, connectionGeneration, client);
6614
6764
  if (!this.resolveSessionConfig) {
6615
6765
  await this.configureSessionModel(roomId, created.sessionId, created.configOptions, connection);
6616
6766
  }
6617
6767
  return created.sessionId;
6618
6768
  }
6619
- async configureSessionConfig(roomId, sessionId, configOptions, connection) {
6769
+ async configureSessionConfig(roomId, sessionId, configOptions, connection, connectionGeneration, client) {
6620
6770
  if (!this.resolveSessionConfig || !Array.isArray(configOptions) || configOptions.length === 0) {
6621
6771
  return;
6622
6772
  }
@@ -6629,35 +6779,76 @@ ${messageWithContext}`;
6629
6779
  if (!selections) {
6630
6780
  return;
6631
6781
  }
6632
- for (const option of advertisedOptions) {
6633
- const selectedValue = selections[option.id];
6634
- if (selectedValue === void 0 || selectedValue === option.currentValue || !isSessionConfigSelect(option)) {
6635
- continue;
6636
- }
6637
- const availableValues = flattenConfigSelectOptions(option.options).map((entry) => entry.value);
6638
- if (!availableValues.includes(selectedValue)) {
6639
- this.safeWarn("resolveSessionConfig selected a value this session does not advertise", {
6640
- sessionId,
6641
- configId: option.id,
6642
- selectedValue,
6643
- availableValues
6644
- });
6645
- continue;
6646
- }
6647
- try {
6648
- await withTimeout(
6649
- connection.setSessionConfigOption({ sessionId, configId: option.id, value: selectedValue }),
6650
- SET_SESSION_CONFIG_TIMEOUT_MS,
6651
- `setSessionConfigOption did not respond within ${SET_SESSION_CONFIG_TIMEOUT_MS}ms`
6652
- );
6653
- } catch (error) {
6654
- this.safeWarn("failed to switch session config option", {
6655
- sessionId,
6656
- configId: option.id,
6657
- selectedValue,
6658
- error: String(error)
6659
- });
6782
+ try {
6783
+ await applySessionConfigSelections({
6784
+ provider: this.provider,
6785
+ sessionId,
6786
+ catalog: advertisedOptions,
6787
+ selections,
6788
+ setOption: (params) => connection.setSessionConfigOption(params),
6789
+ timeoutMs: SET_SESSION_CONFIG_TIMEOUT_MS
6790
+ });
6791
+ } catch (error) {
6792
+ this.abandonFailedConfigSession(
6793
+ roomId,
6794
+ sessionId,
6795
+ connectionGeneration,
6796
+ client,
6797
+ connection,
6798
+ error instanceof AcpSessionConfigError && error.timedOut
6799
+ );
6800
+ throw error;
6801
+ }
6802
+ }
6803
+ // A config failure mid-establish must not leave a half-applied session
6804
+ // active for the room: the next turn needs a fresh `newSession` catalog.
6805
+ abandonFailedConfigSession(roomId, sessionId, connectionGeneration, client, connection, retireConnection) {
6806
+ const key = this.sessionKey(connectionGeneration, sessionId);
6807
+ this.bootstrappedSessions.delete(key);
6808
+ client.resetChunks(sessionId);
6809
+ this.evictAbandonedSession(sessionId, connectionGeneration, connection, () => {
6810
+ const owner = this.roomToSession.get(roomId);
6811
+ if (owner && owner.sessionId === sessionId && owner.generation === connectionGeneration) {
6812
+ this.unlinkOwner(roomId, owner);
6660
6813
  }
6814
+ });
6815
+ if (retireConnection) {
6816
+ this.retireConnection(connection, connectionGeneration);
6817
+ }
6818
+ }
6819
+ // Common half of timeout and config-failure abandon: mark the session
6820
+ // unusable for restore, unlink ownership, and best-effort cancel.
6821
+ evictAbandonedSession(sessionId, connectionGeneration, connection, unlink) {
6822
+ const key = this.sessionKey(connectionGeneration, sessionId);
6823
+ const wasActive = this.activeSessions.delete(key);
6824
+ if (wasActive) {
6825
+ this.abandonedSessions.add(key);
6826
+ }
6827
+ unlink();
6828
+ abandon(
6829
+ () => connection.cancel({ sessionId }),
6830
+ (error) => this.safeWarn("acp_client.cancel_failed", { sessionId, error: asErrorMessage(error) })
6831
+ );
6832
+ }
6833
+ // A timed-out config RPC means this transport has already failed to answer
6834
+ // one request. Retire it so the next turn cannot wait forever on another.
6835
+ retireConnection(connection, generation) {
6836
+ if (this.connection !== connection || this.connectionGeneration !== generation) {
6837
+ return;
6838
+ }
6839
+ const handle = this.connectionHandle;
6840
+ this.connectionGeneration++;
6841
+ this.connection = null;
6842
+ this.connectionHandle = null;
6843
+ this.connectionState = null;
6844
+ this.client = null;
6845
+ this.pruneConnectionGeneration(generation);
6846
+ this.connectionRetirements.get(connection)?.reject(new Error("ACP connection retired after a config timeout"));
6847
+ if (handle) {
6848
+ abandon(
6849
+ () => handle.stop(),
6850
+ (error) => this.safeWarn("acp_client.handle_stop_after_config_timeout", { error: asErrorMessage(error) })
6851
+ );
6661
6852
  }
6662
6853
  }
6663
6854
  // The single gate an establishment must pass before it's allowed to claim
@@ -7271,39 +7462,12 @@ async function createTcpConnection(client, endpoint, signal) {
7271
7462
  };
7272
7463
  }
7273
7464
  var MODEL_CONFIG_OPTION_KEY = "model";
7274
- function isSessionConfigSelect(option) {
7275
- return option?.type === "select";
7276
- }
7277
7465
  function isModelConfigOption(option) {
7278
7466
  return isSessionConfigSelect(option) && option.category === MODEL_CONFIG_OPTION_KEY;
7279
7467
  }
7280
7468
  function isModelConfigOptionById(option) {
7281
7469
  return isSessionConfigSelect(option) && option.id === MODEL_CONFIG_OPTION_KEY;
7282
7470
  }
7283
- function flattenConfigSelectOptions(options) {
7284
- if (!Array.isArray(options)) {
7285
- return [];
7286
- }
7287
- return options.flatMap((entry) => {
7288
- if (!asOptionalRecord(entry)) {
7289
- return [];
7290
- }
7291
- if ("group" in entry) {
7292
- return Array.isArray(entry.options) ? entry.options : [];
7293
- }
7294
- return [entry];
7295
- });
7296
- }
7297
- function isAcpErrorResponse(error) {
7298
- return typeof error === "object" && error !== null && typeof error.code === "number" && typeof error.message === "string";
7299
- }
7300
- function asAcpJsonRpcError(error) {
7301
- if (isAcpErrorResponse(error)) {
7302
- return error;
7303
- }
7304
- const nested = asOptionalRecord(error)?.error;
7305
- return isAcpErrorResponse(nested) ? nested : void 0;
7306
- }
7307
7471
 
7308
7472
  // src/adapters/acp/BandACPServerAdapter.ts
7309
7473
  import { randomUUID as randomUUID2 } from "crypto";
@@ -8127,6 +8291,358 @@ var CopilotACPAdapter = class extends ACPClientAdapter {
8127
8291
  }
8128
8292
  };
8129
8293
 
8294
+ // src/adapters/cursor-acp/CursorACPAdapter.ts
8295
+ var DEFAULT_CURSOR_ACP_COMMAND = ["agent", "acp"];
8296
+ var DEFAULT_CURSOR_DECISION_TIMEOUT_MS = 3e5;
8297
+ var DEFAULT_CURSOR_MAX_PENDING_DECISIONS = 10;
8298
+ var CursorExtensions = class {
8299
+ adapter = null;
8300
+ todosBySession = /* @__PURE__ */ new Map();
8301
+ bind(adapter) {
8302
+ this.adapter = adapter;
8303
+ }
8304
+ async resolvePermission(request, signal) {
8305
+ return this.adapter?.resolveCursorPermission(request, signal);
8306
+ }
8307
+ extensionSessionId() {
8308
+ return this.adapter?.extensionSessionId() ?? null;
8309
+ }
8310
+ async extMethod(method, params, context) {
8311
+ return this.adapter?.resolveExtension(method, params, context.sessionId) ?? null;
8312
+ }
8313
+ async extNotification(method, params, context) {
8314
+ const sessionId = context.sessionId ?? this.extensionSessionId();
8315
+ if (!sessionId) {
8316
+ return;
8317
+ }
8318
+ if (method === "cursor/update_todos") {
8319
+ const content = this.updateTodos(sessionId, params);
8320
+ if (!content) {
8321
+ return;
8322
+ }
8323
+ return [{ chunkType: "plan", content, metadata: { cursor_todos: true }, streamed: false }];
8324
+ }
8325
+ if (method === "cursor/task") {
8326
+ const description = stringValue(params.description);
8327
+ if (!description) {
8328
+ return [];
8329
+ }
8330
+ const subagentType = stringValue(params.subagentType) ?? "unspecified";
8331
+ const model = stringValue(params.model);
8332
+ const suffix = model ? ` (${model})` : "";
8333
+ return [{ chunkType: "plan", content: `[Cursor ${subagentType} task] ${description}${suffix}`, metadata: {}, streamed: false }];
8334
+ }
8335
+ if (method === "cursor/generate_image") {
8336
+ const description = stringValue(params.description);
8337
+ if (!description) {
8338
+ return [];
8339
+ }
8340
+ const filePath = stringValue(params.filePath);
8341
+ return [{ chunkType: "plan", content: `[Cursor generated image] ${description}${filePath ? ` \u2192 ${filePath}` : ""}`, metadata: {}, streamed: false }];
8342
+ }
8343
+ }
8344
+ forgetSession(sessionId) {
8345
+ this.todosBySession.delete(sessionId);
8346
+ }
8347
+ clearSessions() {
8348
+ this.todosBySession.clear();
8349
+ }
8350
+ updateTodos(sessionId, params) {
8351
+ const todos = parseTodos(params.todos);
8352
+ if (params.merge === true) {
8353
+ const current2 = this.todosBySession.get(sessionId) ?? /* @__PURE__ */ new Map();
8354
+ for (const todo of todos) {
8355
+ current2.set(todo.id, todo);
8356
+ }
8357
+ this.todosBySession.set(sessionId, current2);
8358
+ } else {
8359
+ this.todosBySession.set(sessionId, new Map(todos.map((todo) => [todo.id, todo])));
8360
+ }
8361
+ const current = this.todosBySession.get(sessionId);
8362
+ return current && current.size > 0 ? [...current.values()].map((todo) => `- [${todoMark(todo.status)}] ${todo.content}`).join("\n") : void 0;
8363
+ }
8364
+ };
8365
+ var CursorACPAdapter = class extends ACPClientAdapter {
8366
+ provider = "cursor-acp";
8367
+ approvalMode;
8368
+ questionMode;
8369
+ planMode;
8370
+ decisionTimeoutMs;
8371
+ maxPendingDecisions;
8372
+ authorizedSenders;
8373
+ decisionLogger;
8374
+ extensions;
8375
+ turns = /* @__PURE__ */ new Map();
8376
+ pending = /* @__PURE__ */ new Map();
8377
+ activeTurn = null;
8378
+ turnTail = Promise.resolve();
8379
+ constructor(options = {}) {
8380
+ const extensions = new CursorExtensions();
8381
+ validateOptions(options);
8382
+ const env = cursorEnv(options);
8383
+ super({
8384
+ ...options,
8385
+ env,
8386
+ command: options.command ?? [...DEFAULT_CURSOR_ACP_COMMAND],
8387
+ authMethod: "cursor_login",
8388
+ extensionHandler: extensions,
8389
+ resolvePermission: (request, signal) => extensions.resolvePermission(request, signal)
8390
+ });
8391
+ extensions.bind(this);
8392
+ this.extensions = extensions;
8393
+ this.approvalMode = options.approvalMode ?? "manual";
8394
+ this.questionMode = options.questionMode ?? "manual";
8395
+ this.planMode = options.planMode ?? "manual";
8396
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_CURSOR_DECISION_TIMEOUT_MS;
8397
+ this.maxPendingDecisions = options.maxPendingDecisions ?? DEFAULT_CURSOR_MAX_PENDING_DECISIONS;
8398
+ this.authorizedSenders = options.decisionAuthorizedSenders ? new Set(options.decisionAuthorizedSenders) : null;
8399
+ this.decisionLogger = resolveLogger(options.logger);
8400
+ }
8401
+ async onMessage(message, tools, history, participantsMessage, contactsMessage, context) {
8402
+ if (await this.handleControl(message, tools, context.roomId)) {
8403
+ return;
8404
+ }
8405
+ await this.withCursorTurnLock(() => super.onMessage(message, tools, history, participantsMessage, contactsMessage, context));
8406
+ }
8407
+ async onAcpTurnStarted(message, tools, context) {
8408
+ const turn = { messageId: message.id, roomId: context.roomId, tools, requesterId: message.senderId };
8409
+ this.turns.set(context.roomId, turn);
8410
+ this.activeTurn = turn;
8411
+ }
8412
+ async onAcpSessionReady(message, _tools, context, sessionId) {
8413
+ const turn = this.turns.get(context.roomId);
8414
+ if (turn?.messageId === message.id) {
8415
+ turn.sessionId = sessionId;
8416
+ }
8417
+ }
8418
+ async onAcpTurnFinished(message, _tools, context) {
8419
+ const turn = this.turns.get(context.roomId);
8420
+ if (turn?.messageId === message.id) {
8421
+ this.turns.delete(context.roomId);
8422
+ this.cancelRoom(context.roomId);
8423
+ }
8424
+ if (this.activeTurn?.messageId === message.id) {
8425
+ this.activeTurn = null;
8426
+ }
8427
+ }
8428
+ async onCleanup(roomId) {
8429
+ const sessionId = this.turns.get(roomId)?.sessionId;
8430
+ this.cancelRoom(roomId);
8431
+ this.turns.delete(roomId);
8432
+ if (this.activeTurn?.roomId === roomId) {
8433
+ this.activeTurn = null;
8434
+ }
8435
+ await super.onCleanup(roomId);
8436
+ if (sessionId) {
8437
+ this.extensions.forgetSession(sessionId);
8438
+ }
8439
+ }
8440
+ async stop() {
8441
+ for (const decision of this.pending.values()) {
8442
+ decision.resolve(void 0);
8443
+ }
8444
+ this.pending.clear();
8445
+ this.turns.clear();
8446
+ this.activeTurn = null;
8447
+ this.extensions.clearSessions();
8448
+ await super.stop();
8449
+ }
8450
+ async resolveExtension(method, params, sessionId) {
8451
+ const roomId = sessionId ? this.roomIdForSession(sessionId) : this.activeTurn?.roomId;
8452
+ const turn = roomId ? this.turns.get(roomId) : void 0;
8453
+ if (!turn || sessionId && turn.sessionId !== sessionId) {
8454
+ return { outcome: { outcome: "cancelled" } };
8455
+ }
8456
+ if (method === "cursor/ask_question") {
8457
+ return this.resolveQuestion(turn.roomId, turn, params);
8458
+ }
8459
+ if (method === "cursor/create_plan") {
8460
+ return this.resolvePlan(turn.roomId, turn, params);
8461
+ }
8462
+ return {};
8463
+ }
8464
+ async resolveCursorPermission(request, signal) {
8465
+ if (this.approvalMode === "autoAccept") {
8466
+ return allowOption(request.options)?.optionId;
8467
+ }
8468
+ if (this.approvalMode === "autoDecline") {
8469
+ return void 0;
8470
+ }
8471
+ const turn = this.turns.get(request.roomId);
8472
+ if (!turn) {
8473
+ return void 0;
8474
+ }
8475
+ const options = request.options.map((option) => option.optionId);
8476
+ const token = await this.waitForDecision("permission", request.roomId, turn, /* @__PURE__ */ new Map([["permission", options]]), /* @__PURE__ */ new Set(), `Cursor needs permission. Reply \`/cursor select {token} option-id\` or \`/cursor deny {token}\`.`, signal);
8477
+ return typeof token === "string" && options.includes(token) ? token : void 0;
8478
+ }
8479
+ extensionSessionId() {
8480
+ return this.activeTurn?.sessionId ?? null;
8481
+ }
8482
+ async resolveQuestion(roomId, turn, params) {
8483
+ const questions = questionChoices(params.questions);
8484
+ if (questions.choices.size === 0) {
8485
+ return { outcome: { outcome: "cancelled" } };
8486
+ }
8487
+ if (this.questionMode === "autoCancel") {
8488
+ return { outcome: { outcome: "cancelled" } };
8489
+ }
8490
+ if (this.questionMode === "autoFirst") {
8491
+ return answered(Object.fromEntries([...questions.choices].map(([id, options]) => [id, [options[0]]])));
8492
+ }
8493
+ const result = await this.waitForDecision("question", roomId, turn, questions.choices, questions.multiSelect, "Cursor needs input. Reply `/cursor answer {token} question-id=option-id[,option-id] ...`.");
8494
+ return isRecord2(result) ? result : { outcome: { outcome: "cancelled" } };
8495
+ }
8496
+ async resolvePlan(roomId, turn, params) {
8497
+ if (this.planMode === "autoAccept") {
8498
+ return { outcome: { outcome: "accepted" } };
8499
+ }
8500
+ if (this.planMode === "autoDecline") {
8501
+ return { outcome: { outcome: "rejected" } };
8502
+ }
8503
+ const title = stringValue(params.title) ?? "Cursor plan";
8504
+ const result = await this.waitForDecision("plan", roomId, turn, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set(), `${title} needs approval. Reply \`/cursor accept {token}\` or \`/cursor reject {token}\`.`);
8505
+ return isRecord2(result) ? result : { outcome: { outcome: "cancelled" } };
8506
+ }
8507
+ async waitForDecision(kind, roomId, turn, choices, multiSelect, prompt, signal) {
8508
+ if (this.pending.size >= this.maxPendingDecisions) {
8509
+ this.pending.values().next().value?.resolve(void 0);
8510
+ }
8511
+ const token = crypto.randomUUID().slice(0, 8);
8512
+ return new Promise((resolve) => {
8513
+ const timer = setTimeout(() => settle(void 0), this.decisionTimeoutMs);
8514
+ const abort = () => settle(void 0);
8515
+ const settle = (value) => {
8516
+ clearTimeout(timer);
8517
+ signal?.removeEventListener("abort", abort);
8518
+ this.pending.delete(token);
8519
+ resolve(value);
8520
+ };
8521
+ this.pending.set(token, { kind, roomId, choices, multiSelect, resolve: settle });
8522
+ signal?.addEventListener("abort", abort, { once: true });
8523
+ void turn.tools.sendMessage(prompt.replaceAll("{token}", token), [turn.requesterId]).catch((error) => {
8524
+ this.decisionLogger.warn("cursor_acp.decision_prompt_delivery_failed", { roomId, kind, error: String(error) });
8525
+ settle(void 0);
8526
+ });
8527
+ });
8528
+ }
8529
+ async handleControl(message, tools, roomId) {
8530
+ const words = message.content.trim().split(/\s+/);
8531
+ if (words[0]?.toLowerCase() !== "/cursor") {
8532
+ return false;
8533
+ }
8534
+ if (words.length === 1 || words[1]?.toLowerCase() === "decisions") {
8535
+ const entries = [...this.pending.entries()].filter(([, decision2]) => decision2.roomId === roomId).map(([token2, decision2]) => `\`${token2}\` (${decision2.kind})`);
8536
+ await tools.sendMessage(`Pending Cursor decisions: ${entries.join(", ") || "none"}`);
8537
+ return true;
8538
+ }
8539
+ const [_, action, token, ...args] = words;
8540
+ const decision = token ? this.pending.get(token) : void 0;
8541
+ if (!decision || decision.roomId !== roomId) {
8542
+ await tools.sendMessage(`Cursor decision \`${token ?? ""}\` is not pending.`);
8543
+ return true;
8544
+ }
8545
+ if (this.authorizedSenders && !this.authorizedSenders.has(message.senderId)) {
8546
+ await tools.sendMessage("You are not authorized to resolve Cursor decisions.");
8547
+ return true;
8548
+ }
8549
+ const result = commandResult(action ?? "", args, decision);
8550
+ if (result === null) {
8551
+ await tools.sendMessage(`That command is not valid for Cursor ${decision.kind} decision \`${token}\`.`);
8552
+ return true;
8553
+ }
8554
+ decision.resolve(result);
8555
+ await tools.sendMessage(`Cursor ${decision.kind} decision \`${token}\` resolved.`);
8556
+ return true;
8557
+ }
8558
+ cancelRoom(roomId) {
8559
+ for (const [token, decision] of this.pending) {
8560
+ if (decision.roomId === roomId) {
8561
+ decision.resolve(void 0);
8562
+ this.pending.delete(token);
8563
+ }
8564
+ }
8565
+ }
8566
+ async withCursorTurnLock(run) {
8567
+ const queued = this.turnTail.then(run, run);
8568
+ this.turnTail = queued.then(() => void 0, () => void 0);
8569
+ return queued;
8570
+ }
8571
+ };
8572
+ function cursorEnv(options) {
8573
+ const env = { ...options.env };
8574
+ if (options.apiKey) env.CURSOR_API_KEY ??= options.apiKey;
8575
+ if (options.authToken) env.CURSOR_AUTH_TOKEN ??= options.authToken;
8576
+ return Object.keys(env).length > 0 ? env : void 0;
8577
+ }
8578
+ function validateOptions(options) {
8579
+ if (options.apiKey && options.authToken) throw new Error("set either apiKey or authToken, not both");
8580
+ if (Array.isArray(options.command) && options.command.length === 0) throw new Error("Cursor ACP command must not be empty");
8581
+ if (options.decisionTimeoutMs !== void 0 && (!Number.isFinite(options.decisionTimeoutMs) || options.decisionTimeoutMs <= 0)) throw new Error("decisionTimeoutMs must be a positive finite number");
8582
+ if (options.maxPendingDecisions !== void 0 && (!Number.isInteger(options.maxPendingDecisions) || options.maxPendingDecisions <= 0)) throw new Error("maxPendingDecisions must be a positive integer");
8583
+ }
8584
+ function allowOption(options) {
8585
+ return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always");
8586
+ }
8587
+ function questionChoices(value) {
8588
+ const choices = /* @__PURE__ */ new Map();
8589
+ const multiSelect = /* @__PURE__ */ new Set();
8590
+ if (!Array.isArray(value)) return { choices, multiSelect };
8591
+ for (const question of value) {
8592
+ if (!isRecord2(question) || typeof question.id !== "string" || !Array.isArray(question.options)) continue;
8593
+ const options = question.options.filter(isRecord2).map((option) => stringValue(option.id)).filter((id) => !!id);
8594
+ if (options.length === 0) continue;
8595
+ choices.set(question.id, options);
8596
+ if (question.allowMultiple === true) multiSelect.add(question.id);
8597
+ }
8598
+ return { choices, multiSelect };
8599
+ }
8600
+ function commandResult(action, args, decision) {
8601
+ if (decision.kind === "permission") return action === "deny" ? void 0 : action === "select" && args.length === 1 && decision.choices.get("permission")?.includes(args[0] ?? "") ? args[0] : null;
8602
+ if (decision.kind === "plan") return action === "accept" ? { outcome: { outcome: "accepted" } } : action === "reject" ? { outcome: { outcome: "rejected" } } : null;
8603
+ if (action !== "answer") return null;
8604
+ const selected = {};
8605
+ for (const argument of args) {
8606
+ const [id, raw] = argument.split("=", 2);
8607
+ const values = raw?.split(",") ?? [];
8608
+ const offered = id ? decision.choices.get(id) : void 0;
8609
+ if (!id || !offered || selected[id] || values.length === 0 || values.length > 1 && !decision.multiSelect.has(id) || values.some((value) => !offered.includes(value))) return null;
8610
+ selected[id] = values;
8611
+ }
8612
+ return Object.keys(selected).length === decision.choices.size ? answered(selected) : null;
8613
+ }
8614
+ function answered(selected) {
8615
+ return { outcome: { outcome: "answered", answers: Object.entries(selected).map(([questionId, selectedOptionIds]) => ({ questionId, selectedOptionIds })) } };
8616
+ }
8617
+ function parseTodos(value) {
8618
+ if (!Array.isArray(value)) return [];
8619
+ return value.flatMap((todo) => {
8620
+ if (!isRecord2(todo)) return [];
8621
+ const id = stringValue(todo.id);
8622
+ const content = stringValue(todo.content);
8623
+ const status = stringValue(todo.status);
8624
+ return id && content && status ? [{ id, content, status }] : [];
8625
+ });
8626
+ }
8627
+ function todoMark(status) {
8628
+ switch (status) {
8629
+ case "completed":
8630
+ return "x";
8631
+ case "in_progress":
8632
+ return "~";
8633
+ case "cancelled":
8634
+ return "-";
8635
+ default:
8636
+ return " ";
8637
+ }
8638
+ }
8639
+ function isRecord2(value) {
8640
+ return !!value && typeof value === "object" && !Array.isArray(value);
8641
+ }
8642
+ function stringValue(value) {
8643
+ return typeof value === "string" && value.length > 0 ? value : void 0;
8644
+ }
8645
+
8130
8646
  export {
8131
8647
  GenericAdapter,
8132
8648
  CodexJsonRpcError,
@@ -8153,11 +8669,17 @@ export {
8153
8669
  HttpOpencodeClient,
8154
8670
  OpencodeAdapter,
8155
8671
  ClaudeSDKAdapter,
8672
+ FAILURE_CODE_SESSION_CONFIG,
8673
+ MISSING_CONFIG_OPTIONS_REASON,
8674
+ AcpSessionConfigError,
8675
+ applySessionConfigSelections,
8156
8676
  ACPClientAdapter,
8157
8677
  BandACPServerAdapter,
8158
8678
  ACPServer,
8159
8679
  DEFAULT_OMP_ACP_COMMAND,
8160
8680
  OmpACPAdapter,
8161
8681
  DEFAULT_COPILOT_ACP_COMMAND,
8162
- CopilotACPAdapter
8682
+ CopilotACPAdapter,
8683
+ DEFAULT_CURSOR_ACP_COMMAND,
8684
+ CursorACPAdapter
8163
8685
  };