@cursor/july 0.1.3 → 0.1.5

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 (141) hide show
  1. package/dist/bin/agent-serve.d.ts +1 -1
  2. package/dist/bin/agent-serve.js +40 -16
  3. package/dist/channels/slack/log.d.ts +1 -1
  4. package/dist/channels/slack/log.d.ts.map +1 -1
  5. package/dist/channels/slack/log.js +4 -5
  6. package/dist/channels/slack/setup.js +1 -1
  7. package/dist/channels/slack/socket-mode.d.ts.map +1 -1
  8. package/dist/channels/slack/socket-mode.js +10 -9
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.BR0RbIdx.js → app.DabPG-io.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.QwK5BtEH.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.qjsVTneI.js → VPLocalSearchBox.jmyr0bU0.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.2Kg8jIp_.js → theme.DysN9-VN.js} +2 -2
  15. package/dist/docs/assets/{index.md.m81y7TY7.js → index.md.Cylk70gg.js} +1 -1
  16. package/dist/docs/building-with-agents.html +3 -3
  17. package/dist/docs/concepts.html +3 -3
  18. package/dist/docs/deployment.html +3 -3
  19. package/dist/docs/evals.html +3 -3
  20. package/dist/docs/guides/agent-to-agent.html +3 -3
  21. package/dist/docs/guides/cloud-runtime.html +3 -3
  22. package/dist/docs/guides/github.html +3 -3
  23. package/dist/docs/guides/human-in-the-loop.html +3 -3
  24. package/dist/docs/guides/slack.html +3 -3
  25. package/dist/docs/guides/webhooks.html +3 -3
  26. package/dist/docs/hashmap.json +1 -1
  27. package/dist/docs/hillclimbing.html +3 -3
  28. package/dist/docs/index.html +5 -5
  29. package/dist/docs/quickstart.html +3 -3
  30. package/dist/docs/reference/agent-config.html +3 -3
  31. package/dist/docs/reference/channels.html +3 -3
  32. package/dist/docs/reference/cli.html +3 -3
  33. package/dist/docs/reference/connections.html +3 -3
  34. package/dist/docs/reference/hooks.html +3 -3
  35. package/dist/docs/reference/http-api.html +3 -3
  36. package/dist/docs/reference/instructions.html +3 -3
  37. package/dist/docs/reference/playground.html +3 -3
  38. package/dist/docs/reference/project-layout.html +3 -3
  39. package/dist/docs/reference/schedules.html +3 -3
  40. package/dist/docs/reference/sessions.html +3 -3
  41. package/dist/docs/reference/skills.html +3 -3
  42. package/dist/docs/reference/subagents.html +3 -3
  43. package/dist/docs/reference/tools.html +3 -3
  44. package/dist/docs/scaffolding-agents.html +3 -3
  45. package/dist/docs/storage.html +3 -3
  46. package/dist/docs/troubleshooting.html +3 -3
  47. package/dist/internal/ab-collector.js +3 -3
  48. package/dist/internal/ab-fold.js +1 -1
  49. package/dist/internal/cli-ax.d.ts +13 -5
  50. package/dist/internal/cli-ax.d.ts.map +1 -1
  51. package/dist/internal/cli-ax.js +41 -15
  52. package/dist/internal/cli-deploy.d.ts.map +1 -1
  53. package/dist/internal/cli-deploy.js +5 -2
  54. package/dist/internal/cli-github.js +13 -13
  55. package/dist/internal/cursor/account-mcp.js +3 -3
  56. package/dist/internal/cursor/backend-client.js +1 -1
  57. package/dist/internal/cursor/credentials.d.ts +5 -3
  58. package/dist/internal/cursor/credentials.d.ts.map +1 -1
  59. package/dist/internal/cursor/credentials.js +9 -6
  60. package/dist/internal/cursor/github-credentials.js +3 -3
  61. package/dist/internal/cursor-event-relay.d.ts +4 -1
  62. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  63. package/dist/internal/cursor-event-relay.js +7 -1
  64. package/dist/internal/cursor-relay-core.d.ts +37 -2
  65. package/dist/internal/cursor-relay-core.d.ts.map +1 -1
  66. package/dist/internal/cursor-relay-core.js +91 -9
  67. package/dist/internal/cursor-slack-relay.d.ts +2 -2
  68. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  69. package/dist/internal/cursor-slack-relay.js +5 -1
  70. package/dist/internal/distribution.d.ts +7 -0
  71. package/dist/internal/distribution.d.ts.map +1 -1
  72. package/dist/internal/distribution.js +7 -0
  73. package/dist/internal/docs-site.js +3 -3
  74. package/dist/internal/eval-run-store.d.ts.map +1 -1
  75. package/dist/internal/eval-run-store.js +8 -8
  76. package/dist/internal/github-fanout.js +2 -2
  77. package/dist/internal/host-platforms.js +5 -5
  78. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  79. package/dist/internal/mcp-endpoint.js +1 -1
  80. package/dist/internal/playground/toolchain.js +6 -6
  81. package/dist/internal/reminder-runner.d.ts.map +1 -1
  82. package/dist/internal/reminder-runner.js +11 -11
  83. package/dist/internal/resolve-prod-target.d.ts +2 -0
  84. package/dist/internal/resolve-prod-target.d.ts.map +1 -1
  85. package/dist/internal/resolve-prod-target.js +44 -21
  86. package/dist/internal/resolved-connections.js +5 -5
  87. package/dist/internal/schedule-runner.js +1 -1
  88. package/dist/internal/server.js +14 -14
  89. package/dist/internal/session-engine.d.ts.map +1 -1
  90. package/dist/internal/session-engine.js +13 -13
  91. package/dist/internal/sessions-client.d.ts +13 -0
  92. package/dist/internal/sessions-client.d.ts.map +1 -1
  93. package/dist/internal/sessions-client.js +25 -0
  94. package/dist/internal/storage-coordinator.js +6 -6
  95. package/dist/internal/update-check.d.ts +93 -0
  96. package/dist/internal/update-check.d.ts.map +1 -0
  97. package/dist/internal/update-check.js +431 -0
  98. package/dist/playground/assets/{index-D-vo2lV_.css → index-BEauYlII.css} +1 -1
  99. package/dist/playground/assets/index-BtM0wEGg.js +319 -0
  100. package/dist/playground/index.html +2 -2
  101. package/dist/types.d.ts +3 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/docs/README.md +0 -1
  104. package/package.json +2 -1
  105. package/skills/setup-slack/SKILL.md +1 -1
  106. package/src/bin/agent-serve.ts +42 -16
  107. package/src/channels/slack/log.ts +5 -5
  108. package/src/channels/slack/setup.ts +1 -1
  109. package/src/channels/slack/socket-mode.ts +11 -9
  110. package/src/internal/ab-collector.ts +3 -3
  111. package/src/internal/ab-fold.ts +1 -1
  112. package/src/internal/cli-ax.ts +55 -16
  113. package/src/internal/cli-deploy.ts +5 -2
  114. package/src/internal/cli-github.ts +13 -15
  115. package/src/internal/cursor/account-mcp.ts +3 -3
  116. package/src/internal/cursor/backend-client.ts +1 -1
  117. package/src/internal/cursor/credentials.ts +12 -7
  118. package/src/internal/cursor/github-credentials.ts +3 -3
  119. package/src/internal/cursor-event-relay.ts +11 -2
  120. package/src/internal/cursor-relay-core.ts +119 -11
  121. package/src/internal/cursor-slack-relay.ts +7 -3
  122. package/src/internal/distribution.ts +8 -0
  123. package/src/internal/docs-site.ts +3 -3
  124. package/src/internal/eval-run-store.ts +8 -10
  125. package/src/internal/github-fanout.ts +2 -2
  126. package/src/internal/host-platforms.ts +5 -5
  127. package/src/internal/mcp-endpoint.ts +1 -3
  128. package/src/internal/playground/toolchain.ts +6 -6
  129. package/src/internal/reminder-runner.ts +11 -17
  130. package/src/internal/resolve-prod-target.ts +55 -23
  131. package/src/internal/resolved-connections.ts +5 -5
  132. package/src/internal/schedule-runner.ts +1 -1
  133. package/src/internal/server.ts +14 -14
  134. package/src/internal/session-engine.ts +13 -15
  135. package/src/internal/sessions-client.ts +34 -0
  136. package/src/internal/storage-coordinator.ts +6 -6
  137. package/src/internal/update-check.ts +552 -0
  138. package/src/types.ts +3 -1
  139. package/dist/docs/assets/chunks/@localSearchIndexroot.DtIOQzGj.js +0 -1
  140. package/dist/playground/assets/index-x60b9q2j.js +0 -312
  141. /package/dist/docs/assets/{index.md.m81y7TY7.lean.js → index.md.Cylk70gg.lean.js} +0 -0
@@ -41,6 +41,7 @@ import {
41
41
  fetchSessionEvents,
42
42
  formatSessionsTable,
43
43
  listSessions,
44
+ resolvePreviousSessionId,
44
45
  } from "./sessions-client.js";
45
46
  import { createStreamProgress } from "./stream-progress.js";
46
47
  import {
@@ -118,6 +119,11 @@ export interface AxCliOptions {
118
119
  * (CLI flag `--print`.)
119
120
  */
120
121
  print?: boolean;
122
+ /**
123
+ * `chat --resume` / `resume` with no session id: reattach the most
124
+ * recently updated followable session from `/v1/sessions`.
125
+ */
126
+ resume?: boolean;
121
127
  /** Abort signal (tests / programmatic stop of `playground` proxy). */
122
128
  signal?: AbortSignal;
123
129
  }
@@ -310,26 +316,30 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
310
316
  * prints a trajectory instead of the interactive UI.
311
317
  */
312
318
  /**
313
- * `resume <sessionId>` — reattach the interactive chat REPL to an existing
314
- * session (looks up the continuation token via `/v1/sessions` when omitted).
315
- * Same flags as `chat` (`--prod`, `--message`, `--json`, ). `--session`
316
- * is accepted as a redundant alias for the positional id.
319
+ * `resume [sessionId]` — reattach the interactive chat REPL to an existing
320
+ * session. When the id is omitted, picks the most recently updated
321
+ * followable session from `/v1/sessions` (same as `chat --resume`).
322
+ * Looks up the continuation token via `/v1/sessions` when
323
+ * `--continuation-token` is omitted. Same flags as `chat` (`--prod`,
324
+ * `--message`, `--json`, …). `--session` is accepted as a redundant
325
+ * alias for the positional id.
317
326
  */
318
327
  export async function cmdResume(
319
328
  sessionId: string | undefined,
320
329
  options: AxCliOptions
321
330
  ): Promise<number> {
322
331
  const resolved = trimNonEmpty(sessionId) ?? trimNonEmpty(options.sessionId);
323
- if (resolved === undefined) {
324
- process.stderr.write(
325
- `Usage: ${CLI_COMMAND_NAME} resume <sessionId> [--url | --prod] [--message <text>] [--json] [--text]\n` +
326
- ` [--continuation-token <t>] [--slug <slug>] [--team <id>]\n`
327
- );
328
- return 2;
332
+ if (resolved !== undefined) {
333
+ return cmdChat({
334
+ ...options,
335
+ sessionId: resolved,
336
+ resume: false,
337
+ });
329
338
  }
330
339
  return cmdChat({
331
340
  ...options,
332
- sessionId: resolved,
341
+ sessionId: undefined,
342
+ resume: true,
333
343
  });
334
344
  }
335
345
 
@@ -352,6 +362,35 @@ export async function cmdChat(options: AxCliOptions): Promise<number> {
352
362
  : (options.url ?? "http://127.0.0.1:3000"),
353
363
  });
354
364
 
365
+ let sessionId = trimNonEmpty(options.sessionId);
366
+ // `--resume` without an id → most recently updated followable session.
367
+ // An explicit `--session` / positional id always wins.
368
+ if (options.resume === true && sessionId === undefined) {
369
+ try {
370
+ const previous = await resolvePreviousSessionId({
371
+ baseUrl: target.agentUrl,
372
+ auth: {
373
+ bearerToken: options.bearerToken,
374
+ headers: target.headers,
375
+ },
376
+ fetchImpl: options.fetchImpl,
377
+ });
378
+ if (previous === undefined) {
379
+ process.stderr.write(
380
+ "No previous followable session found. Start one with " +
381
+ `\`${CLI_COMMAND_NAME} chat\` or pass a session id.\n`
382
+ );
383
+ return 1;
384
+ }
385
+ sessionId = previous;
386
+ } catch (error) {
387
+ process.stderr.write(
388
+ `${error instanceof Error ? error.message : String(error)}\n`
389
+ );
390
+ return 1;
391
+ }
392
+ }
393
+
355
394
  if (options.json) {
356
395
  if (message === undefined) {
357
396
  process.stderr.write("chat --json requires --message\n");
@@ -359,13 +398,13 @@ export async function cmdChat(options: AxCliOptions): Promise<number> {
359
398
  }
360
399
  let continuationToken = options.continuationToken;
361
400
  if (
362
- options.sessionId !== undefined &&
401
+ sessionId !== undefined &&
363
402
  (continuationToken === undefined || continuationToken === "")
364
403
  ) {
365
404
  try {
366
405
  continuationToken = await lookupSessionContinuation({
367
406
  baseUrl: target.agentUrl,
368
- sessionId: options.sessionId,
407
+ sessionId,
369
408
  auth: {
370
409
  bearerToken: options.bearerToken,
371
410
  headers: target.headers,
@@ -381,7 +420,7 @@ export async function cmdChat(options: AxCliOptions): Promise<number> {
381
420
  const result = await runSession({
382
421
  baseUrl: target.agentUrl,
383
422
  messages: [message],
384
- sessionId: options.sessionId,
423
+ sessionId,
385
424
  continuationToken,
386
425
  bearerToken: options.bearerToken,
387
426
  headers: target.headers,
@@ -409,7 +448,7 @@ export async function cmdChat(options: AxCliOptions): Promise<number> {
409
448
  await runChat({
410
449
  baseUrl: target.agentUrl,
411
450
  message,
412
- sessionId: options.sessionId,
451
+ sessionId,
413
452
  continuationToken: options.continuationToken,
414
453
  bearerToken: options.bearerToken,
415
454
  headers: target.headers,
@@ -892,7 +931,7 @@ export async function cmdSessions(options: AxCliOptions): Promise<number> {
892
931
  /**
893
932
  * `session <id>` — view a full session (trajectory text by default).
894
933
  * `--json` → trajectory JSON; `--events` (`rawEvents`) → raw event list.
895
- * Interactive reattach: `resume <id>` (or `chat --session`).
934
+ * Interactive reattach: `resume [id]` / `chat --resume` (or `chat --session`).
896
935
  */
897
936
  export async function cmdSession(
898
937
  sessionId: string | undefined,
@@ -674,7 +674,8 @@ async function deployOne(
674
674
  ` generation: ${detail.generation}\n` +
675
675
  ` kind: ${detail.deploymentKind}\n` +
676
676
  ` alias: ${aliasUrl}\n` +
677
- ` playground: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
677
+ ` playground: ${aliasUrl}/playground (sign in with the alias token)\n` +
678
+ ` or: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
678
679
  );
679
680
  return 0;
680
681
  }
@@ -795,6 +796,7 @@ export async function cmdDeploymentStatus(
795
796
  ` generation: ${detail.generation}`,
796
797
  ` kind: ${detail.deploymentKind}`,
797
798
  ` alias: ${aliasUrl}`,
799
+ ` playground: ${aliasUrl}/playground`,
798
800
  ];
799
801
  if (detail.gitRepoUrl !== null && detail.gitRepoUrl !== undefined) {
800
802
  lines.push(` repo: ${detail.gitRepoUrl} @ ${detail.gitRef ?? ""}`);
@@ -1285,7 +1287,8 @@ function aliasTokenBlock(aliasToken: string): string {
1285
1287
  return (
1286
1288
  `\n Alias token (shown ONCE — save it now, it cannot be retrieved again):\n\n` +
1287
1289
  ` ${aliasToken}\n\n` +
1288
- ` Send it as the X-Agent-Alias-Token header on requests to the alias URL.\n` +
1290
+ ` Send it as the X-Agent-Alias-Token header on requests to the alias URL,\n` +
1291
+ ` or paste it into the hosted playground (<alias>/playground) sign-in gate.\n` +
1289
1292
  ` Lost tokens: \`${CLI} rotate-token <slug>\` mints a replacement.\n\n`
1290
1293
  );
1291
1294
  }
@@ -196,7 +196,7 @@ async function cmdGithubForward(options: GithubCliOptions): Promise<number> {
196
196
  );
197
197
  return 1;
198
198
  }
199
- process.stderr.write(`[agent-serve] inferred --repo ${repo}\n`);
199
+ process.stderr.write(`[agentkit] inferred --repo ${repo}\n`);
200
200
  }
201
201
 
202
202
  const secret = options.secret ?? process.env.GITHUB_WEBHOOK_SECRET;
@@ -219,30 +219,30 @@ async function cmdGithubForward(options: GithubCliOptions): Promise<number> {
219
219
  });
220
220
 
221
221
  process.stderr.write(
222
- `[agent-serve] forwarding ${org !== undefined ? `org ${org}` : `repo ${repo}`} to ${targets.length} channel${targets.length === 1 ? "" : "s"}:\n`
222
+ `[agentkit] forwarding ${org !== undefined ? `org ${org}` : `repo ${repo}`} to ${targets.length} channel${targets.length === 1 ? "" : "s"}:\n`
223
223
  );
224
224
  for (const target of targets) {
225
225
  process.stderr.write(
226
- `[agent-serve] → ${targetLabel(target)} ${target.url} (${target.events.join(", ")})\n`
226
+ `[agentkit] → ${targetLabel(target)} ${target.url} (${target.events.join(", ")})\n`
227
227
  );
228
228
  }
229
229
  if (fanout) {
230
230
  process.stderr.write(
231
- `[agent-serve] fan-out proxy on ${forwardUrl} (union events: ${events.join(", ")})\n`
231
+ `[agentkit] fan-out proxy on ${forwardUrl} (union events: ${events.join(", ")})\n`
232
232
  );
233
233
  }
234
234
  if (secret === undefined || secret === "") {
235
235
  process.stderr.write(
236
- "[agent-serve] no signing secret — deliveries are unsigned. A `serve --dev` github channel accepts them; set GITHUB_WEBHOOK_SECRET on both sides to verify signatures (required for a non-dev target).\n"
236
+ "[agentkit] no signing secret — deliveries are unsigned. A `serve --dev` github channel accepts them; set GITHUB_WEBHOOK_SECRET on both sides to verify signatures (required for a non-dev target).\n"
237
237
  );
238
238
  }
239
239
  const envToken = detectGitHubEnvTokenOverride();
240
240
  if (envToken !== undefined) {
241
241
  process.stderr.write(
242
- `[agent-serve] warning: ${githubEnvTokenHint(envToken)}\n`
242
+ `[agentkit] warning: ${githubEnvTokenHint(envToken)}\n`
243
243
  );
244
244
  }
245
- process.stderr.write(`[agent-serve] running: gh ${redact(args)}\n`);
245
+ process.stderr.write(`[agentkit] running: gh ${redact(args)}\n`);
246
246
 
247
247
  try {
248
248
  return await spawnGh(args);
@@ -318,7 +318,7 @@ async function cmdGithubReplay(options: GithubCliOptions): Promise<number> {
318
318
  );
319
319
  if (unsupported.length > 0) {
320
320
  process.stderr.write(
321
- `[agent-serve] cannot synthesize (skipped): ${unsupported.join(", ")}\n`
321
+ `[agentkit] cannot synthesize (skipped): ${unsupported.join(", ")}\n`
322
322
  );
323
323
  }
324
324
  if (eventsToReplay.length === 0) {
@@ -364,9 +364,7 @@ async function cmdGithubReplay(options: GithubCliOptions): Promise<number> {
364
364
  check,
365
365
  });
366
366
  if ("error" in result) {
367
- process.stderr.write(
368
- `[agent-serve] skipping ${event}: ${result.error}\n`
369
- );
367
+ process.stderr.write(`[agentkit] skipping ${event}: ${result.error}\n`);
370
368
  continue;
371
369
  }
372
370
  synthesized.push(result);
@@ -378,7 +376,7 @@ async function cmdGithubReplay(options: GithubCliOptions): Promise<number> {
378
376
 
379
377
  const secret = options.secret ?? process.env.GITHUB_WEBHOOK_SECRET;
380
378
  process.stderr.write(
381
- `[agent-serve] replaying ${ref.label} (${synthesized
379
+ `[agentkit] replaying ${ref.label} (${synthesized
382
380
  .map((s) => s.eventName)
383
381
  .join(
384
382
  ", "
@@ -401,7 +399,7 @@ async function cmdGithubReplay(options: GithubCliOptions): Promise<number> {
401
399
 
402
400
  if (options.out !== undefined) {
403
401
  const file = await writeReplayFixture(options.out, event);
404
- process.stderr.write(`[agent-serve] wrote ${file}\n`);
402
+ process.stderr.write(`[agentkit] wrote ${file}\n`);
405
403
  }
406
404
 
407
405
  const label = eventLabel(event);
@@ -436,7 +434,7 @@ async function cmdGithubReplay(options: GithubCliOptions): Promise<number> {
436
434
  `${delivery.label} ${delivery.status || "ERR"}${delivery.ok ? "" : " (rejected)"}`
437
435
  )
438
436
  .join(", ") || "(no matching channel)";
439
- process.stderr.write(`[agent-serve] ${label} → ${summary}\n`);
437
+ process.stderr.write(`[agentkit] ${label} → ${summary}\n`);
440
438
  }
441
439
  }
442
440
 
@@ -747,7 +745,7 @@ async function ensureWebhookExtension(
747
745
  };
748
746
  }
749
747
  process.stderr.write(
750
- `[agent-serve] installing ${GITHUB_WEBHOOK_EXTENSION}@${GITHUB_WEBHOOK_EXTENSION_PIN}…\n`
748
+ `[agentkit] installing ${GITHUB_WEBHOOK_EXTENSION}@${GITHUB_WEBHOOK_EXTENSION_PIN}…\n`
751
749
  );
752
750
  if (
753
751
  (await tryExec({
@@ -231,7 +231,7 @@ export class CursorAccountMcpBridge {
231
231
  )
232
232
  .join(", ");
233
233
  this.logger(
234
- `[agent-serve] Cursor account MCP connection "${ref.name}": ${
234
+ `[agentkit] Cursor account MCP connection "${ref.name}": ${
235
235
  catalog.servers.length === 0
236
236
  ? "no matching connectors on this account — connect servers in Cursor (dashboard → MCP) or fix the `servers` filter"
237
237
  : summary
@@ -242,7 +242,7 @@ export class CursorAccountMcpBridge {
242
242
  );
243
243
  if (needsAuth.length > 0) {
244
244
  this.logger(
245
- `[agent-serve] Cursor account MCP connection "${ref.name}": ${needsAuth
245
+ `[agentkit] Cursor account MCP connection "${ref.name}": ${needsAuth
246
246
  .map((server) => server.serverIdentifier)
247
247
  .join(
248
248
  ", "
@@ -252,7 +252,7 @@ export class CursorAccountMcpBridge {
252
252
  })
253
253
  .catch((error: unknown) => {
254
254
  this.logger(
255
- `[agent-serve] Cursor account MCP connection "${ref.name}" preflight failed: ${describeError(error)}`
255
+ `[agentkit] Cursor account MCP connection "${ref.name}" preflight failed: ${describeError(error)}`
256
256
  );
257
257
  });
258
258
  }
@@ -445,7 +445,7 @@ export class CursorBackendClient {
445
445
  value: body.data.accessToken,
446
446
  expiresAtMs: jwtExpiryMs(body.data.accessToken),
447
447
  };
448
- this.logger("[agent-serve] cursor account: minted a fresh access token");
448
+ this.logger("[agentkit] cursor account: minted a fresh access token");
449
449
  return body.data.accessToken;
450
450
  }
451
451
  }
@@ -73,18 +73,23 @@ const storedCredentialsSchema: z.ZodObject<
73
73
  export type StoredCredentials = z.infer<typeof storedCredentialsSchema>;
74
74
 
75
75
  /**
76
- * `~/.config/agent-serve/credentials.json` (0600), honoring
77
- * `XDG_CONFIG_HOME`. `AGENT_SERVE_CONFIG_DIR` pins the directory outright
78
- * (tests, containers).
76
+ * `~/.config/agent-serve/` (honoring `XDG_CONFIG_HOME`), the host-level
77
+ * config directory for credentials and the update-check cache.
78
+ * `AGENT_SERVE_CONFIG_DIR` pins the directory outright (tests, containers).
79
79
  */
80
- export function credentialsFilePath(): string {
81
- const configDir =
80
+ export function configDirPath(): string {
81
+ return (
82
82
  process.env.AGENT_SERVE_CONFIG_DIR ??
83
83
  join(
84
84
  process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
85
85
  "agent-serve"
86
- );
87
- return join(configDir, "credentials.json");
86
+ )
87
+ );
88
+ }
89
+
90
+ /** `<configDir>/credentials.json` (0600). */
91
+ export function credentialsFilePath(): string {
92
+ return join(configDirPath(), "credentials.json");
88
93
  }
89
94
 
90
95
  export async function readStoredCredentials(): Promise<
@@ -148,14 +148,14 @@ export class CursorGithubCredentialProvider {
148
148
  this.scheduleRefresh();
149
149
  }
150
150
  this.logger(
151
- `[agent-serve] GitHub auth: refreshed Cursor account credential for ${this.repos.length} repo(s)`
151
+ `[agentkit] GitHub auth: refreshed Cursor account credential for ${this.repos.length} repo(s)`
152
152
  );
153
153
  return result.token;
154
154
  } catch (error) {
155
155
  const message = error instanceof Error ? error.message : String(error);
156
156
  if (this.cached !== undefined && this.now() < this.cached.expiresAtMs) {
157
157
  this.logger(
158
- `[agent-serve] GitHub auth: Cursor account credential refresh failed; using the current lease: ${message}`
158
+ `[agentkit] GitHub auth: Cursor account credential refresh failed; using the current lease: ${message}`
159
159
  );
160
160
  this.cached.refreshAtMs = Math.min(
161
161
  this.cached.expiresAtMs,
@@ -207,7 +207,7 @@ export class CursorGithubCredentialProvider {
207
207
  this.refreshTimer = setTimeout(() => {
208
208
  void this.getToken().catch((error: unknown) => {
209
209
  this.logger(
210
- `[agent-serve] GitHub auth: Cursor account credential refresh failed: ${
210
+ `[agentkit] GitHub auth: Cursor account credential refresh failed: ${
211
211
  error instanceof Error ? error.message : String(error)
212
212
  }`
213
213
  );
@@ -68,7 +68,10 @@ export interface CursorEventRelayOptions {
68
68
  repos: readonly string[];
69
69
  events: readonly string[];
70
70
  stateDir: string;
71
- /** Called for each matching event; failures are logged, not retried. */
71
+ /**
72
+ * Called for each matching event; failures retry a bounded number of
73
+ * times, then log and skip.
74
+ */
72
75
  dispatch: (event: CursorScmEvent) => Promise<void>;
73
76
  logger: (line: string) => void;
74
77
  /** Test seam. Defaults to global fetch. */
@@ -213,6 +216,12 @@ export class CursorEventRelay {
213
216
  events: [...this.options.events],
214
217
  })),
215
218
  }),
219
+ // Empty subscriptions drop this consumer's registration on clean stop
220
+ // instead of leaving it to lapse by TTL.
221
+ buildDeregistrationBody: (consumerId) => ({
222
+ consumerId,
223
+ subscriptions: [],
224
+ }),
216
225
  applyRegistrationResponse: (json) => {
217
226
  const result = json as { repos?: SubscriptionStatus[] };
218
227
  const statuses = result.repos ?? [];
@@ -263,7 +272,7 @@ export class CursorEventRelay {
263
272
  if (!this.loggedSkippedRepos.has(event.repo)) {
264
273
  this.loggedSkippedRepos.add(event.repo);
265
274
  this.options.logger(
266
- `[agent-serve] cursor-events: skipping events for ${event.repo}: not in this host's subscription (another consumer of this API key likely subscribed it)`
275
+ `[agentkit] cursor-events: skipping events for ${event.repo}: not in this host's subscription (another consumer of this API key likely subscribed it)`
267
276
  );
268
277
  }
269
278
  return false;
@@ -12,6 +12,8 @@ import { randomUUID } from "node:crypto";
12
12
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
 
15
+ import { LOG_PREFIX } from "./distribution.js";
16
+
15
17
  const OFFSET_SNAPSHOT_VERSION = 1 as const;
16
18
  const DEFAULT_POLL_WAIT_MS = 25_000;
17
19
  const DEFAULT_POLL_LIMIT = 100;
@@ -21,6 +23,11 @@ const DEFAULT_IDLE_POLL_DELAY_MS = 250;
21
23
  const MIN_BACKOFF_MS = 1_000;
22
24
  const MAX_BACKOFF_MS = 30_000;
23
25
  const AUTH_FAILURE_BACKOFF_MS = 60_000;
26
+ /** Attempts per event before the offset advances past it. */
27
+ const DISPATCH_ATTEMPTS = 3;
28
+ const DISPATCH_RETRY_DELAYS_MS = [250, 1_000];
29
+ /** Budget for the best-effort deregistration PUT during stop(). */
30
+ const DEREGISTER_TIMEOUT_MS = 1_500;
24
31
 
25
32
  interface OffsetSnapshot {
26
33
  version: typeof OFFSET_SNAPSHOT_VERSION;
@@ -98,7 +105,7 @@ interface PollResponse<TEvent> {
98
105
  * anchor/poll/dedupe/offset loop, and backoff — is shared.
99
106
  */
100
107
  export interface RelayAdapter<TEvent extends { offset: string }> {
101
- /** Log fragment, e.g. `cursor-events` → `[agent-serve] cursor-events: …`. */
108
+ /** Log fragment, e.g. `cursor-events` → `[agentkit] cursor-events: …`. */
102
109
  readonly logPrefix: string;
103
110
  /** Subscription registration path, e.g. `/v0/scm-events/subscriptions`. */
104
111
  readonly subscribePath: string;
@@ -106,6 +113,14 @@ export interface RelayAdapter<TEvent extends { offset: string }> {
106
113
  readonly pollPath: string;
107
114
  /** The registration request body for this consumer id. */
108
115
  buildRegistrationBody(consumerId: string): unknown;
116
+ /**
117
+ * The empty-scope registration body used to deregister this consumer on a
118
+ * clean stop. When present, `stop()` best-effort PUTs it so the consumer's
119
+ * registration does not linger as a ghost until its server-side TTL (a
120
+ * ghost with this host's agent names makes name routing ambiguous for a
121
+ * redeployed sibling until it expires).
122
+ */
123
+ buildDeregistrationBody?(consumerId: string): unknown;
109
124
  /**
110
125
  * Apply the (ok) registration response: update the accepted-scope filter
111
126
  * and return the human-readable outcome lines to log. The core logs them
@@ -119,7 +134,10 @@ export interface RelayAdapter<TEvent extends { offset: string }> {
119
134
  * the offset but are not dispatched.
120
135
  */
121
136
  shouldDispatch(event: TEvent): boolean;
122
- /** Handle one matching event. Failures are logged, not retried. */
137
+ /**
138
+ * Handle one matching event. Failures retry a bounded number of times
139
+ * ({@link DISPATCH_ATTEMPTS}), then log and skip.
140
+ */
123
141
  dispatch(event: TEvent): Promise<void>;
124
142
  }
125
143
 
@@ -145,6 +163,14 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
145
163
  private consumerId = "";
146
164
  /** undefined until the first registration, so a clean first success logs. */
147
165
  private lastRegistrationLog: string | undefined;
166
+ /** Whether any registration succeeded (gates the stop-time deregister). */
167
+ private registeredOnce = false;
168
+ /**
169
+ * The in-flight heartbeat re-registration, if any. `stop()` awaits it
170
+ * before deregistering so a just-sent heartbeat cannot land after the
171
+ * deregistration PUT and resurrect the registration for a full TTL.
172
+ */
173
+ private heartbeatPromise: Promise<boolean> | undefined;
148
174
 
149
175
  constructor(private readonly options: CursorRelayCoreOptions<TEvent>) {
150
176
  this.offsetStore = new RelayOffsetStore(options.stateDir);
@@ -168,7 +194,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
168
194
  const registered = await this.registerUntilSuccess();
169
195
  if (!registered) return;
170
196
  this.registerTimer = setInterval(() => {
171
- void this.registerOnce();
197
+ this.heartbeatPromise = this.registerOnce();
172
198
  }, DEFAULT_REGISTER_INTERVAL_MS);
173
199
  this.registerTimer.unref?.();
174
200
  await this.consumeLoop();
@@ -227,11 +253,61 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
227
253
  timer.unref?.();
228
254
  }),
229
255
  ]);
256
+ // A heartbeat re-registration may be in flight (its fetch was just
257
+ // aborted); let it settle so it cannot land after the deregistration.
258
+ await this.heartbeatPromise?.catch(() => {});
259
+ await this.deregister();
260
+ }
261
+
262
+ /**
263
+ * Best-effort empty-scope PUT on clean stop, so this consumer's
264
+ * registration is dropped immediately instead of lingering until its
265
+ * server-side TTL. A ghost registration keeps this host's agent names
266
+ * live and makes name routing ambiguous for a redeployed sibling with a
267
+ * fresh consumer id. Failures only log — the TTL still cleans up.
268
+ */
269
+ private async deregister(): Promise<void> {
270
+ const adapter = this.options.adapter;
271
+ if (
272
+ adapter.buildDeregistrationBody === undefined ||
273
+ !this.registeredOnce ||
274
+ this.consumerId === ""
275
+ ) {
276
+ return;
277
+ }
278
+ const url = `${this.options.baseUrl.replace(/\/+$/, "")}${adapter.subscribePath}`;
279
+ const controller = new AbortController();
280
+ const timer = setTimeout(() => controller.abort(), DEREGISTER_TIMEOUT_MS);
281
+ timer.unref?.();
282
+ try {
283
+ const response = await this.fetchImpl(url, {
284
+ method: "PUT",
285
+ headers: {
286
+ authorization: `Bearer ${this.options.apiKey}`,
287
+ "content-type": "application/json",
288
+ },
289
+ body: JSON.stringify(adapter.buildDeregistrationBody(this.consumerId)),
290
+ signal: controller.signal,
291
+ });
292
+ if (response.ok) {
293
+ this.log("deregistered on shutdown");
294
+ } else {
295
+ this.log(
296
+ `deregistration failed: ${response.status} (registration lapses by TTL)`
297
+ );
298
+ }
299
+ } catch (error) {
300
+ this.log(
301
+ `deregistration failed: ${describeRelayError(error)} (registration lapses by TTL)`
302
+ );
303
+ } finally {
304
+ clearTimeout(timer);
305
+ }
230
306
  }
231
307
 
232
308
  private log(line: string): void {
233
309
  this.options.logger(
234
- `[agent-serve] ${this.options.adapter.logPrefix}: ${line}`
310
+ `${LOG_PREFIX} ${this.options.adapter.logPrefix}: ${line}`
235
311
  );
236
312
  }
237
313
 
@@ -289,6 +365,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
289
365
  this.logRegistrationOutcome(
290
366
  this.options.adapter.applyRegistrationResponse(json)
291
367
  );
368
+ this.registeredOnce = true;
292
369
  return true;
293
370
  } catch (error) {
294
371
  if (this.isAbort(error)) return false;
@@ -371,13 +448,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
371
448
  // on replay after a crash between dispatch and save).
372
449
  if (compareStreamOffsets(event.offset, offset) <= 0) continue;
373
450
  if (this.options.adapter.shouldDispatch(event)) {
374
- try {
375
- await this.options.adapter.dispatch(event);
376
- } catch (error) {
377
- this.log(
378
- `dispatch failed at offset ${event.offset}: ${describeRelayError(error)}`
379
- );
380
- }
451
+ await this.dispatchWithRetry(event);
381
452
  }
382
453
  offset = event.offset;
383
454
  }
@@ -401,6 +472,43 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
401
472
  }
402
473
  }
403
474
 
475
+ /**
476
+ * Dispatch one event with a small bounded retry before the offset advances
477
+ * past it. Transient local failures (e.g. a connection reset while POSTing
478
+ * to the channel) used to drop the event outright; a poison event still
479
+ * only costs {@link DISPATCH_ATTEMPTS} tries and is then skipped, keeping
480
+ * at-least-once semantics without a retry loop.
481
+ */
482
+ private async dispatchWithRetry(event: TEvent): Promise<void> {
483
+ for (let attempt = 1; attempt <= DISPATCH_ATTEMPTS; attempt++) {
484
+ if (attempt > 1 && !this.running) {
485
+ this.log(
486
+ `dispatch abandoned at offset ${event.offset}: relay stopping`
487
+ );
488
+ return;
489
+ }
490
+ try {
491
+ await this.options.adapter.dispatch(event);
492
+ return;
493
+ } catch (error) {
494
+ if (attempt === DISPATCH_ATTEMPTS) {
495
+ this.log(
496
+ `dispatch failed at offset ${event.offset} after ${attempt} attempts: ${describeRelayError(error)}`
497
+ );
498
+ return;
499
+ }
500
+ this.log(
501
+ `dispatch attempt ${attempt} failed at offset ${event.offset}: ${describeRelayError(error)}; retrying`
502
+ );
503
+ await this.sleep(
504
+ DISPATCH_RETRY_DELAYS_MS[
505
+ Math.min(attempt - 1, DISPATCH_RETRY_DELAYS_MS.length - 1)
506
+ ]
507
+ );
508
+ }
509
+ }
510
+ }
511
+
404
512
  /** Log the failure, sleep, and return the backoff for the NEXT failure. */
405
513
  private async backoffAfterHttpFailure(
406
514
  response: Response,
@@ -44,8 +44,8 @@ export interface CursorSlackRelayOptions {
44
44
  agents: readonly CursorSlackAgentRegistration[];
45
45
  stateDir: string;
46
46
  /**
47
- * Called for each event addressed to this consumer's agents; failures are
48
- * logged, not retried.
47
+ * Called for each event addressed to this consumer's agents; failures
48
+ * retry a bounded number of times, then log and skip.
49
49
  */
50
50
  dispatch: (envelope: CursorSlackRelayEvent) => Promise<void>;
51
51
  logger: (line: string) => void;
@@ -122,7 +122,7 @@ export function createCursorSlackApiCaller(args: {
122
122
  };
123
123
  } catch (error) {
124
124
  args.logger?.(
125
- `[agent-serve] cursor-slack: ${method} proxy call failed: ${describeRelayError(error)}`
125
+ `[agentkit] cursor-slack: ${method} proxy call failed: ${describeRelayError(error)}`
126
126
  );
127
127
  return { ok: false, error: "cursor_backend_unreachable" };
128
128
  }
@@ -163,6 +163,10 @@ export class CursorSlackRelay {
163
163
  })),
164
164
  };
165
165
  },
166
+ // Empty agents drop this consumer's registration on clean stop, so a
167
+ // redeployed sibling's identical agent names never tie with this
168
+ // host's ghost until the server-side TTL lapses.
169
+ buildDeregistrationBody: (consumerId) => ({ consumerId, agents: [] }),
166
170
  applyRegistrationResponse: (json) => {
167
171
  const result = json as RegistrationResponse;
168
172
  const agentStatuses = result.agents ?? [];
@@ -25,6 +25,14 @@ export const LEGACY_PACKAGE_NAME = "@anysphere/agent-serve";
25
25
  */
26
26
  export const AGENTKIT_PACKAGE_NAME = "@cursor/agentkit";
27
27
 
28
+ /**
29
+ * Bracketed product tag for stderr / ServeOptions.logger lines
30
+ * (`[agentkit] …`, `[agentkit/slack] …`).
31
+ */
32
+ export const LOG_PRODUCT = "agentkit";
33
+ export const LOG_PREFIX: string = `[${LOG_PRODUCT}]`;
34
+ export const SLACK_LOG_PREFIX: string = `[${LOG_PRODUCT}/slack]`;
35
+
28
36
  /**
29
37
  * Command name this CLI was invoked as. Both bins point at the same entry;
30
38
  * keep help text and hints copy-pasteable for whichever the user typed.