@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.
- 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 +468 -125
- 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-VHYPQAQ5.js} +455 -114
- package/dist/index.cjs +405 -62
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 {};
|
|
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 =
|
|
6071
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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(
|
|
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
|
};
|