@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.
Files changed (3) hide show
  1. package/README.md +18 -7
  2. package/dist/main.js +142 -25
  3. 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. Version 0.1.0 is not published yet; the
9
- commands below require that release.
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.1.0 login # Cursor's browser login, the key kept in the OS keychain
15
- npx -y @chorded/cursor-cloud-acp@0.1.0 status # whether the keychain holds a login
16
- npx -y @chorded/cursor-cloud-acp@0.1.0 logout # the key removed from the keychain
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.1.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 apiKey(source) {
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 stored.apiKey;
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 artifacts = await Promise.race([
915
- this.#context.artifacts(),
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 (artifacts === "cancelled" || this.#cancelled) {
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 key = async () => {
1013
- const found = await apiKey(context.credentials());
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
- throw signedOut();
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
- await port.me({ apiKey: found });
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("authenticate", () => {
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) => current() ? client.notify("session/update", {
1174
- sessionId: params.sessionId,
1175
- update
1176
- }) : Promise.resolve()
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.1.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.2.0",
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",