@chorded/cursor-cloud-acp 0.1.0 → 0.2.0
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/README.md +18 -7
- package/dist/main.js +142 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,21 +5,25 @@ v1 agent on stdin and stdout, written on Cursor's TypeScript SDK,
|
|
|
5
5
|
`@cursor/sdk`. [chord](https://github.com/IKatsuba/chord) starts it as its
|
|
6
6
|
`cursor-cloud` agent.
|
|
7
7
|
|
|
8
|
-
Requires Node 22.13 or newer.
|
|
9
|
-
|
|
8
|
+
Requires Node 22.13 or newer. The commands below require 0.2.0
|
|
9
|
+
to be published; the owner publishes it before chord's next release.
|
|
10
10
|
|
|
11
11
|
## Signing in
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npx -y @chorded/cursor-cloud-acp@0.
|
|
15
|
-
npx -y @chorded/cursor-cloud-acp@0.
|
|
16
|
-
npx -y @chorded/cursor-cloud-acp@0.
|
|
14
|
+
npx -y @chorded/cursor-cloud-acp@0.2.0 login # Cursor's browser login, the key kept in the OS keychain
|
|
15
|
+
npx -y @chorded/cursor-cloud-acp@0.2.0 status # whether the keychain holds a login
|
|
16
|
+
npx -y @chorded/cursor-cloud-acp@0.2.0 logout # the key removed from the keychain
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
`CURSOR_API_KEY`, when set, is used instead of the keychain's key. Where no
|
|
20
20
|
keychain is there (Linux without a Secret Service), `login` refuses and
|
|
21
21
|
points to `CURSOR_API_KEY`; no key is written to a file. The SDK's own
|
|
22
22
|
login file is not used. Keychain calls time out after 10 seconds.
|
|
23
|
+
ACP `auth/status` reports the account's name and email when given, with
|
|
24
|
+
the key's source and keychain expiry when known; chord shows them in
|
|
25
|
+
`/status`. Changes push at the next status request, session start, list,
|
|
26
|
+
resume or load, without polling.
|
|
23
27
|
|
|
24
28
|
What each prints, on stdout (the SDK's own lines, the login URL among
|
|
25
29
|
them, go to stderr):
|
|
@@ -37,7 +41,7 @@ them, go to stderr):
|
|
|
37
41
|
```sh
|
|
38
42
|
chord cursor-cloud
|
|
39
43
|
# Or serve ACP v1 to another client in the current directory:
|
|
40
|
-
npx -y @chorded/cursor-cloud-acp@0.
|
|
44
|
+
npx -y @chorded/cursor-cloud-acp@0.2.0
|
|
41
45
|
```
|
|
42
46
|
|
|
43
47
|
Start in a git work tree on a pushed branch with a push remote. The check
|
|
@@ -50,7 +54,11 @@ nothing is downloaded.
|
|
|
50
54
|
|
|
51
55
|
Creating a session is refused while the Cursor account's prepaid balance
|
|
52
56
|
is empty: top up or enable auto top-up. `usage_limit_exceeded` appears as
|
|
53
|
-
one plain line with Cursor's message.
|
|
57
|
+
one plain line with Cursor's message. After each run, ACP `usage_update`
|
|
58
|
+
reports the whole agent's undiscounted token cost in USD when available.
|
|
59
|
+
Cost may lag until a later run. chord's `/cost` counts it per session, so
|
|
60
|
+
resuming does not count earlier runs again. No context meter is shown:
|
|
61
|
+
the SDK gives no context window.
|
|
54
62
|
|
|
55
63
|
Agent and Plan modes apply from the next prompt. Models have one entry
|
|
56
64
|
each, with separate parameter options; choosing a model starts outside
|
|
@@ -65,6 +73,9 @@ In chord, `/resume` lists chord's records;
|
|
|
65
73
|
The adapter replays the history Cursor provides. A run still going is
|
|
66
74
|
reported, not followed live; prompts are refused until it ends.
|
|
67
75
|
ACP `session/list` lists the account's cloud agents for the repository.
|
|
76
|
+
Cursor's agent name becomes the title only when it changes after a run,
|
|
77
|
+
never on new, resume or load. `/rename` changes chord's title alone; a later
|
|
78
|
+
change to Cursor's name replaces it.
|
|
68
79
|
MCP servers are not forwarded: chord holds them back by default; servers
|
|
69
80
|
sent by another client are named unreachable in the first message.
|
|
70
81
|
|
package/dist/main.js
CHANGED
|
@@ -157,6 +157,7 @@ function agent(real, from) {
|
|
|
157
157
|
},
|
|
158
158
|
[Symbol.asyncDispose]: () => guarded(() => real[Symbol.asyncDispose](), from),
|
|
159
159
|
listArtifacts: () => guarded(() => real.listArtifacts(), from),
|
|
160
|
+
getUsage: (options) => guarded(() => real.getUsage(options), from),
|
|
160
161
|
send: async (...args) => run(await guarded(() => real.send(...args), from), from)
|
|
161
162
|
};
|
|
162
163
|
}
|
|
@@ -383,6 +384,14 @@ var MODE = "mode";
|
|
|
383
384
|
var MODEL = "model";
|
|
384
385
|
var CURSORS_DEFAULT = "cursor-default";
|
|
385
386
|
var PARAMETER = "parameter:";
|
|
387
|
+
var MARKED = {
|
|
388
|
+
reasoning_effort: { category: "thought_level" },
|
|
389
|
+
context: {
|
|
390
|
+
category: "model_config",
|
|
391
|
+
_meta: { "chord/parameter": "context" }
|
|
392
|
+
},
|
|
393
|
+
fast: { category: "model_config", _meta: { "chord/parameter": "fast" } }
|
|
394
|
+
};
|
|
386
395
|
function sdkMode(choice) {
|
|
387
396
|
return choice.mode === "plan" ? "plan" : "agent";
|
|
388
397
|
}
|
|
@@ -461,6 +470,7 @@ function configOptions(choice, models, defaultOffered) {
|
|
|
461
470
|
options.push({
|
|
462
471
|
id: `${PARAMETER}${parameter.id}`,
|
|
463
472
|
name: parameter.displayName ?? parameter.id,
|
|
473
|
+
...Object.hasOwn(MARKED, parameter.id) ? MARKED[parameter.id] : {},
|
|
464
474
|
type: "select",
|
|
465
475
|
currentValue: value,
|
|
466
476
|
options: parameter.values.filter((each) => allowed.has(each.value)).map((each) => ({
|
|
@@ -504,17 +514,42 @@ function choose(choice, models, defaultOffered, id, value) {
|
|
|
504
514
|
};
|
|
505
515
|
}
|
|
506
516
|
|
|
517
|
+
// packages/cursor-cloud-acp/src/auth_status.ts
|
|
518
|
+
var UNAUTHENTICATED = { state: "unauthenticated" };
|
|
519
|
+
var UNKNOWN = { state: "unknown" };
|
|
520
|
+
function description(found) {
|
|
521
|
+
if (found.source === "env") {
|
|
522
|
+
return "CURSOR_API_KEY";
|
|
523
|
+
}
|
|
524
|
+
return found.expiresAtMs === undefined ? "keychain" : `keychain, expires ${new Date(found.expiresAtMs).toISOString().slice(0, 10)}`;
|
|
525
|
+
}
|
|
526
|
+
function authenticated(found, user) {
|
|
527
|
+
const name = [user.userFirstName, user.userLastName].filter((part) => part !== undefined && part !== "").join(" ");
|
|
528
|
+
const account = {
|
|
529
|
+
label: "Cursor",
|
|
530
|
+
description: description(found),
|
|
531
|
+
active: true,
|
|
532
|
+
...name === "" ? {} : { name },
|
|
533
|
+
...user.userEmail === undefined || user.userEmail === "" ? {} : { email: user.userEmail }
|
|
534
|
+
};
|
|
535
|
+
return { state: "authenticated", accounts: [account] };
|
|
536
|
+
}
|
|
537
|
+
|
|
507
538
|
// packages/cursor-cloud-acp/src/credentials.ts
|
|
508
|
-
async function
|
|
539
|
+
async function findKey(source) {
|
|
509
540
|
const fromEnv = source.env.CURSOR_API_KEY;
|
|
510
541
|
if (fromEnv !== undefined && fromEnv !== "") {
|
|
511
|
-
return fromEnv;
|
|
542
|
+
return { apiKey: fromEnv, source: "env" };
|
|
512
543
|
}
|
|
513
544
|
const stored = await source.store.load();
|
|
514
545
|
if (stored === undefined || stored.apiKeyExpiresAtMs !== undefined && stored.apiKeyExpiresAtMs <= source.now()) {
|
|
515
546
|
return;
|
|
516
547
|
}
|
|
517
|
-
return
|
|
548
|
+
return {
|
|
549
|
+
apiKey: stored.apiKey,
|
|
550
|
+
source: "keychain",
|
|
551
|
+
...stored.apiKeyExpiresAtMs === undefined ? {} : { expiresAtMs: stored.apiKeyExpiresAtMs }
|
|
552
|
+
};
|
|
518
553
|
}
|
|
519
554
|
|
|
520
555
|
// packages/cursor-cloud-acp/src/turn.ts
|
|
@@ -911,14 +946,19 @@ class Turn {
|
|
|
911
946
|
if (result.status === "error") {
|
|
912
947
|
throw new RequestError2(INTERNAL_ERROR, result.error?.message ?? "The run failed.", result.error?.code === undefined ? undefined : { code: result.error.code });
|
|
913
948
|
}
|
|
914
|
-
const
|
|
915
|
-
|
|
949
|
+
const asked = await Promise.race([
|
|
950
|
+
Promise.all([
|
|
951
|
+
this.#context.artifacts(),
|
|
952
|
+
this.#context.ended?.() ?? Promise.resolve([])
|
|
953
|
+
]),
|
|
916
954
|
this.#cancelling
|
|
917
955
|
]);
|
|
918
|
-
if (
|
|
956
|
+
if (asked === "cancelled" || this.#cancelled) {
|
|
919
957
|
return { stopReason: "cancelled", ...tokens };
|
|
920
958
|
}
|
|
959
|
+
const [artifacts, ended] = asked;
|
|
921
960
|
await this.#draw(runEnd(result, artifacts).map((text) => line(text)));
|
|
961
|
+
await this.#draw(ended);
|
|
922
962
|
return this.#cancelled ? { stopReason: "cancelled", ...tokens } : { stopReason: "end_turn", ...tokens };
|
|
923
963
|
}
|
|
924
964
|
async#unconfirmed() {
|
|
@@ -972,6 +1012,7 @@ function initialize(version) {
|
|
|
972
1012
|
}
|
|
973
1013
|
],
|
|
974
1014
|
agentCapabilities: {
|
|
1015
|
+
auth: { status: {} },
|
|
975
1016
|
loadSession: true,
|
|
976
1017
|
promptCapabilities: { image: true, embeddedContext: false },
|
|
977
1018
|
mcpCapabilities: { http: false, sse: false },
|
|
@@ -1009,13 +1050,44 @@ function serve(stream, context) {
|
|
|
1009
1050
|
}
|
|
1010
1051
|
return open;
|
|
1011
1052
|
};
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1053
|
+
const resolve = async () => {
|
|
1054
|
+
let found;
|
|
1055
|
+
try {
|
|
1056
|
+
found = await findKey(context.credentials());
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
return { status: UNKNOWN, error };
|
|
1059
|
+
}
|
|
1014
1060
|
if (found === undefined) {
|
|
1015
|
-
|
|
1061
|
+
return { status: UNAUTHENTICATED, error: signedOut() };
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
const user = await port.me({ apiKey: found.apiKey });
|
|
1065
|
+
return { status: authenticated(found, user), key: found.apiKey };
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
return {
|
|
1068
|
+
status: error instanceof CursorError && error.kind === "auth" ? UNAUTHENTICATED : UNKNOWN,
|
|
1069
|
+
error
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
let kept;
|
|
1074
|
+
const report = async (client, status) => {
|
|
1075
|
+
const next = JSON.stringify(status);
|
|
1076
|
+
if (kept === undefined || kept === next) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
kept = next;
|
|
1080
|
+
await client.notify("auth/status_update", status).catch(() => {
|
|
1081
|
+
return;
|
|
1082
|
+
});
|
|
1083
|
+
};
|
|
1084
|
+
const key = async (client) => {
|
|
1085
|
+
const resolved = await resolve();
|
|
1086
|
+
await report(client, resolved.status);
|
|
1087
|
+
if ("error" in resolved) {
|
|
1088
|
+
throw resolved.error;
|
|
1016
1089
|
}
|
|
1017
|
-
|
|
1018
|
-
return found;
|
|
1090
|
+
return resolved.key;
|
|
1019
1091
|
};
|
|
1020
1092
|
const models = (key) => port.models({ apiKey: key }).catch(() => []);
|
|
1021
1093
|
const options = (open) => configOptions(open.choice, open.models, open.agent.model === undefined);
|
|
@@ -1067,9 +1139,9 @@ function serve(stream, context) {
|
|
|
1067
1139
|
} while (cursor !== undefined);
|
|
1068
1140
|
return runs;
|
|
1069
1141
|
};
|
|
1070
|
-
const reopen = async (id, mcpServers) => {
|
|
1142
|
+
const reopen = async (id, mcpServers, client) => {
|
|
1071
1143
|
const generation = begin(id);
|
|
1072
|
-
const found = await key();
|
|
1144
|
+
const found = await key(client);
|
|
1073
1145
|
const reopened = await port.resume(id, { apiKey: found });
|
|
1074
1146
|
return opening(reopened, async () => {
|
|
1075
1147
|
const [latest] = (await port.listRuns(id, { runtime: "cloud", apiKey: found })).items;
|
|
@@ -1085,7 +1157,8 @@ function serve(stream, context) {
|
|
|
1085
1157
|
...reopened.model === undefined ? {} : { model: chosen(reopened.model) }
|
|
1086
1158
|
},
|
|
1087
1159
|
models: await models(found),
|
|
1088
|
-
turn: undefined
|
|
1160
|
+
turn: undefined,
|
|
1161
|
+
name: info.name.trim() === "" ? undefined : info.name.trim()
|
|
1089
1162
|
};
|
|
1090
1163
|
await hold(open, generation);
|
|
1091
1164
|
return {
|
|
@@ -1095,6 +1168,32 @@ function serve(stream, context) {
|
|
|
1095
1168
|
};
|
|
1096
1169
|
});
|
|
1097
1170
|
};
|
|
1171
|
+
const renamed = async (open) => {
|
|
1172
|
+
const info = await port.get(open.agent.agentId, { apiKey: open.apiKey }).catch(() => {
|
|
1173
|
+
return;
|
|
1174
|
+
});
|
|
1175
|
+
const name = info?.name.trim();
|
|
1176
|
+
return info === undefined || name === undefined || name === "" || name === open.name ? [] : [
|
|
1177
|
+
{
|
|
1178
|
+
sessionUpdate: "session_info_update",
|
|
1179
|
+
title: name,
|
|
1180
|
+
updatedAt: new Date(info.lastModified).toISOString()
|
|
1181
|
+
}
|
|
1182
|
+
];
|
|
1183
|
+
};
|
|
1184
|
+
const cost = async (open) => {
|
|
1185
|
+
const usage = await open.agent.getUsage().catch(() => {
|
|
1186
|
+
return;
|
|
1187
|
+
});
|
|
1188
|
+
return usage?.cost === undefined ? [] : [
|
|
1189
|
+
{
|
|
1190
|
+
sessionUpdate: "usage_update",
|
|
1191
|
+
used: 0,
|
|
1192
|
+
size: 0,
|
|
1193
|
+
cost: { amount: usage.cost.rawCostCents / 100, currency: "USD" }
|
|
1194
|
+
}
|
|
1195
|
+
];
|
|
1196
|
+
};
|
|
1098
1197
|
let shutting;
|
|
1099
1198
|
const shutdown = async () => {
|
|
1100
1199
|
ended = true;
|
|
@@ -1106,12 +1205,20 @@ function serve(stream, context) {
|
|
|
1106
1205
|
new Promise((resolve) => setTimeout(resolve, RELEASE_MS).unref())
|
|
1107
1206
|
]);
|
|
1108
1207
|
};
|
|
1109
|
-
const connection = agent2({ name: NAME }).onRequest("initialize", () => initialize(context.version)).onRequest("
|
|
1208
|
+
const connection = agent2({ name: NAME }).onRequest("initialize", () => initialize(context.version)).onRequest("auth/status", async ({ client }) => {
|
|
1209
|
+
const { status } = await resolve();
|
|
1210
|
+
if (kept === undefined) {
|
|
1211
|
+
kept = JSON.stringify(status);
|
|
1212
|
+
} else {
|
|
1213
|
+
await report(client, status);
|
|
1214
|
+
}
|
|
1215
|
+
return status;
|
|
1216
|
+
}).onRequest("authenticate", () => {
|
|
1110
1217
|
throw new RequestError3(INVALID_PARAMS, `Sign in with \`${loginCommand(context.version)}\` in a terminal, or set CURSOR_API_KEY.`);
|
|
1111
1218
|
}).onRequest("session/new", async ({ params, client }) => {
|
|
1112
1219
|
try {
|
|
1113
1220
|
const repo = await repository(params.cwd, context.git);
|
|
1114
|
-
const found = await key();
|
|
1221
|
+
const found = await key(client);
|
|
1115
1222
|
const created = await port.create({
|
|
1116
1223
|
apiKey: found,
|
|
1117
1224
|
mode: "agent",
|
|
@@ -1129,7 +1236,8 @@ function serve(stream, context) {
|
|
|
1129
1236
|
mcpServers: serverNames(params.mcpServers),
|
|
1130
1237
|
choice: { mode: "default" },
|
|
1131
1238
|
models: await models(found),
|
|
1132
|
-
turn: undefined
|
|
1239
|
+
turn: undefined,
|
|
1240
|
+
name: undefined
|
|
1133
1241
|
};
|
|
1134
1242
|
await hold(opened, generation);
|
|
1135
1243
|
return opened;
|
|
@@ -1165,15 +1273,24 @@ function serve(stream, context) {
|
|
|
1165
1273
|
});
|
|
1166
1274
|
},
|
|
1167
1275
|
artifacts: () => open.agent.listArtifacts().catch(() => []),
|
|
1276
|
+
ended: async () => (await Promise.all([renamed(open), cost(open)])).flat(),
|
|
1168
1277
|
ran: (model) => {
|
|
1169
1278
|
if (current() && open.choice === sentWith) {
|
|
1170
1279
|
open.choice = { ...open.choice, model: chosen(model) };
|
|
1171
1280
|
}
|
|
1172
1281
|
},
|
|
1173
|
-
emit: (update) =>
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1282
|
+
emit: (update) => {
|
|
1283
|
+
if (!current()) {
|
|
1284
|
+
return Promise.resolve();
|
|
1285
|
+
}
|
|
1286
|
+
if (update.sessionUpdate === "session_info_update") {
|
|
1287
|
+
open.name = update.title ?? open.name;
|
|
1288
|
+
}
|
|
1289
|
+
return client.notify("session/update", {
|
|
1290
|
+
sessionId: params.sessionId,
|
|
1291
|
+
update
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1177
1294
|
});
|
|
1178
1295
|
if (open.turn === undefined) {
|
|
1179
1296
|
open.turn = turn;
|
|
@@ -1209,9 +1326,9 @@ function serve(stream, context) {
|
|
|
1209
1326
|
}
|
|
1210
1327
|
open.choice = next;
|
|
1211
1328
|
return { configOptions: options(open) };
|
|
1212
|
-
}).onRequest("session/list", async ({ params }) => {
|
|
1329
|
+
}).onRequest("session/list", async ({ params, client }) => {
|
|
1213
1330
|
try {
|
|
1214
|
-
const found = await key();
|
|
1331
|
+
const found = await key(client);
|
|
1215
1332
|
const url = params.cwd === undefined || params.cwd === null ? undefined : (await repository(params.cwd, context.git, { pushed: false })).url;
|
|
1216
1333
|
const listed = await port.list({
|
|
1217
1334
|
runtime: "cloud",
|
|
@@ -1232,7 +1349,7 @@ function serve(stream, context) {
|
|
|
1232
1349
|
}
|
|
1233
1350
|
}).onRequest("session/resume", async ({ params, client }) => {
|
|
1234
1351
|
try {
|
|
1235
|
-
const { open, last, url } = await reopen(params.sessionId, params.mcpServers);
|
|
1352
|
+
const { open, last, url } = await reopen(params.sessionId, params.mcpServers, client);
|
|
1236
1353
|
await notifyWhile(open, client, params.sessionId, line(resumedMessage({
|
|
1237
1354
|
url,
|
|
1238
1355
|
agentId: params.sessionId,
|
|
@@ -1245,7 +1362,7 @@ function serve(stream, context) {
|
|
|
1245
1362
|
}
|
|
1246
1363
|
}).onRequest("session/load", async ({ params, client }) => {
|
|
1247
1364
|
try {
|
|
1248
|
-
const { open, last, url } = await reopen(params.sessionId, params.mcpServers);
|
|
1365
|
+
const { open, last, url } = await reopen(params.sessionId, params.mcpServers, client);
|
|
1249
1366
|
const notify = (update) => notifyWhile(open, client, params.sessionId, update);
|
|
1250
1367
|
const runs = await allRuns(params.sessionId, open.apiKey);
|
|
1251
1368
|
for (const run of oldestFirst(runs)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chorded/cursor-cloud-acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Cursor's cloud agents as an ACP v1 agent on stdin and stdout",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@agentclientprotocol/sdk": "^2.
|
|
31
|
+
"@agentclientprotocol/sdk": "^2.6.0",
|
|
32
32
|
"@cursor/sdk": "1.3.1",
|
|
33
33
|
"@napi-rs/keyring": "^2.1.2",
|
|
34
34
|
"hosted-git-info": "^9.0.3",
|