@cursor/july 0.1.32 → 0.1.34

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 (186) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts.map +1 -1
  3. package/dist/artifacts.js +0 -5
  4. package/dist/bin/agent-serve.d.ts +1 -0
  5. package/dist/bin/agent-serve.d.ts.map +1 -1
  6. package/dist/bin/agent-serve.js +32 -2
  7. package/dist/channels/github/types.d.ts +10 -7
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.C2aVczmo.js → app.FPupl4SP.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.WoYunhnT.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.DLEzetbD.js → VPLocalSearchBox.Cd182Cu0.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.BkOH1oMp.js → theme.BEM3Okcd.js} +2 -2
  15. package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
  16. package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
  17. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
  18. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +3 -3
  20. package/dist/docs/concepts.html +3 -3
  21. package/dist/docs/deployment.html +3 -3
  22. package/dist/docs/evals.html +3 -3
  23. package/dist/docs/example-agents/approval-buddy.html +3 -3
  24. package/dist/docs/example-agents/benny.html +3 -3
  25. package/dist/docs/example-agents/bugbot.html +3 -3
  26. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  27. package/dist/docs/example-agents/codeowners-review.html +3 -3
  28. package/dist/docs/example-agents/concierge.html +3 -3
  29. package/dist/docs/example-agents/fsd.html +3 -3
  30. package/dist/docs/example-agents/index.html +3 -3
  31. package/dist/docs/example-agents/knowledge-base.html +3 -3
  32. package/dist/docs/example-agents/oncall.html +3 -3
  33. package/dist/docs/example-agents/security-reviewer.html +3 -3
  34. package/dist/docs/example-agents/slack-agent.html +3 -3
  35. package/dist/docs/example-agents/weather-agent.html +10 -8
  36. package/dist/docs/guides/agent-to-agent.html +3 -3
  37. package/dist/docs/guides/cloud-runtime.html +5 -5
  38. package/dist/docs/guides/github.html +3 -3
  39. package/dist/docs/guides/human-in-the-loop.html +3 -3
  40. package/dist/docs/guides/mcp-oauth.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/prompt.html +3 -3
  57. package/dist/docs/reference/schedules.html +3 -3
  58. package/dist/docs/reference/sessions.html +3 -3
  59. package/dist/docs/reference/skills.html +3 -3
  60. package/dist/docs/reference/subagents.html +3 -3
  61. package/dist/docs/reference/tools.html +3 -3
  62. package/dist/docs/scaffolding-agents.html +3 -3
  63. package/dist/docs/storage.html +3 -3
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/files-backends/agent-store-presigned-url.d.ts.map +1 -1
  66. package/dist/files-backends/agent-store-presigned-url.js +19 -810
  67. package/dist/files.d.ts +20 -0
  68. package/dist/files.d.ts.map +1 -1
  69. package/dist/files.js +20 -0
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -2
  73. package/dist/internal/artifacts-store.d.ts +14 -6
  74. package/dist/internal/artifacts-store.d.ts.map +1 -1
  75. package/dist/internal/artifacts-store.js +160 -16
  76. package/dist/internal/cli-convert-automation.d.ts +39 -0
  77. package/dist/internal/cli-convert-automation.d.ts.map +1 -0
  78. package/dist/internal/cli-convert-automation.js +280 -0
  79. package/dist/internal/cli-deploy.d.ts +2 -0
  80. package/dist/internal/cli-deploy.d.ts.map +1 -1
  81. package/dist/internal/cli-deploy.js +1 -1
  82. package/dist/internal/cli-mcp-oauth.d.ts +23 -0
  83. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  84. package/dist/internal/cli-mcp-oauth.js +57 -33
  85. package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
  86. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
  87. package/dist/internal/convert-automation/convert-workflow.js +998 -0
  88. package/dist/internal/convert-automation/index.d.ts +3 -0
  89. package/dist/internal/convert-automation/index.d.ts.map +1 -0
  90. package/dist/internal/convert-automation/index.js +1 -0
  91. package/dist/internal/convert-automation/slug.d.ts +11 -0
  92. package/dist/internal/convert-automation/slug.d.ts.map +1 -0
  93. package/dist/internal/convert-automation/slug.js +36 -0
  94. package/dist/internal/convert-automation/types.d.ts +191 -0
  95. package/dist/internal/convert-automation/types.d.ts.map +1 -0
  96. package/dist/internal/convert-automation/types.js +13 -0
  97. package/dist/internal/cursor-relay-core.d.ts +12 -0
  98. package/dist/internal/cursor-relay-core.d.ts.map +1 -1
  99. package/dist/internal/cursor-relay-core.js +42 -8
  100. package/dist/internal/deploy-client.d.ts +31 -0
  101. package/dist/internal/deploy-client.d.ts.map +1 -1
  102. package/dist/internal/deploy-client.js +48 -0
  103. package/dist/internal/discovery.d.ts.map +1 -1
  104. package/dist/internal/discovery.js +3 -32
  105. package/dist/internal/init-project.d.ts +1 -0
  106. package/dist/internal/init-project.d.ts.map +1 -1
  107. package/dist/internal/init-project.js +1 -1
  108. package/dist/internal/mcp-endpoint.d.ts +6 -0
  109. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  110. package/dist/internal/mcp-endpoint.js +168 -0
  111. package/dist/internal/resolved-connections.d.ts +15 -0
  112. package/dist/internal/resolved-connections.d.ts.map +1 -1
  113. package/dist/internal/resolved-connections.js +12 -5
  114. package/dist/internal/review-comments.d.ts +9 -1
  115. package/dist/internal/review-comments.d.ts.map +1 -1
  116. package/dist/internal/review-comments.js +76 -0
  117. package/dist/internal/server.d.ts.map +1 -1
  118. package/dist/internal/server.js +83 -8
  119. package/dist/internal/session-engine.d.ts +56 -3
  120. package/dist/internal/session-engine.d.ts.map +1 -1
  121. package/dist/internal/session-engine.js +252 -83
  122. package/dist/internal/storage-coordinator.d.ts +0 -15
  123. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  124. package/dist/internal/storage-coordinator.js +0 -52
  125. package/dist/internal/workspace.d.ts +10 -5
  126. package/dist/internal/workspace.d.ts.map +1 -1
  127. package/dist/internal/workspace.js +13 -0
  128. package/dist/memory.d.ts +22 -6
  129. package/dist/memory.d.ts.map +1 -1
  130. package/dist/memory.js +100 -8
  131. package/dist/playground/assets/index-CDDWw0YX.js +85 -0
  132. package/dist/playground/assets/index-MVuNTd8v.css +1 -0
  133. package/dist/playground/index.html +2 -2
  134. package/dist/storage.d.ts +0 -4
  135. package/dist/storage.d.ts.map +1 -1
  136. package/dist/storage.js +0 -4
  137. package/dist/types.d.ts +18 -0
  138. package/dist/types.d.ts.map +1 -1
  139. package/docs/example-agents/weather-agent.md +29 -11
  140. package/docs/guides/cloud-runtime.md +6 -4
  141. package/package.json +3 -22
  142. package/src/artifacts.ts +0 -6
  143. package/src/bin/agent-serve.ts +36 -2
  144. package/src/channels/github/types.ts +10 -7
  145. package/src/files-backends/agent-store-presigned-url.ts +7 -5
  146. package/src/files.ts +27 -0
  147. package/src/index.ts +6 -1
  148. package/src/internal/artifacts-store.ts +185 -22
  149. package/src/internal/cli-convert-automation.ts +352 -0
  150. package/src/internal/cli-deploy.ts +1 -1
  151. package/src/internal/cli-mcp-oauth.ts +69 -31
  152. package/src/internal/convert-automation/convert-workflow.ts +1422 -0
  153. package/src/internal/convert-automation/index.ts +10 -0
  154. package/src/internal/convert-automation/slug.ts +38 -0
  155. package/src/internal/convert-automation/types.ts +199 -0
  156. package/src/internal/cursor-relay-core.ts +62 -3
  157. package/src/internal/deploy-client.ts +78 -0
  158. package/src/internal/discovery.ts +3 -37
  159. package/src/internal/init-project.ts +1 -1
  160. package/src/internal/mcp-endpoint.ts +243 -0
  161. package/src/internal/resolved-connections.ts +32 -6
  162. package/src/internal/review-comments.ts +102 -2
  163. package/src/internal/server.ts +108 -2
  164. package/src/internal/session-engine.ts +329 -99
  165. package/src/internal/storage-coordinator.ts +0 -62
  166. package/src/internal/workspace.ts +19 -5
  167. package/src/memory.ts +126 -10
  168. package/src/storage.ts +0 -6
  169. package/src/types.ts +15 -0
  170. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  171. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  172. package/dist/docs/assets/chunks/@localSearchIndexroot.xgMnlUSO.js +0 -1
  173. package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
  174. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
  175. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
  176. package/dist/internal/json-dir-store.d.ts +0 -32
  177. package/dist/internal/json-dir-store.d.ts.map +0 -1
  178. package/dist/internal/persistence-coordinator.d.ts +0 -127
  179. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  180. package/dist/multi-tenant.d.ts +0 -80
  181. package/dist/multi-tenant.d.ts.map +0 -1
  182. package/dist/multi-tenant.js +0 -69
  183. package/dist/persistence.d.ts +0 -184
  184. package/dist/persistence.d.ts.map +0 -1
  185. package/dist/playground/assets/index-50PKeJlG.css +0 -1
  186. package/dist/playground/assets/index-C0f2Wl1q.js +0 -85
@@ -10,6 +10,7 @@
10
10
  * agent-serve run [--dir .] [--message "..."] [--json|--text]
11
11
  * agent-serve trajectory --events <file.ndjson>
12
12
  * agent-serve init [dir] [--json]
13
+ * agent-serve convert-automation <url> [--dir <path>] [--json]
13
14
  * agent-serve eval [--dir .] [evalId...]
14
15
  * agent-serve validate [--dir .]
15
16
  * agent-serve slack <subcommand>
@@ -69,7 +70,7 @@ const HELP = `${CLI} — serve a filesystem-defined Cursor agent over channels
69
70
  Usage:
70
71
  ${CLI} serve [--dir <path>] [--port <n>] [--host <host>] [--dev]
71
72
  [--mode single|multi] [--api-key <key>] [--no-schedules]
72
- [--public-url <url>]
73
+ [--public-url <url>] [--cloud-tools-url <url>]
73
74
  [--cursor-events --repo <owner/name>]...
74
75
  ${CLI} dev [path] [--port <n>] [--host <host>] …
75
76
  Same flags as serve; implies --dev. Path defaults to
@@ -100,6 +101,7 @@ Usage:
100
101
  ${CLI} call <tool> [--input <json>] [--dir <path> | --url <baseUrl> | --prod]
101
102
  [--session <id>] [--slug <slug>] [--team <id>]
102
103
  ${CLI} init [dir] [--json]
104
+ ${CLI} convert-automation <url> [--dir <path>] [--json]
103
105
  ${CLI} install-skills [--print] [--json]
104
106
  ${CLI} mcp --prod [--slug <slug>] [--team <id>] [--dir <path>]
105
107
  | --url <baseUrl> [--bearer-token <t>]
@@ -188,6 +190,17 @@ Commands:
188
190
  run login when unsigned, then print next steps. --json is
189
191
  machine-readable and never blocks on login. No directory:
190
192
  print the setup guide.
193
+ convert-automation
194
+ Export a Cursor Automation into an agentkit project. <url> is
195
+ the dashboard URL (…/automations/<uuid> or
196
+ …/custom-agents/<uuid>) or a bare uuid. Fetches the definition
197
+ with your Cursor credentials, writes the converted files into
198
+ --dir (default ./<automation-name>), fills in the init scaffold
199
+ around them (existing files are kept), and installs npm
200
+ dependencies. MCP servers convert to Cursor-account connections
201
+ resolved at runtime (manage authorization in Cursor, dashboard
202
+ -> MCP). Review the project, then run \`validate\` / \`dev\`.
203
+ Warnings never fail the command.
191
204
  install-skills
192
205
  Install (or refresh) the Agent SDK skills for coding agents in
193
206
  ~/.cursor/skills/agentkit and print where they landed. \`init\`
@@ -299,6 +312,9 @@ Options:
299
312
  (e.g. https://agent-serve.example.com). Required for peer
300
313
  connections (defineConnection({ agent })) on cloud-runtime
301
314
  turns; local turns always use loopback.
315
+ --cloud-tools-url serve: authenticated HTTP MCP URL for this deployment's
316
+ direct server-tool endpoint. Hosted AgentSDK deployments
317
+ configure this automatically.
302
318
  --no-schedules Disable the cron runner outside dev mode.
303
319
  --no-control-plane serve: do not start the bundled control plane (schedule
304
320
  and reminder clocks). Cursor hosting passes this so the
@@ -358,7 +374,8 @@ Options:
358
374
  --no-artifacts eval: do not write run artifacts.
359
375
  --skip-report eval: ignore reporters from evals.config.ts / eval files.
360
376
  --no-stream run/eval: silence live progress on stderr (JSON still prints).
361
- --json JSON output (init/install-skills/info/chat/eval/logs/
377
+ --json JSON output (init/convert-automation/install-skills/
378
+ info/chat/eval/logs/
362
379
  sessions/session/github; run defaults to JSON). logs:
363
380
  NDJSON progress/log events. session: trajectory JSON
364
381
  (not raw events).
@@ -486,6 +503,7 @@ async function main(argv: string[]): Promise<number> {
486
503
  name: { type: "string" },
487
504
  env: { type: "string" },
488
505
  "public-url": { type: "string" },
506
+ "cloud-tools-url": { type: "string" },
489
507
  "mount-path": { type: "string" },
490
508
  "base-url": { type: "string" },
491
509
  print: { type: "boolean", default: false },
@@ -604,6 +622,21 @@ async function main(argv: string[]): Promise<number> {
604
622
  });
605
623
  }
606
624
 
625
+ case "convert-automation": {
626
+ const { cmdConvertAutomation } = await import(
627
+ "../internal/cli-convert-automation.js"
628
+ );
629
+ const dirFlagPassed =
630
+ rest.includes("--dir") || rest.some((arg) => arg.startsWith("--dir="));
631
+ return cmdConvertAutomation({
632
+ url: positionals[0],
633
+ // Default target is ./<automation-name-slug>, not the cwd.
634
+ dir: dirFlagPassed ? dir : undefined,
635
+ json: values.json,
636
+ apiKey: values["api-key"],
637
+ });
638
+ }
639
+
607
640
  case "install-skills": {
608
641
  const { cmdInstallSkills } = await import("../internal/cli-skills.js");
609
642
  return cmdInstallSkills({
@@ -1170,6 +1203,7 @@ async function main(argv: string[]): Promise<number> {
1170
1203
  playground: values["no-playground"] === true ? false : undefined,
1171
1204
  docs: values["no-docs"] === true ? false : undefined,
1172
1205
  publicUrl: values["public-url"],
1206
+ cloudToolsUrl: values["cloud-tools-url"],
1173
1207
  cursorEvents:
1174
1208
  values["cursor-events"] === true
1175
1209
  ? { repos: values.repo ?? [] }
@@ -404,10 +404,12 @@ export type GitHubStatusHook = (
404
404
  ) => GitHubDispatchResult;
405
405
 
406
406
  /**
407
- * Catch-all for any webhook event (including ones with typed hooks). Runs
408
- * after the typed handler path when the typed hook is absent or returns
409
- * undefined-as-no-handler; for events with no typed hook, this is the only
410
- * dispatch path. Return `null` to ignore.
407
+ * Catch-all for webhook events with no typed hook (`push`,
408
+ * `pull_request_review`, `pull_request_review_thread`, …). Events that have
409
+ * a typed hook (`pull_request`, `issues`, `check_suite`, `check_run`,
410
+ * `workflow_run`, `status`, comments) NEVER reach this hook — when their
411
+ * typed hook is not declared, the delivery is skipped as `no_handler`
412
+ * instead of falling back here. Return `null` to ignore.
411
413
  */
412
414
  export type GitHubEventHook = (
413
415
  ctx: GitHubEventContext,
@@ -490,9 +492,10 @@ export interface GitHubChannelOptions {
490
492
  /** Opt-in legacy commit `status` webhook handler. No default dispatch. */
491
493
  onStatus?: GitHubStatusHook;
492
494
  /**
493
- * Catch-all webhook handler. Invoked for events without a typed hook, and
494
- * as a fallback when a typed hook is not declared. Prefer typed hooks when
495
- * the event is known.
495
+ * Catch-all webhook handler for events without a typed hook (`push`,
496
+ * `pull_request_review`, …). NOT a fallback: events with a typed hook are
497
+ * skipped as `no_handler` when that hook is undeclared, and never reach
498
+ * this one. Prefer typed hooks when the event has one.
496
499
  */
497
500
  onEvent?: GitHubEventHook;
498
501
  /**
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Re-export the agent-store SSRF pin used by `cursorHostedFiles`.
2
+ * Re-export bundled into dist by `scripts/bundle-publish-shims.mjs`.
3
3
  *
4
- * Source keeps a single dependency on `@anysphere/agent-store-sync` (dev-only
5
- * for publish). The build overwrites the emitted `dist` copy with an esbuild
6
- * bundle so the npm tarball does not need that private package at runtime.
4
+ * Imports the `presigned-url` entry point rather than the package root: the
5
+ * root is a barrel over the whole library, and bundling through it inlines
6
+ * every module the barrel can reach (sync engine, local sqlite index, secure
7
+ * open) into the published tarball. This module has no imports of its own, so
8
+ * the bundle stays limited to the validator.
7
9
  */
8
- export { createAgentStorePresignedUrlValidatorForBackend } from "@anysphere/agent-store-sync";
10
+ export { createAgentStorePresignedUrlValidatorForBackend } from "@anysphere/agent-store-sync/presigned-url";
package/src/files.ts CHANGED
@@ -140,6 +140,33 @@ export const fileKeys = {
140
140
  `${STORAGE_KEY_ROOT}/${agent}/files/sessions/${sessionId}/`,
141
141
  } as const;
142
142
 
143
+ /**
144
+ * Framework-owned keys for state that lives on the deployment's Agent Store
145
+ * but is not author `host.files` data: artifact rows and payloads, and the
146
+ * cross-session memory journal. Kept out of `files/` so author listings never
147
+ * see framework rows.
148
+ */
149
+ export const agentStoreKeys = {
150
+ artifactRow: (agent: string, id: string): string =>
151
+ `${STORAGE_KEY_ROOT}/${agent}/artifacts/rows/${encodeURIComponent(id)}.json`,
152
+ artifactRowPrefix: (agent: string): string =>
153
+ `${STORAGE_KEY_ROOT}/${agent}/artifacts/rows/`,
154
+ artifactContent: (agent: string, id: string): string =>
155
+ `${STORAGE_KEY_ROOT}/${agent}/artifacts/content/${encodeURIComponent(id)}`,
156
+ memoryDir: (agent: string): string => `${STORAGE_KEY_ROOT}/${agent}/memory`,
157
+ memoryJournal: (agent: string): string =>
158
+ `${STORAGE_KEY_ROOT}/${agent}/memory/journal.jsonl`,
159
+ memoryJournalRotated: (agent: string, stamp: string): string =>
160
+ `${STORAGE_KEY_ROOT}/${agent}/memory/journal-${stamp}.jsonl`,
161
+ } as const;
162
+
163
+ /**
164
+ * Deployment store FUSE mount on cloud-runtime VMs. Mirrors
165
+ * `AGENT_STORE_MOUNT_ROOT` / `AGENT_SERVE_DEPLOYMENT_STORE_MOUNT_NAME` in
166
+ * `@anysphere/constants` (not a dependency of this published package).
167
+ */
168
+ export const DEPLOYMENT_STORE_MOUNT_PATH = "/cursor/stores/deployment";
169
+
143
170
  /** Errors raised by the file surface. Thrown by sinks and by the framework. */
144
171
  export class FileConflictError extends Error {
145
172
  constructor(
package/src/index.ts CHANGED
@@ -76,6 +76,7 @@ export {
76
76
  MCP_CHANNEL_ID,
77
77
  MCP_ENDPOINT_PATH,
78
78
  mcpEndpointToolNames,
79
+ TOOL_MCP_ENDPOINT_PATH,
79
80
  } from "./internal/mcp-endpoint.js";
80
81
  export {
81
82
  ReminderValidationError,
@@ -91,7 +92,11 @@ export {
91
92
  UnknownToolError,
92
93
  } from "./internal/session-engine.js";
93
94
  export type { MemoryBackend, TurnMemoryRecord } from "./memory.js";
94
- export { fileMemoryBackend, memoryHook } from "./memory.js";
95
+ export {
96
+ agentStoreMemoryBackend,
97
+ fileMemoryBackend,
98
+ memoryHook,
99
+ } from "./memory.js";
95
100
  export type { PromptTag } from "./prompt.js";
96
101
  export { prompt } from "./prompt.js";
97
102
  export type {
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Durable artifact rows behind `ctx.artifacts` ({@link ArtifactsApi}).
2
+ * Durable artifact rows and payloads behind `ctx.artifacts`
3
+ * ({@link ArtifactsApi}).
3
4
  *
4
- * Mirror of host-kv: when `defineStorage` is authored with `get` + `list`,
5
- * rows live in that sink under `storageKeys.artifact`; otherwise one
6
- * JSON file per row under `--state-root/artifacts`.
5
+ * On Cursor-managed hosting rows and file payloads live on the deployment's
6
+ * Agent Store (the same store behind `ctx.host.files`, so they survive
7
+ * deploys and are visible on cloud VMs under the store mount); otherwise
8
+ * they land under `--state-root/artifacts` on local disk.
7
9
  */
8
10
 
9
11
  import { createHash, randomUUID } from "node:crypto";
@@ -17,6 +19,11 @@ import {
17
19
  } from "node:fs/promises";
18
20
  import { join } from "node:path";
19
21
  import { resolveArtifactsMax } from "../artifacts.js";
22
+ import { agentStoreKeys, type FileSink } from "../files.js";
23
+ import {
24
+ cursorHostedFiles,
25
+ isCursorHostedFilesAvailable,
26
+ } from "../files-backends/cursor-hosted.js";
20
27
  import type {
21
28
  ArtifactListFilter,
22
29
  ArtifactRecord,
@@ -24,23 +31,30 @@ import type {
24
31
  ArtifactTagInput,
25
32
  JsonValue,
26
33
  } from "../types.js";
27
- import type { StorageCoordinator } from "./storage-coordinator.js";
28
34
 
29
- // TODO(agent-stores): the KV backing store is migrating to Agent Stores.
30
- // Swap only this adapter to the Agent Stores API when that lands; the
31
- // engine, tool, HTTP, and playground layers must not change.
32
35
  interface ArtifactRowStore {
33
36
  get(id: string): Promise<ArtifactRecord | undefined>;
34
37
  put(record: ArtifactRecord): Promise<void>;
35
38
  delete(id: string): Promise<void>;
36
39
  list(): Promise<ArtifactRecord[]>;
40
+ /** Row count without hydrating bodies — the per-insert cap check. */
41
+ count(): Promise<number>;
42
+ }
43
+
44
+ /** File/blob payload bytes, keyed by artifact id (metadata lives on the row). */
45
+ interface ArtifactContentStore {
46
+ get(id: string): Promise<Uint8Array | undefined>;
47
+ put(id: string, body: Uint8Array): Promise<void>;
48
+ delete(id: string): Promise<void>;
37
49
  }
38
50
 
39
51
  export interface CreateArtifactsStoreOptions {
40
52
  stateRoot: string;
41
- storage?: StorageCoordinator;
53
+ agentName: string;
42
54
  /** `agent/artifacts.ts` config, when authored. */
43
55
  config?: ArtifactsConfig;
56
+ /** Explicit sink override (tests). Omitted → hosted store or local disk. */
57
+ sink?: FileSink;
44
58
  }
45
59
 
46
60
  /** Kind used by freeform tags when no kinds are declared. */
@@ -54,16 +68,23 @@ export const ARTIFACT_DEFAULT_KIND = "artifact";
54
68
  */
55
69
  export class ArtifactsStore {
56
70
  private readonly rows: ArtifactRowStore;
71
+ private readonly content: ArtifactContentStore;
57
72
  private readonly config: ArtifactsConfig | undefined;
58
73
  private readonly max: number;
59
74
  /** Serializes tags so read-modify-write upserts and eviction don't race. */
60
75
  private tagChain: Promise<unknown> = Promise.resolve();
61
76
 
62
77
  constructor(options: CreateArtifactsStoreOptions) {
63
- this.rows =
64
- options.storage?.supportsArtifacts === true
65
- ? createCoordinatorRowStore(options.storage)
66
- : createFilesystemRowStore(options.stateRoot);
78
+ const sink =
79
+ options.sink ??
80
+ (isCursorHostedFilesAvailable() ? cursorHostedFiles() : undefined);
81
+ if (sink === undefined) {
82
+ this.rows = createFilesystemRowStore(options.stateRoot);
83
+ this.content = createFilesystemContentStore(options.stateRoot);
84
+ } else {
85
+ this.rows = createSinkRowStore(sink, options.agentName);
86
+ this.content = createSinkContentStore(sink, options.agentName);
87
+ }
67
88
  this.config = options.config;
68
89
  this.max = resolveArtifactsMax(options.config);
69
90
  }
@@ -82,7 +103,9 @@ export class ArtifactsStore {
82
103
  input.key === undefined
83
104
  ? `art_${randomUUID()}`
84
105
  : artifactIdForKey(input.key);
85
- const existing = await this.rows.get(id);
106
+ // Fresh random ids can't collide; only keyed tags upsert.
107
+ const existing =
108
+ input.key === undefined ? undefined : await this.rows.get(id);
86
109
  const now = new Date().toISOString();
87
110
  const record: ArtifactRecord = {
88
111
  id,
@@ -96,6 +119,25 @@ export class ArtifactsStore {
96
119
  createdAt: existing?.createdAt ?? now,
97
120
  updatedAt: now,
98
121
  };
122
+ if (input.contents !== undefined) {
123
+ const body =
124
+ typeof input.contents === "string"
125
+ ? Buffer.from(input.contents, "utf8")
126
+ : input.contents;
127
+ await this.content.put(id, body);
128
+ record.content = {
129
+ size: body.byteLength,
130
+ contentType: input.contentType,
131
+ };
132
+ } else if (existing?.content !== undefined) {
133
+ if (existing.sessionId === record.sessionId) {
134
+ record.content = existing.content;
135
+ } else {
136
+ // Keys are deployment-global: rebinding a keyed row to another
137
+ // session must not carry the prior session's payload with it.
138
+ await this.content.delete(id);
139
+ }
140
+ }
99
141
  await this.rows.put(record);
100
142
  if (existing === undefined) {
101
143
  await this.evictPastCap();
@@ -103,6 +145,15 @@ export class ArtifactsStore {
103
145
  return record;
104
146
  }
105
147
 
148
+ get(id: string): Promise<ArtifactRecord | undefined> {
149
+ return this.rows.get(id);
150
+ }
151
+
152
+ /** Payload bytes for one artifact; undefined when it carries none. */
153
+ getContent(id: string): Promise<Uint8Array | undefined> {
154
+ return this.content.get(id);
155
+ }
156
+
106
157
  async list(filter?: ArtifactListFilter): Promise<ArtifactRecord[]> {
107
158
  const records = await this.rows.list();
108
159
  return records
@@ -152,6 +203,9 @@ export class ArtifactsStore {
152
203
  }
153
204
 
154
205
  private async evictPastCap(): Promise<void> {
206
+ if ((await this.rows.count()) <= this.max) {
207
+ return;
208
+ }
155
209
  const records = await this.rows.list();
156
210
  if (records.length <= this.max) {
157
211
  return;
@@ -159,6 +213,9 @@ export class ArtifactsStore {
159
213
  const oldestFirst = records.sort((a, b) => byNewestUpdated(b, a));
160
214
  for (const record of oldestFirst.slice(0, records.length - this.max)) {
161
215
  await this.rows.delete(record.id);
216
+ if (record.content !== undefined) {
217
+ await this.content.delete(record.id);
218
+ }
162
219
  }
163
220
  }
164
221
  }
@@ -206,23 +263,90 @@ function isArtifactRecord(
206
263
  return true;
207
264
  }
208
265
 
209
- function createCoordinatorRowStore(
210
- storage: StorageCoordinator
266
+ function parseRecordBytes(
267
+ bytes: Uint8Array | undefined
268
+ ): ArtifactRecord | undefined {
269
+ if (bytes === undefined) {
270
+ return undefined;
271
+ }
272
+ let value: JsonValue;
273
+ try {
274
+ value = JSON.parse(Buffer.from(bytes).toString("utf8")) as JsonValue;
275
+ } catch {
276
+ // A corrupt row must not poison list().
277
+ return undefined;
278
+ }
279
+ return isArtifactRecord(value) ? value : undefined;
280
+ }
281
+
282
+ function sinkBody(
283
+ found: Awaited<ReturnType<FileSink["get"]>>
284
+ ): Uint8Array | undefined {
285
+ if (found === undefined) {
286
+ return undefined;
287
+ }
288
+ return found instanceof Uint8Array ? found : found.body;
289
+ }
290
+
291
+ /** Rows as one JSON file each on the deployment's Agent Store. */
292
+ function createSinkRowStore(
293
+ sink: FileSink,
294
+ agentName: string
211
295
  ): ArtifactRowStore {
296
+ const key = (id: string): string => agentStoreKeys.artifactRow(agentName, id);
212
297
  return {
213
298
  async get(id): Promise<ArtifactRecord | undefined> {
214
- const value = await storage.artifactGet(id);
215
- return isArtifactRecord(value) ? value : undefined;
299
+ return parseRecordBytes(sinkBody(await sink.get(key(id))));
216
300
  },
217
301
  async put(record): Promise<void> {
218
- await storage.artifactPut(record.id, record as unknown as JsonValue);
302
+ await sink.put(
303
+ key(record.id),
304
+ Buffer.from(JSON.stringify(record), "utf8")
305
+ );
219
306
  },
220
307
  async delete(id): Promise<void> {
221
- await storage.artifactDelete(id);
308
+ await sink.delete(key(id));
222
309
  },
223
310
  async list(): Promise<ArtifactRecord[]> {
224
- const values = await storage.artifactList();
225
- return values.filter(isArtifactRecord);
311
+ const entries = await sink.list(
312
+ agentStoreKeys.artifactRowPrefix(agentName)
313
+ );
314
+ const bodies = await Promise.all(
315
+ entries.map((entry) => sink.get(entry.key))
316
+ );
317
+ const records: ArtifactRecord[] = [];
318
+ for (const body of bodies) {
319
+ const record = parseRecordBytes(sinkBody(body));
320
+ if (record !== undefined) {
321
+ records.push(record);
322
+ }
323
+ }
324
+ return records;
325
+ },
326
+ async count(): Promise<number> {
327
+ const entries = await sink.list(
328
+ agentStoreKeys.artifactRowPrefix(agentName)
329
+ );
330
+ return entries.length;
331
+ },
332
+ };
333
+ }
334
+
335
+ function createSinkContentStore(
336
+ sink: FileSink,
337
+ agentName: string
338
+ ): ArtifactContentStore {
339
+ const key = (id: string): string =>
340
+ agentStoreKeys.artifactContent(agentName, id);
341
+ return {
342
+ async get(id): Promise<Uint8Array | undefined> {
343
+ return sinkBody(await sink.get(key(id)));
344
+ },
345
+ async put(id, body): Promise<void> {
346
+ await sink.put(key(id), body);
347
+ },
348
+ async delete(id): Promise<void> {
349
+ await sink.delete(key(id));
226
350
  },
227
351
  };
228
352
  }
@@ -286,5 +410,44 @@ function createFilesystemRowStore(stateRoot: string): ArtifactRowStore {
286
410
  }
287
411
  return records;
288
412
  },
413
+ async count(): Promise<number> {
414
+ try {
415
+ const names = await readdir(dir);
416
+ return names.filter(
417
+ (name) => !name.startsWith(".") && name.endsWith(".json")
418
+ ).length;
419
+ } catch (error) {
420
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
421
+ return 0;
422
+ }
423
+ throw error;
424
+ }
425
+ },
426
+ };
427
+ }
428
+
429
+ function createFilesystemContentStore(stateRoot: string): ArtifactContentStore {
430
+ const dir = join(stateRoot, "artifacts", "content");
431
+ const filePath = (id: string): string => join(dir, encodeURIComponent(id));
432
+ return {
433
+ async get(id): Promise<Uint8Array | undefined> {
434
+ try {
435
+ return new Uint8Array(await readFile(filePath(id)));
436
+ } catch (error) {
437
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
438
+ return undefined;
439
+ }
440
+ throw error;
441
+ }
442
+ },
443
+ async put(id, body): Promise<void> {
444
+ await mkdir(dir, { recursive: true });
445
+ const tmp = join(dir, `.tmp-${randomUUID()}`);
446
+ await writeFile(tmp, body);
447
+ await rename(tmp, filePath(id));
448
+ },
449
+ async delete(id): Promise<void> {
450
+ await rm(filePath(id), { force: true });
451
+ },
289
452
  };
290
453
  }