@band-ai/sdk 0.4.2 → 0.4.4

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.
@@ -483,15 +483,20 @@ function systemUpdateParts(participantsMessage, contactsMessage) {
483
483
  }
484
484
  function buildConversationPrompt(options) {
485
485
  const parts = [];
486
- if (options.isSessionBootstrap && options.history.length > 0) {
487
- const historyText = options.history.raw.slice(-(options.maxHistoryMessages ?? 50)).map(formatHistoryLine).join("\n");
488
- parts.push(`${options.historyHeader}
486
+ if (options.isSessionBootstrap) {
487
+ const historyText = options.history.raw.slice(-(options.maxHistoryMessages ?? 50)).filter(isTextHistoryEntry).map(formatHistoryLine).join("\n");
488
+ if (historyText) {
489
+ parts.push(`${options.historyHeader}
489
490
  ${historyText}`);
491
+ }
490
492
  }
491
493
  parts.push(...systemUpdateParts(options.participantsMessage, options.contactsMessage));
492
494
  parts.push(options.currentMessage);
493
495
  return parts.join("\n\n");
494
496
  }
497
+ function isTextHistoryEntry(entry) {
498
+ return entry.message_type === void 0 || entry.message_type === "text";
499
+ }
495
500
  function formatHistoryLine(entry) {
496
501
  const sender = String(entry.sender_name ?? entry.sender_type ?? "Unknown");
497
502
  const content = String(entry.content ?? "");
@@ -5926,74 +5931,17 @@ import { spawn as spawn2 } from "child_process";
5926
5931
  import { createConnection } from "net";
5927
5932
  import { Duplex, Readable, Writable } from "stream";
5928
5933
 
5929
- // src/adapters/acp/types.ts
5930
- var DEFAULT_ACP_SERVER_MODES = [
5931
- {
5932
- id: "default",
5933
- name: "Default",
5934
- description: "General-purpose chat mode"
5935
- },
5936
- {
5937
- id: "code",
5938
- name: "Code",
5939
- description: "Route prompts toward coding peers when available"
5940
- }
5941
- ];
5942
- function createPendingPrompt(sessionId) {
5943
- let markDone = () => void 0;
5944
- const done = new Promise((resolve) => {
5945
- markDone = resolve;
5946
- });
5947
- return {
5948
- sessionId,
5949
- done,
5950
- markDone,
5951
- terminalMessageSeen: false,
5952
- completionTimer: null
5953
- };
5954
- }
5955
- function choosePermissionOption(options) {
5956
- if (options.length === 0) {
5957
- return null;
5958
- }
5959
- return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options[0];
5960
- }
5961
- function asJsonSafe(value) {
5962
- if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5963
- return value;
5964
- }
5965
- if (Array.isArray(value)) {
5966
- return value.map((item) => asJsonSafe(item));
5967
- }
5968
- if (typeof value === "object") {
5969
- if ("model_dump" in value && typeof value.model_dump === "function") {
5970
- return asJsonSafe(value.model_dump());
5971
- }
5972
- if ("toJSON" in value && typeof value.toJSON === "function") {
5973
- return asJsonSafe(value.toJSON());
5974
- }
5975
- return Object.fromEntries(
5976
- Object.entries(value).map(([key, item]) => [key, asJsonSafe(item)])
5977
- );
5978
- }
5979
- return String(value);
5980
- }
5981
- function normalizeMcpServers(mcpServers) {
5982
- if (!mcpServers) {
5983
- return [];
5984
- }
5985
- return mcpServers.map((server) => asJsonSafe(server)).filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
5986
- }
5987
-
5988
5934
  // src/adapters/acp/client.ts
5989
5935
  var BandACPClient = class {
5990
5936
  sessionChunks = /* @__PURE__ */ new Map();
5991
5937
  permissionHandler;
5938
+ extensionHandler;
5992
5939
  // The handler is connection-scoped and required at construction, so it is
5993
5940
  // already in place before the agent process is spawned: there is no window
5994
5941
  // in which a `session/request_permission` has nowhere to go.
5995
- constructor(permissionHandler) {
5942
+ constructor(permissionHandler, extensionHandler) {
5996
5943
  this.permissionHandler = permissionHandler;
5944
+ this.extensionHandler = extensionHandler;
5997
5945
  }
5998
5946
  beginSession(sessionId) {
5999
5947
  this.sessionChunks.set(sessionId, []);
@@ -6038,62 +5986,26 @@ var BandACPClient = class {
6038
5986
  return chunks;
6039
5987
  }
6040
5988
  async extMethod(method, params) {
6041
- if (method === "cursor/ask_question") {
6042
- const options = Array.isArray(params.options) ? params.options : [];
6043
- const selected = choosePermissionOption(
6044
- options.filter((option) => !!option && typeof option === "object")
6045
- );
6046
- if (!selected) {
6047
- return {
6048
- outcome: {
6049
- type: "cancelled"
6050
- }
6051
- };
6052
- }
6053
- return {
6054
- outcome: {
6055
- type: "selected",
6056
- optionId: selected.optionId
6057
- }
6058
- };
6059
- }
6060
- if (method === "cursor/create_plan") {
6061
- return {
6062
- outcome: {
6063
- type: "approved"
6064
- }
6065
- };
6066
- }
6067
- return {};
5989
+ const result = await this.extensionHandler?.extMethod?.(
5990
+ method,
5991
+ params,
5992
+ { sessionId: sessionIdFrom(params) }
5993
+ );
5994
+ return result ?? {};
6068
5995
  }
6069
5996
  async extNotification(method, params) {
6070
- const sessionId = toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
6071
- if (!sessionId) {
5997
+ const sessionId = sessionIdFrom(params);
5998
+ const chunks = await this.extensionHandler?.extNotification?.(
5999
+ method,
6000
+ params,
6001
+ { sessionId }
6002
+ );
6003
+ const targetSessionId = sessionId ?? this.extensionHandler?.extensionSessionId?.() ?? null;
6004
+ if (!targetSessionId || !chunks) {
6072
6005
  return;
6073
6006
  }
6074
- if (method === "cursor/update_todos") {
6075
- const todos = Array.isArray(params.todos) ? params.todos : [];
6076
- const lines = todos.filter((todo) => !!todo && typeof todo === "object").map((todo) => `- [${todo.completed === true ? "x" : " "}] ${String(todo.content ?? "")}`).filter((line) => line.trim().length > 0);
6077
- if (lines.length > 0) {
6078
- this.appendChunk(sessionId, {
6079
- chunkType: "plan",
6080
- content: lines.join("\n"),
6081
- metadata: {},
6082
- streamed: false
6083
- });
6084
- }
6085
- return;
6086
- }
6087
- if (method === "cursor/task") {
6088
- const result = toOptionalString(params.result);
6089
- if (result) {
6090
- this.appendChunk(sessionId, {
6091
- chunkType: "text",
6092
- content: `[Task completed] ${result}`,
6093
- metadata: {},
6094
- streamed: false
6095
- });
6096
- }
6007
+ for (const chunk of chunks) {
6008
+ this.appendChunk(targetSessionId, chunk);
6097
6009
  }
6098
6010
  }
6099
6011
  appendChunk(sessionId, chunk) {
@@ -6218,6 +6130,68 @@ function extractTextFromContent(content) {
6218
6130
  function toOptionalString(value) {
6219
6131
  return typeof value === "string" && value.length > 0 ? value : null;
6220
6132
  }
6133
+ function sessionIdFrom(params) {
6134
+ return toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
6135
+ }
6136
+
6137
+ // src/adapters/acp/types.ts
6138
+ var DEFAULT_ACP_SERVER_MODES = [
6139
+ {
6140
+ id: "default",
6141
+ name: "Default",
6142
+ description: "General-purpose chat mode"
6143
+ },
6144
+ {
6145
+ id: "code",
6146
+ name: "Code",
6147
+ description: "Route prompts toward coding peers when available"
6148
+ }
6149
+ ];
6150
+ function createPendingPrompt(sessionId) {
6151
+ let markDone = () => void 0;
6152
+ const done = new Promise((resolve) => {
6153
+ markDone = resolve;
6154
+ });
6155
+ return {
6156
+ sessionId,
6157
+ done,
6158
+ markDone,
6159
+ terminalMessageSeen: false,
6160
+ completionTimer: null
6161
+ };
6162
+ }
6163
+ function choosePermissionOption(options) {
6164
+ if (options.length === 0) {
6165
+ return null;
6166
+ }
6167
+ return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always") ?? options[0];
6168
+ }
6169
+ function asJsonSafe(value) {
6170
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6171
+ return value;
6172
+ }
6173
+ if (Array.isArray(value)) {
6174
+ return value.map((item) => asJsonSafe(item));
6175
+ }
6176
+ if (typeof value === "object") {
6177
+ if ("model_dump" in value && typeof value.model_dump === "function") {
6178
+ return asJsonSafe(value.model_dump());
6179
+ }
6180
+ if ("toJSON" in value && typeof value.toJSON === "function") {
6181
+ return asJsonSafe(value.toJSON());
6182
+ }
6183
+ return Object.fromEntries(
6184
+ Object.entries(value).map(([key, item]) => [key, asJsonSafe(item)])
6185
+ );
6186
+ }
6187
+ return String(value);
6188
+ }
6189
+ function normalizeMcpServers(mcpServers) {
6190
+ if (!mcpServers) {
6191
+ return [];
6192
+ }
6193
+ return mcpServers.map((server) => asJsonSafe(server)).filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
6194
+ }
6221
6195
 
6222
6196
  // src/adapters/acp/loader.ts
6223
6197
  var acpModule = new LazyAsyncValue({
@@ -6263,6 +6237,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6263
6237
  additionalMcpTools;
6264
6238
  clientCapabilities;
6265
6239
  connectionFactory;
6240
+ extensionHandler;
6266
6241
  tcpEndpoint;
6267
6242
  // The value's `generation` is the connection generation the session was
6268
6243
  // last established/restored against. `client` is the exact BandACPClient
@@ -6327,6 +6302,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6327
6302
  this.additionalMcpTools = [...options.additionalMcpTools ?? []];
6328
6303
  this.clientCapabilities = options.clientCapabilities;
6329
6304
  this.connectionFactory = options.connectionFactory;
6305
+ this.extensionHandler = options.extensionHandler;
6330
6306
  this.tcpEndpoint = tcpEndpoint;
6331
6307
  this.resolvePermission = options.resolvePermission;
6332
6308
  this.resolveSessionMode = options.resolveSessionMode;
@@ -6382,6 +6358,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6382
6358
  let client = null;
6383
6359
  let sessionId;
6384
6360
  let generation = 0;
6361
+ await this.onAcpTurnStarted(message, tools, context);
6385
6362
  try {
6386
6363
  const ensured = await this.ensureConnection();
6387
6364
  connection = ensured.connection;
@@ -6392,6 +6369,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6392
6369
  }
6393
6370
  sessionId = await this.getOrCreateSession(context.roomId, connection, generation, client);
6394
6371
  const sessionKey = this.sessionKey(generation, sessionId);
6372
+ await this.onAcpSessionReady(message, tools, context, sessionId);
6395
6373
  client.beginSession(sessionId);
6396
6374
  const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
6397
6375
  const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
@@ -6462,8 +6440,16 @@ ${messageWithContext}`;
6462
6440
  ...configError ? { optionId: configError.optionId, selectedValue: configError.selectedValue } : {}
6463
6441
  }
6464
6442
  );
6443
+ } finally {
6444
+ await this.onAcpTurnFinished(message, tools, context);
6465
6445
  }
6466
6446
  }
6447
+ async onAcpTurnStarted(_message, _tools, _context) {
6448
+ }
6449
+ async onAcpTurnFinished(_message, _tools, _context) {
6450
+ }
6451
+ async onAcpSessionReady(_message, _tools, _context, _sessionId) {
6452
+ }
6467
6453
  // Best-effort: tells the agent to stop working on a turn Band has already
6468
6454
  // given up waiting for (the ACP client has no way to force it), evicts the
6469
6455
  // session so the room's next turn re-establishes rather than reuses it
@@ -6633,6 +6619,9 @@ ${messageWithContext}`;
6633
6619
  sessionKey(generation, sessionId) {
6634
6620
  return `${generation}:${sessionId}`;
6635
6621
  }
6622
+ roomIdForSession(sessionId) {
6623
+ return [...this.roomToSession.entries()].find(([, owner]) => owner.sessionId === sessionId)?.[0];
6624
+ }
6636
6625
  async ensureConnection() {
6637
6626
  if (this.connection && !this.connection.signal.aborted) {
6638
6627
  return { connection: this.connection, generation: this.connectionGeneration };
@@ -6669,7 +6658,10 @@ ${messageWithContext}`;
6669
6658
  throw new Error(CONNECTION_ATTEMPT_SUPERSEDED_ERROR);
6670
6659
  }
6671
6660
  const owner = { generation: -1 };
6672
- const client = new BandACPClient((params) => this.routePermissionRequest(params, owner.generation));
6661
+ const client = new BandACPClient(
6662
+ (params) => this.routePermissionRequest(params, owner.generation),
6663
+ this.extensionHandler
6664
+ );
6673
6665
  handle = await (this.connectionFactory ? this.connectionFactory(client, {
6674
6666
  command: this.command,
6675
6667
  cwd: this.cwd,
@@ -8304,6 +8296,358 @@ var CopilotACPAdapter = class extends ACPClientAdapter {
8304
8296
  }
8305
8297
  };
8306
8298
 
8299
+ // src/adapters/cursor-acp/CursorACPAdapter.ts
8300
+ var DEFAULT_CURSOR_ACP_COMMAND = ["agent", "acp"];
8301
+ var DEFAULT_CURSOR_DECISION_TIMEOUT_MS = 3e5;
8302
+ var DEFAULT_CURSOR_MAX_PENDING_DECISIONS = 10;
8303
+ var CursorExtensions = class {
8304
+ adapter = null;
8305
+ todosBySession = /* @__PURE__ */ new Map();
8306
+ bind(adapter) {
8307
+ this.adapter = adapter;
8308
+ }
8309
+ async resolvePermission(request, signal) {
8310
+ return this.adapter?.resolveCursorPermission(request, signal);
8311
+ }
8312
+ extensionSessionId() {
8313
+ return this.adapter?.extensionSessionId() ?? null;
8314
+ }
8315
+ async extMethod(method, params, context) {
8316
+ return this.adapter?.resolveExtension(method, params, context.sessionId) ?? null;
8317
+ }
8318
+ async extNotification(method, params, context) {
8319
+ const sessionId = context.sessionId ?? this.extensionSessionId();
8320
+ if (!sessionId) {
8321
+ return;
8322
+ }
8323
+ if (method === "cursor/update_todos") {
8324
+ const content = this.updateTodos(sessionId, params);
8325
+ if (!content) {
8326
+ return;
8327
+ }
8328
+ return [{ chunkType: "plan", content, metadata: { cursor_todos: true }, streamed: false }];
8329
+ }
8330
+ if (method === "cursor/task") {
8331
+ const description = stringValue(params.description);
8332
+ if (!description) {
8333
+ return [];
8334
+ }
8335
+ const subagentType = stringValue(params.subagentType) ?? "unspecified";
8336
+ const model = stringValue(params.model);
8337
+ const suffix = model ? ` (${model})` : "";
8338
+ return [{ chunkType: "plan", content: `[Cursor ${subagentType} task] ${description}${suffix}`, metadata: {}, streamed: false }];
8339
+ }
8340
+ if (method === "cursor/generate_image") {
8341
+ const description = stringValue(params.description);
8342
+ if (!description) {
8343
+ return [];
8344
+ }
8345
+ const filePath = stringValue(params.filePath);
8346
+ return [{ chunkType: "plan", content: `[Cursor generated image] ${description}${filePath ? ` \u2192 ${filePath}` : ""}`, metadata: {}, streamed: false }];
8347
+ }
8348
+ }
8349
+ forgetSession(sessionId) {
8350
+ this.todosBySession.delete(sessionId);
8351
+ }
8352
+ clearSessions() {
8353
+ this.todosBySession.clear();
8354
+ }
8355
+ updateTodos(sessionId, params) {
8356
+ const todos = parseTodos(params.todos);
8357
+ if (params.merge === true) {
8358
+ const current2 = this.todosBySession.get(sessionId) ?? /* @__PURE__ */ new Map();
8359
+ for (const todo of todos) {
8360
+ current2.set(todo.id, todo);
8361
+ }
8362
+ this.todosBySession.set(sessionId, current2);
8363
+ } else {
8364
+ this.todosBySession.set(sessionId, new Map(todos.map((todo) => [todo.id, todo])));
8365
+ }
8366
+ const current = this.todosBySession.get(sessionId);
8367
+ return current && current.size > 0 ? [...current.values()].map((todo) => `- [${todoMark(todo.status)}] ${todo.content}`).join("\n") : void 0;
8368
+ }
8369
+ };
8370
+ var CursorACPAdapter = class extends ACPClientAdapter {
8371
+ provider = "cursor-acp";
8372
+ approvalMode;
8373
+ questionMode;
8374
+ planMode;
8375
+ decisionTimeoutMs;
8376
+ maxPendingDecisions;
8377
+ authorizedSenders;
8378
+ decisionLogger;
8379
+ extensions;
8380
+ turns = /* @__PURE__ */ new Map();
8381
+ pending = /* @__PURE__ */ new Map();
8382
+ activeTurn = null;
8383
+ turnTail = Promise.resolve();
8384
+ constructor(options = {}) {
8385
+ const extensions = new CursorExtensions();
8386
+ validateOptions(options);
8387
+ const env = cursorEnv(options);
8388
+ super({
8389
+ ...options,
8390
+ env,
8391
+ command: options.command ?? [...DEFAULT_CURSOR_ACP_COMMAND],
8392
+ authMethod: "cursor_login",
8393
+ extensionHandler: extensions,
8394
+ resolvePermission: (request, signal) => extensions.resolvePermission(request, signal)
8395
+ });
8396
+ extensions.bind(this);
8397
+ this.extensions = extensions;
8398
+ this.approvalMode = options.approvalMode ?? "manual";
8399
+ this.questionMode = options.questionMode ?? "manual";
8400
+ this.planMode = options.planMode ?? "manual";
8401
+ this.decisionTimeoutMs = options.decisionTimeoutMs ?? DEFAULT_CURSOR_DECISION_TIMEOUT_MS;
8402
+ this.maxPendingDecisions = options.maxPendingDecisions ?? DEFAULT_CURSOR_MAX_PENDING_DECISIONS;
8403
+ this.authorizedSenders = options.decisionAuthorizedSenders ? new Set(options.decisionAuthorizedSenders) : null;
8404
+ this.decisionLogger = resolveLogger(options.logger);
8405
+ }
8406
+ async onMessage(message, tools, history, participantsMessage, contactsMessage, context) {
8407
+ if (await this.handleControl(message, tools, context.roomId)) {
8408
+ return;
8409
+ }
8410
+ await this.withCursorTurnLock(() => super.onMessage(message, tools, history, participantsMessage, contactsMessage, context));
8411
+ }
8412
+ async onAcpTurnStarted(message, tools, context) {
8413
+ const turn = { messageId: message.id, roomId: context.roomId, tools, requesterId: message.senderId };
8414
+ this.turns.set(context.roomId, turn);
8415
+ this.activeTurn = turn;
8416
+ }
8417
+ async onAcpSessionReady(message, _tools, context, sessionId) {
8418
+ const turn = this.turns.get(context.roomId);
8419
+ if (turn?.messageId === message.id) {
8420
+ turn.sessionId = sessionId;
8421
+ }
8422
+ }
8423
+ async onAcpTurnFinished(message, _tools, context) {
8424
+ const turn = this.turns.get(context.roomId);
8425
+ if (turn?.messageId === message.id) {
8426
+ this.turns.delete(context.roomId);
8427
+ this.cancelRoom(context.roomId);
8428
+ }
8429
+ if (this.activeTurn?.messageId === message.id) {
8430
+ this.activeTurn = null;
8431
+ }
8432
+ }
8433
+ async onCleanup(roomId) {
8434
+ const sessionId = this.turns.get(roomId)?.sessionId;
8435
+ this.cancelRoom(roomId);
8436
+ this.turns.delete(roomId);
8437
+ if (this.activeTurn?.roomId === roomId) {
8438
+ this.activeTurn = null;
8439
+ }
8440
+ await super.onCleanup(roomId);
8441
+ if (sessionId) {
8442
+ this.extensions.forgetSession(sessionId);
8443
+ }
8444
+ }
8445
+ async stop() {
8446
+ for (const decision of this.pending.values()) {
8447
+ decision.resolve(void 0);
8448
+ }
8449
+ this.pending.clear();
8450
+ this.turns.clear();
8451
+ this.activeTurn = null;
8452
+ this.extensions.clearSessions();
8453
+ await super.stop();
8454
+ }
8455
+ async resolveExtension(method, params, sessionId) {
8456
+ const roomId = sessionId ? this.roomIdForSession(sessionId) : this.activeTurn?.roomId;
8457
+ const turn = roomId ? this.turns.get(roomId) : void 0;
8458
+ if (!turn || sessionId && turn.sessionId !== sessionId) {
8459
+ return { outcome: { outcome: "cancelled" } };
8460
+ }
8461
+ if (method === "cursor/ask_question") {
8462
+ return this.resolveQuestion(turn.roomId, turn, params);
8463
+ }
8464
+ if (method === "cursor/create_plan") {
8465
+ return this.resolvePlan(turn.roomId, turn, params);
8466
+ }
8467
+ return {};
8468
+ }
8469
+ async resolveCursorPermission(request, signal) {
8470
+ if (this.approvalMode === "autoAccept") {
8471
+ return allowOption(request.options)?.optionId;
8472
+ }
8473
+ if (this.approvalMode === "autoDecline") {
8474
+ return void 0;
8475
+ }
8476
+ const turn = this.turns.get(request.roomId);
8477
+ if (!turn) {
8478
+ return void 0;
8479
+ }
8480
+ const options = request.options.map((option) => option.optionId);
8481
+ 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);
8482
+ return typeof token === "string" && options.includes(token) ? token : void 0;
8483
+ }
8484
+ extensionSessionId() {
8485
+ return this.activeTurn?.sessionId ?? null;
8486
+ }
8487
+ async resolveQuestion(roomId, turn, params) {
8488
+ const questions = questionChoices(params.questions);
8489
+ if (questions.choices.size === 0) {
8490
+ return { outcome: { outcome: "cancelled" } };
8491
+ }
8492
+ if (this.questionMode === "autoCancel") {
8493
+ return { outcome: { outcome: "cancelled" } };
8494
+ }
8495
+ if (this.questionMode === "autoFirst") {
8496
+ return answered(Object.fromEntries([...questions.choices].map(([id, options]) => [id, [options[0]]])));
8497
+ }
8498
+ 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] ...`.");
8499
+ return isRecord2(result) ? result : { outcome: { outcome: "cancelled" } };
8500
+ }
8501
+ async resolvePlan(roomId, turn, params) {
8502
+ if (this.planMode === "autoAccept") {
8503
+ return { outcome: { outcome: "accepted" } };
8504
+ }
8505
+ if (this.planMode === "autoDecline") {
8506
+ return { outcome: { outcome: "rejected" } };
8507
+ }
8508
+ const title = stringValue(params.title) ?? "Cursor plan";
8509
+ 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}\`.`);
8510
+ return isRecord2(result) ? result : { outcome: { outcome: "cancelled" } };
8511
+ }
8512
+ async waitForDecision(kind, roomId, turn, choices, multiSelect, prompt, signal) {
8513
+ if (this.pending.size >= this.maxPendingDecisions) {
8514
+ this.pending.values().next().value?.resolve(void 0);
8515
+ }
8516
+ const token = crypto.randomUUID().slice(0, 8);
8517
+ return new Promise((resolve) => {
8518
+ const timer = setTimeout(() => settle(void 0), this.decisionTimeoutMs);
8519
+ const abort = () => settle(void 0);
8520
+ const settle = (value) => {
8521
+ clearTimeout(timer);
8522
+ signal?.removeEventListener("abort", abort);
8523
+ this.pending.delete(token);
8524
+ resolve(value);
8525
+ };
8526
+ this.pending.set(token, { kind, roomId, choices, multiSelect, resolve: settle });
8527
+ signal?.addEventListener("abort", abort, { once: true });
8528
+ void turn.tools.sendMessage(prompt.replaceAll("{token}", token), [turn.requesterId]).catch((error) => {
8529
+ this.decisionLogger.warn("cursor_acp.decision_prompt_delivery_failed", { roomId, kind, error: String(error) });
8530
+ settle(void 0);
8531
+ });
8532
+ });
8533
+ }
8534
+ async handleControl(message, tools, roomId) {
8535
+ const words = message.content.trim().split(/\s+/);
8536
+ if (words[0]?.toLowerCase() !== "/cursor") {
8537
+ return false;
8538
+ }
8539
+ if (words.length === 1 || words[1]?.toLowerCase() === "decisions") {
8540
+ const entries = [...this.pending.entries()].filter(([, decision2]) => decision2.roomId === roomId).map(([token2, decision2]) => `\`${token2}\` (${decision2.kind})`);
8541
+ await tools.sendMessage(`Pending Cursor decisions: ${entries.join(", ") || "none"}`);
8542
+ return true;
8543
+ }
8544
+ const [_, action, token, ...args] = words;
8545
+ const decision = token ? this.pending.get(token) : void 0;
8546
+ if (!decision || decision.roomId !== roomId) {
8547
+ await tools.sendMessage(`Cursor decision \`${token ?? ""}\` is not pending.`);
8548
+ return true;
8549
+ }
8550
+ if (this.authorizedSenders && !this.authorizedSenders.has(message.senderId)) {
8551
+ await tools.sendMessage("You are not authorized to resolve Cursor decisions.");
8552
+ return true;
8553
+ }
8554
+ const result = commandResult(action ?? "", args, decision);
8555
+ if (result === null) {
8556
+ await tools.sendMessage(`That command is not valid for Cursor ${decision.kind} decision \`${token}\`.`);
8557
+ return true;
8558
+ }
8559
+ decision.resolve(result);
8560
+ await tools.sendMessage(`Cursor ${decision.kind} decision \`${token}\` resolved.`);
8561
+ return true;
8562
+ }
8563
+ cancelRoom(roomId) {
8564
+ for (const [token, decision] of this.pending) {
8565
+ if (decision.roomId === roomId) {
8566
+ decision.resolve(void 0);
8567
+ this.pending.delete(token);
8568
+ }
8569
+ }
8570
+ }
8571
+ async withCursorTurnLock(run) {
8572
+ const queued = this.turnTail.then(run, run);
8573
+ this.turnTail = queued.then(() => void 0, () => void 0);
8574
+ return queued;
8575
+ }
8576
+ };
8577
+ function cursorEnv(options) {
8578
+ const env = { ...options.env };
8579
+ if (options.apiKey) env.CURSOR_API_KEY ??= options.apiKey;
8580
+ if (options.authToken) env.CURSOR_AUTH_TOKEN ??= options.authToken;
8581
+ return Object.keys(env).length > 0 ? env : void 0;
8582
+ }
8583
+ function validateOptions(options) {
8584
+ if (options.apiKey && options.authToken) throw new Error("set either apiKey or authToken, not both");
8585
+ if (Array.isArray(options.command) && options.command.length === 0) throw new Error("Cursor ACP command must not be empty");
8586
+ if (options.decisionTimeoutMs !== void 0 && (!Number.isFinite(options.decisionTimeoutMs) || options.decisionTimeoutMs <= 0)) throw new Error("decisionTimeoutMs must be a positive finite number");
8587
+ if (options.maxPendingDecisions !== void 0 && (!Number.isInteger(options.maxPendingDecisions) || options.maxPendingDecisions <= 0)) throw new Error("maxPendingDecisions must be a positive integer");
8588
+ }
8589
+ function allowOption(options) {
8590
+ return options.find((option) => option.kind === "allow_once") ?? options.find((option) => option.kind === "allow_always");
8591
+ }
8592
+ function questionChoices(value) {
8593
+ const choices = /* @__PURE__ */ new Map();
8594
+ const multiSelect = /* @__PURE__ */ new Set();
8595
+ if (!Array.isArray(value)) return { choices, multiSelect };
8596
+ for (const question of value) {
8597
+ if (!isRecord2(question) || typeof question.id !== "string" || !Array.isArray(question.options)) continue;
8598
+ const options = question.options.filter(isRecord2).map((option) => stringValue(option.id)).filter((id) => !!id);
8599
+ if (options.length === 0) continue;
8600
+ choices.set(question.id, options);
8601
+ if (question.allowMultiple === true) multiSelect.add(question.id);
8602
+ }
8603
+ return { choices, multiSelect };
8604
+ }
8605
+ function commandResult(action, args, decision) {
8606
+ if (decision.kind === "permission") return action === "deny" ? void 0 : action === "select" && args.length === 1 && decision.choices.get("permission")?.includes(args[0] ?? "") ? args[0] : null;
8607
+ if (decision.kind === "plan") return action === "accept" ? { outcome: { outcome: "accepted" } } : action === "reject" ? { outcome: { outcome: "rejected" } } : null;
8608
+ if (action !== "answer") return null;
8609
+ const selected = {};
8610
+ for (const argument of args) {
8611
+ const [id, raw] = argument.split("=", 2);
8612
+ const values = raw?.split(",") ?? [];
8613
+ const offered = id ? decision.choices.get(id) : void 0;
8614
+ if (!id || !offered || selected[id] || values.length === 0 || values.length > 1 && !decision.multiSelect.has(id) || values.some((value) => !offered.includes(value))) return null;
8615
+ selected[id] = values;
8616
+ }
8617
+ return Object.keys(selected).length === decision.choices.size ? answered(selected) : null;
8618
+ }
8619
+ function answered(selected) {
8620
+ return { outcome: { outcome: "answered", answers: Object.entries(selected).map(([questionId, selectedOptionIds]) => ({ questionId, selectedOptionIds })) } };
8621
+ }
8622
+ function parseTodos(value) {
8623
+ if (!Array.isArray(value)) return [];
8624
+ return value.flatMap((todo) => {
8625
+ if (!isRecord2(todo)) return [];
8626
+ const id = stringValue(todo.id);
8627
+ const content = stringValue(todo.content);
8628
+ const status = stringValue(todo.status);
8629
+ return id && content && status ? [{ id, content, status }] : [];
8630
+ });
8631
+ }
8632
+ function todoMark(status) {
8633
+ switch (status) {
8634
+ case "completed":
8635
+ return "x";
8636
+ case "in_progress":
8637
+ return "~";
8638
+ case "cancelled":
8639
+ return "-";
8640
+ default:
8641
+ return " ";
8642
+ }
8643
+ }
8644
+ function isRecord2(value) {
8645
+ return !!value && typeof value === "object" && !Array.isArray(value);
8646
+ }
8647
+ function stringValue(value) {
8648
+ return typeof value === "string" && value.length > 0 ? value : void 0;
8649
+ }
8650
+
8307
8651
  export {
8308
8652
  GenericAdapter,
8309
8653
  CodexJsonRpcError,
@@ -8340,5 +8684,7 @@ export {
8340
8684
  DEFAULT_OMP_ACP_COMMAND,
8341
8685
  OmpACPAdapter,
8342
8686
  DEFAULT_COPILOT_ACP_COMMAND,
8343
- CopilotACPAdapter
8687
+ CopilotACPAdapter,
8688
+ DEFAULT_CURSOR_ACP_COMMAND,
8689
+ CursorACPAdapter
8344
8690
  };