@band-ai/sdk 0.4.2 → 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.
@@ -5926,74 +5926,17 @@ import { spawn as spawn2 } from "child_process";
5926
5926
  import { createConnection } from "net";
5927
5927
  import { Duplex, Readable, Writable } from "stream";
5928
5928
 
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
5929
  // src/adapters/acp/client.ts
5989
5930
  var BandACPClient = class {
5990
5931
  sessionChunks = /* @__PURE__ */ new Map();
5991
5932
  permissionHandler;
5933
+ extensionHandler;
5992
5934
  // The handler is connection-scoped and required at construction, so it is
5993
5935
  // already in place before the agent process is spawned: there is no window
5994
5936
  // in which a `session/request_permission` has nowhere to go.
5995
- constructor(permissionHandler) {
5937
+ constructor(permissionHandler, extensionHandler) {
5996
5938
  this.permissionHandler = permissionHandler;
5939
+ this.extensionHandler = extensionHandler;
5997
5940
  }
5998
5941
  beginSession(sessionId) {
5999
5942
  this.sessionChunks.set(sessionId, []);
@@ -6038,62 +5981,26 @@ var BandACPClient = class {
6038
5981
  return chunks;
6039
5982
  }
6040
5983
  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 {};
5984
+ const result = await this.extensionHandler?.extMethod?.(
5985
+ method,
5986
+ params,
5987
+ { sessionId: sessionIdFrom(params) }
5988
+ );
5989
+ return result ?? {};
6068
5990
  }
6069
5991
  async extNotification(method, params) {
6070
- const sessionId = toOptionalString(params.sessionId) ?? toOptionalString(params.session_id);
6071
- if (!sessionId) {
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) {
6072
6000
  return;
6073
6001
  }
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
- }
6002
+ for (const chunk of chunks) {
6003
+ this.appendChunk(targetSessionId, chunk);
6097
6004
  }
6098
6005
  }
6099
6006
  appendChunk(sessionId, chunk) {
@@ -6218,6 +6125,68 @@ function extractTextFromContent(content) {
6218
6125
  function toOptionalString(value) {
6219
6126
  return typeof value === "string" && value.length > 0 ? value : null;
6220
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
+ }
6221
6190
 
6222
6191
  // src/adapters/acp/loader.ts
6223
6192
  var acpModule = new LazyAsyncValue({
@@ -6263,6 +6232,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6263
6232
  additionalMcpTools;
6264
6233
  clientCapabilities;
6265
6234
  connectionFactory;
6235
+ extensionHandler;
6266
6236
  tcpEndpoint;
6267
6237
  // The value's `generation` is the connection generation the session was
6268
6238
  // last established/restored against. `client` is the exact BandACPClient
@@ -6327,6 +6297,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6327
6297
  this.additionalMcpTools = [...options.additionalMcpTools ?? []];
6328
6298
  this.clientCapabilities = options.clientCapabilities;
6329
6299
  this.connectionFactory = options.connectionFactory;
6300
+ this.extensionHandler = options.extensionHandler;
6330
6301
  this.tcpEndpoint = tcpEndpoint;
6331
6302
  this.resolvePermission = options.resolvePermission;
6332
6303
  this.resolveSessionMode = options.resolveSessionMode;
@@ -6382,6 +6353,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6382
6353
  let client = null;
6383
6354
  let sessionId;
6384
6355
  let generation = 0;
6356
+ await this.onAcpTurnStarted(message, tools, context);
6385
6357
  try {
6386
6358
  const ensured = await this.ensureConnection();
6387
6359
  connection = ensured.connection;
@@ -6392,6 +6364,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6392
6364
  }
6393
6365
  sessionId = await this.getOrCreateSession(context.roomId, connection, generation, client);
6394
6366
  const sessionKey = this.sessionKey(generation, sessionId);
6367
+ await this.onAcpSessionReady(message, tools, context, sessionId);
6395
6368
  client.beginSession(sessionId);
6396
6369
  const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
6397
6370
  const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
@@ -6462,8 +6435,16 @@ ${messageWithContext}`;
6462
6435
  ...configError ? { optionId: configError.optionId, selectedValue: configError.selectedValue } : {}
6463
6436
  }
6464
6437
  );
6438
+ } finally {
6439
+ await this.onAcpTurnFinished(message, tools, context);
6465
6440
  }
6466
6441
  }
6442
+ async onAcpTurnStarted(_message, _tools, _context) {
6443
+ }
6444
+ async onAcpTurnFinished(_message, _tools, _context) {
6445
+ }
6446
+ async onAcpSessionReady(_message, _tools, _context, _sessionId) {
6447
+ }
6467
6448
  // Best-effort: tells the agent to stop working on a turn Band has already
6468
6449
  // given up waiting for (the ACP client has no way to force it), evicts the
6469
6450
  // session so the room's next turn re-establishes rather than reuses it
@@ -6633,6 +6614,9 @@ ${messageWithContext}`;
6633
6614
  sessionKey(generation, sessionId) {
6634
6615
  return `${generation}:${sessionId}`;
6635
6616
  }
6617
+ roomIdForSession(sessionId) {
6618
+ return [...this.roomToSession.entries()].find(([, owner]) => owner.sessionId === sessionId)?.[0];
6619
+ }
6636
6620
  async ensureConnection() {
6637
6621
  if (this.connection && !this.connection.signal.aborted) {
6638
6622
  return { connection: this.connection, generation: this.connectionGeneration };
@@ -6669,7 +6653,10 @@ ${messageWithContext}`;
6669
6653
  throw new Error(CONNECTION_ATTEMPT_SUPERSEDED_ERROR);
6670
6654
  }
6671
6655
  const owner = { generation: -1 };
6672
- 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
+ );
6673
6660
  handle = await (this.connectionFactory ? this.connectionFactory(client, {
6674
6661
  command: this.command,
6675
6662
  cwd: this.cwd,
@@ -8304,6 +8291,358 @@ var CopilotACPAdapter = class extends ACPClientAdapter {
8304
8291
  }
8305
8292
  };
8306
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
+
8307
8646
  export {
8308
8647
  GenericAdapter,
8309
8648
  CodexJsonRpcError,
@@ -8340,5 +8679,7 @@ export {
8340
8679
  DEFAULT_OMP_ACP_COMMAND,
8341
8680
  OmpACPAdapter,
8342
8681
  DEFAULT_COPILOT_ACP_COMMAND,
8343
- CopilotACPAdapter
8682
+ CopilotACPAdapter,
8683
+ DEFAULT_CURSOR_ACP_COMMAND,
8684
+ CursorACPAdapter
8344
8685
  };