@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.
- package/dist/{CopilotACPAdapter-Bj6NRdqd.d.ts → CursorACPAdapter-7-Jtl1LV.d.ts} +90 -1
- package/dist/{CopilotACPAdapter-HRvat7CP.d.cts → CursorACPAdapter-_YhrXRWq.d.cts} +90 -1
- package/dist/adapters.cjs +476 -128
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +5 -1
- package/dist/{chunk-KXLV645N.js → chunk-STUSNTHF.js} +463 -117
- package/dist/index.cjs +413 -65
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/package.json +6 -6
|
@@ -483,15 +483,20 @@ function systemUpdateParts(participantsMessage, contactsMessage) {
|
|
|
483
483
|
}
|
|
484
484
|
function buildConversationPrompt(options) {
|
|
485
485
|
const parts = [];
|
|
486
|
-
if (options.isSessionBootstrap
|
|
487
|
-
const historyText = options.history.raw.slice(-(options.maxHistoryMessages ?? 50)).map(formatHistoryLine).join("\n");
|
|
488
|
-
|
|
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
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
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 =
|
|
6071
|
-
|
|
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
|
-
|
|
6075
|
-
|
|
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(
|
|
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
|
};
|