@cursor/july 0.1.7 → 0.1.9

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 (210) hide show
  1. package/dist/bin/agent-serve.js +31 -2
  2. package/dist/channels/slack/channel-watch.d.ts +6 -1
  3. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  4. package/dist/channels/slack/channel-watch.js +22 -1
  5. package/dist/channels/slack/cursor-account.d.ts +4 -0
  6. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.js +12 -2
  8. package/dist/channels/slack/defaults.d.ts +10 -5
  9. package/dist/channels/slack/defaults.d.ts.map +1 -1
  10. package/dist/channels/slack/defaults.js +39 -17
  11. package/dist/channels/slack/dispatch.d.ts +18 -0
  12. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  13. package/dist/channels/slack/dispatch.js +141 -38
  14. package/dist/channels/slack/inbound.d.ts +13 -3
  15. package/dist/channels/slack/inbound.d.ts.map +1 -1
  16. package/dist/channels/slack/inbound.js +17 -9
  17. package/dist/channels/slack/index.d.ts +4 -0
  18. package/dist/channels/slack/index.d.ts.map +1 -1
  19. package/dist/channels/slack/index.js +4 -0
  20. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  21. package/dist/channels/slack/live-delivery.js +1 -0
  22. package/dist/channels/slack/manifest.d.ts.map +1 -1
  23. package/dist/channels/slack/manifest.js +3 -0
  24. package/dist/channels/slack/nudge-carrier.d.ts +19 -0
  25. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
  26. package/dist/channels/slack/nudge-carrier.js +166 -0
  27. package/dist/channels/slack/nudge-store.d.ts +61 -0
  28. package/dist/channels/slack/nudge-store.d.ts.map +1 -0
  29. package/dist/channels/slack/nudge-store.js +266 -0
  30. package/dist/channels/slack/post-update-delivery.js +318 -0
  31. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  32. package/dist/channels/slack/slack-channel.js +74 -25
  33. package/dist/channels/slack/stop.d.ts +14 -0
  34. package/dist/channels/slack/stop.d.ts.map +1 -0
  35. package/dist/channels/slack/stop.js +22 -0
  36. package/dist/channels/slack/thread-context.d.ts +81 -0
  37. package/dist/channels/slack/thread-context.d.ts.map +1 -0
  38. package/dist/channels/slack/thread-context.js +470 -0
  39. package/dist/channels/slack/types.d.ts +27 -0
  40. package/dist/channels/slack/types.d.ts.map +1 -1
  41. package/dist/docs/404.html +2 -2
  42. package/dist/docs/ab.html +4 -4
  43. package/dist/docs/assets/{app.COTN7wgo.js → app.Ciosf24M.js} +1 -1
  44. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
  45. package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
  46. package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.CMpbjdfx.js} +2 -2
  47. package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
  48. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
  49. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
  50. package/dist/docs/assets/example-agents_index.md.BPXjUtRB.js +2 -0
  51. package/dist/docs/assets/example-agents_index.md.BPXjUtRB.lean.js +1 -0
  52. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
  53. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
  54. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
  55. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
  56. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
  57. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
  58. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
  59. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
  60. package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.DRTdnFvd.js} +9 -2
  61. package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
  62. package/dist/docs/building-with-agents.html +4 -4
  63. package/dist/docs/concepts.html +4 -4
  64. package/dist/docs/deployment.html +6 -6
  65. package/dist/docs/evals.html +4 -4
  66. package/dist/docs/example-agents/approval-buddy.html +5 -5
  67. package/dist/docs/example-agents/benny.html +5 -5
  68. package/dist/docs/example-agents/bugbot.html +5 -5
  69. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  70. package/dist/docs/example-agents/codeowners-review.html +4 -4
  71. package/dist/docs/example-agents/concierge.html +4 -4
  72. package/dist/docs/example-agents/fsd.html +4 -4
  73. package/dist/docs/example-agents/index.html +6 -6
  74. package/dist/docs/example-agents/knowledge-base.html +4 -4
  75. package/dist/docs/example-agents/oncall.html +34 -0
  76. package/dist/docs/example-agents/security-reviewer.html +6 -6
  77. package/dist/docs/example-agents/slack-agent.html +4 -4
  78. package/dist/docs/example-agents/weather-agent.html +4 -4
  79. package/dist/docs/guides/agent-to-agent.html +4 -4
  80. package/dist/docs/guides/cloud-runtime.html +4 -4
  81. package/dist/docs/guides/github.html +4 -4
  82. package/dist/docs/guides/human-in-the-loop.html +4 -4
  83. package/dist/docs/guides/slack.html +7 -6
  84. package/dist/docs/guides/webhooks.html +4 -4
  85. package/dist/docs/hashmap.json +1 -1
  86. package/dist/docs/hillclimbing.html +4 -4
  87. package/dist/docs/index.html +4 -4
  88. package/dist/docs/quickstart.html +4 -4
  89. package/dist/docs/reference/agent-config.html +4 -4
  90. package/dist/docs/reference/channels.html +4 -4
  91. package/dist/docs/reference/cli.html +4 -4
  92. package/dist/docs/reference/connections.html +4 -4
  93. package/dist/docs/reference/hooks.html +4 -4
  94. package/dist/docs/reference/http-api.html +4 -4
  95. package/dist/docs/reference/instructions.html +4 -4
  96. package/dist/docs/reference/playground.html +4 -4
  97. package/dist/docs/reference/project-layout.html +4 -4
  98. package/dist/docs/reference/schedules.html +4 -4
  99. package/dist/docs/reference/sessions.html +5 -5
  100. package/dist/docs/reference/skills.html +4 -4
  101. package/dist/docs/reference/subagents.html +4 -4
  102. package/dist/docs/reference/tools.html +4 -4
  103. package/dist/docs/scaffolding-agents.html +4 -4
  104. package/dist/docs/storage.html +13 -6
  105. package/dist/docs/troubleshooting.html +4 -4
  106. package/dist/internal/cli-deploy.d.ts.map +1 -1
  107. package/dist/internal/cli-deploy.js +45 -18
  108. package/dist/internal/cli-mcp.d.ts +64 -0
  109. package/dist/internal/cli-mcp.d.ts.map +1 -0
  110. package/dist/internal/cli-mcp.js +570 -0
  111. package/dist/internal/deploy-client.d.ts +7 -1
  112. package/dist/internal/deploy-client.d.ts.map +1 -1
  113. package/dist/internal/deploy-client.js +3 -0
  114. package/dist/internal/deploy-source.d.ts +3 -0
  115. package/dist/internal/deploy-source.d.ts.map +1 -1
  116. package/dist/internal/deploy-source.js +25 -7
  117. package/dist/internal/discovery.js +1 -1
  118. package/dist/internal/distribution.d.ts.map +1 -1
  119. package/dist/internal/distribution.js +3 -0
  120. package/dist/internal/host-kv.d.ts +17 -0
  121. package/dist/internal/host-kv.d.ts.map +1 -0
  122. package/dist/internal/host-kv.js +77 -0
  123. package/dist/internal/hosting.d.ts +12 -2
  124. package/dist/internal/hosting.d.ts.map +1 -1
  125. package/dist/internal/hosting.js +21 -4
  126. package/dist/internal/json-dir-store.js +100 -0
  127. package/dist/internal/logs-client.d.ts +16 -2
  128. package/dist/internal/logs-client.d.ts.map +1 -1
  129. package/dist/internal/logs-client.js +51 -16
  130. package/dist/internal/server.d.ts.map +1 -1
  131. package/dist/internal/server.js +8 -0
  132. package/dist/internal/session-engine.d.ts +42 -7
  133. package/dist/internal/session-engine.d.ts.map +1 -1
  134. package/dist/internal/session-engine.js +276 -66
  135. package/dist/internal/storage-coordinator.d.ts +18 -1
  136. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  137. package/dist/internal/storage-coordinator.js +43 -0
  138. package/dist/internal/workspace.d.ts +19 -0
  139. package/dist/internal/workspace.d.ts.map +1 -1
  140. package/dist/internal/workspace.js +47 -13
  141. package/dist/playground/assets/{index-72vCOBWO.js → index-C0-QEG4O.js} +42 -42
  142. package/dist/playground/assets/index-z3JY9O2x.css +1 -0
  143. package/dist/playground/index.html +2 -2
  144. package/dist/storage-backends/cursor-hosted.d.ts +65 -0
  145. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
  146. package/dist/storage-backends/cursor-hosted.js +203 -0
  147. package/dist/storage-backends/file-kv.d.ts +42 -0
  148. package/dist/storage-backends/file-kv.d.ts.map +1 -0
  149. package/dist/storage-backends/file-kv.js +190 -0
  150. package/dist/storage-backends/postgres-kv.d.ts +82 -0
  151. package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
  152. package/dist/storage-backends/postgres-kv.js +185 -0
  153. package/dist/storage.d.ts +10 -1
  154. package/dist/storage.d.ts.map +1 -1
  155. package/dist/storage.js +10 -1
  156. package/dist/types.d.ts +79 -0
  157. package/dist/types.d.ts.map +1 -1
  158. package/docs/.vitepress/cache/deps/package.json +3 -0
  159. package/docs/deployment.md +7 -3
  160. package/docs/example-agents/approval-buddy.md +1 -1
  161. package/docs/example-agents/index.md +15 -10
  162. package/docs/example-agents/oncall.md +216 -0
  163. package/docs/example-agents/security-reviewer.md +5 -5
  164. package/docs/guides/slack.md +11 -2
  165. package/docs/reference/sessions.md +13 -4
  166. package/docs/storage.md +41 -0
  167. package/package.json +22 -1
  168. package/skills/framework-map/SKILL.md +6 -4
  169. package/src/bin/agent-serve.ts +32 -2
  170. package/src/channels/slack/channel-watch.ts +28 -1
  171. package/src/channels/slack/cursor-account.ts +20 -0
  172. package/src/channels/slack/defaults.ts +51 -20
  173. package/src/channels/slack/dispatch.ts +215 -40
  174. package/src/channels/slack/inbound.ts +30 -10
  175. package/src/channels/slack/index.ts +4 -0
  176. package/src/channels/slack/live-delivery.ts +1 -0
  177. package/src/channels/slack/manifest.ts +3 -0
  178. package/src/channels/slack/nudge-carrier.ts +182 -0
  179. package/src/channels/slack/nudge-store.ts +308 -0
  180. package/src/channels/slack/slack-channel.ts +106 -31
  181. package/src/channels/slack/stop.ts +25 -0
  182. package/src/channels/slack/thread-context.ts +572 -0
  183. package/src/channels/slack/types.ts +27 -0
  184. package/src/internal/cli-deploy.ts +58 -20
  185. package/src/internal/cli-mcp.ts +708 -0
  186. package/src/internal/deploy-client.ts +11 -0
  187. package/src/internal/deploy-source.ts +24 -6
  188. package/src/internal/discovery.ts +1 -1
  189. package/src/internal/distribution.ts +3 -0
  190. package/src/internal/host-kv.ts +85 -0
  191. package/src/internal/hosting.ts +24 -4
  192. package/src/internal/logs-client.ts +67 -21
  193. package/src/internal/server.ts +31 -0
  194. package/src/internal/session-engine.ts +379 -61
  195. package/src/internal/storage-coordinator.ts +41 -0
  196. package/src/internal/workspace.ts +48 -11
  197. package/src/storage-backends/cursor-hosted.ts +246 -0
  198. package/src/storage-backends/file-kv.ts +221 -0
  199. package/src/storage-backends/postgres-kv.ts +253 -0
  200. package/src/storage.ts +11 -1
  201. package/src/types.ts +83 -0
  202. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
  203. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
  204. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +0 -1
  205. package/dist/internal/persistence-coordinator.d.ts +0 -127
  206. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  207. package/dist/persistence.d.ts +0 -184
  208. package/dist/persistence.d.ts.map +0 -1
  209. package/dist/playground/assets/index-BjnMwYoR.css +0 -1
  210. /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { chmod, copyFile, mkdir, stat, writeFile } from "node:fs/promises";
11
- import { dirname, join, relative, resolve } from "node:path";
11
+ import { dirname, join, parse, relative, resolve } from "node:path";
12
12
  import type {
13
13
  DiscoveredSkill,
14
14
  DiscoveredTool,
@@ -123,22 +123,59 @@ export function buildAgentToolsCatalog(
123
123
  return `${parts.join("\n").trimEnd()}\n`;
124
124
  }
125
125
 
126
+ /**
127
+ * The full AGENTS.md-equivalent context for an agent: instructions, identity
128
+ * section, and the agent-side tool catalog. Written to the session workspace
129
+ * as `AGENTS.md`, and inlined into the first turn's prompt when the harness
130
+ * cannot discover that file (cloud runtime, or a local workspace the harness
131
+ * will not treat as the project root — see {@link isNestedInGitRepo}).
132
+ */
133
+ export function buildAgentsMdContent(
134
+ agent: ResolvedAgent,
135
+ options: { includeScripts: boolean }
136
+ ): string {
137
+ return [
138
+ agent.instructions?.trim() ?? "",
139
+ buildIdentitySection(agent),
140
+ buildAgentToolsCatalog(agent.tools, {
141
+ includeScripts: options.includeScripts,
142
+ }),
143
+ ]
144
+ .filter((part) => part !== "")
145
+ .join("\n\n");
146
+ }
147
+
148
+ /**
149
+ * True when `dir` sits strictly inside a git repository (a `.git` exists in
150
+ * an ancestor directory but not in `dir` itself). The Cursor harness resolves
151
+ * its project root by walking up to the nearest git root, so a nested session
152
+ * workspace loses its own `AGENTS.md` / `.cursor` scaffold: the enclosing
153
+ * repo's rules load instead, and the (necessarily gitignored) workspace files
154
+ * are skipped by nested rule discovery.
155
+ */
156
+ export async function isNestedInGitRepo(dir: string): Promise<boolean> {
157
+ let current = resolve(dir);
158
+ if (await exists(join(current, ".git"))) {
159
+ // The workspace is itself a repo root; the harness roots rules here.
160
+ return false;
161
+ }
162
+ const { root } = parse(current);
163
+ while (current !== root) {
164
+ current = dirname(current);
165
+ if (await exists(join(current, ".git"))) {
166
+ return true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+
126
172
  export async function materializeWorkspace(
127
173
  options: MaterializeWorkspaceOptions
128
174
  ): Promise<void> {
129
175
  const { agent, workspaceDir, seed } = options;
130
176
  await mkdir(workspaceDir, { recursive: true });
131
177
 
132
- const agentToolsSection = buildAgentToolsCatalog(agent.tools, {
133
- includeScripts: false,
134
- });
135
- const agentsMd = [
136
- agent.instructions?.trim() ?? "",
137
- buildIdentitySection(agent),
138
- agentToolsSection,
139
- ]
140
- .filter((part) => part !== "")
141
- .join("\n\n");
178
+ const agentsMd = buildAgentsMdContent(agent, { includeScripts: false });
142
179
  await writeFile(join(workspaceDir, "AGENTS.md"), agentsMd, "utf8");
143
180
 
144
181
  for (const tool of agent.tools) {
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Cursor-managed durable storage for hosted agents.
3
+ *
4
+ * Talks to the control-plane storage proxy
5
+ * (`POST /internal/agent-serve/storage/*`) using the engine's pod
6
+ * `CURSOR_API_KEY`. The server resolves `deployment_id` from that service
7
+ * account — engines never receive a Bugbot DSN and cannot address another
8
+ * deployment's rows.
9
+ *
10
+ * On Cursor-managed hosting the start script pins `CURSOR_API_BASE_URL` to
11
+ * `https://api.cursor.com` (where the proxy is mounted). Locally, point
12
+ * `CURSOR_API_BASE_URL` at a backend that mounts Agent Serve routes and use
13
+ * a deployment SA key, or use `@cursor/july/storage/postgres-kv` /
14
+ * `file-kv` instead.
15
+ *
16
+ * ```ts
17
+ * import { defineStorage } from "@cursor/july/storage";
18
+ * import { cursorHostedStorage } from "@cursor/july/storage/cursor-hosted";
19
+ *
20
+ * export default defineStorage({
21
+ * ...cursorHostedStorage(),
22
+ * });
23
+ * ```
24
+ */
25
+
26
+ import type { ABSnapshot } from "../ab.js";
27
+ import type { EvalRunSnapshot } from "../evals.js";
28
+ import type {
29
+ StorageABTable,
30
+ StorageConfig,
31
+ StorageEntry,
32
+ StorageEvalsTable,
33
+ } from "../storage.js";
34
+ import type { JsonValue } from "../types.js";
35
+
36
+ /**
37
+ * Same default as {@link cursorExternalApiUrl} in the CLI credentials module,
38
+ * inlined here so this storage entrypoint does not import CLI/zod code.
39
+ * Hosted monorepo examples load this file from `packages/agent-serve/src`
40
+ * while `npm install` runs in the example package — a credentials import
41
+ * would resolve `zod` from the wrong `node_modules` tree and crash serve.
42
+ */
43
+ const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
44
+
45
+ /**
46
+ * Reserved so user `secrets set` cannot smuggle a Bugbot DSN into the pod.
47
+ * Not injected by Cursor hosting (storage is HTTP-proxied).
48
+ */
49
+ export const AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR =
50
+ "BUGBOTDB_URL" as const;
51
+
52
+ /**
53
+ * Diagnostic / self-host deployment id. Not the hosted-storage authz
54
+ * boundary — the proxy scopes rows from the authenticated service account.
55
+ */
56
+ export const AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR =
57
+ "AGENT_SERVE_DEPLOYMENT_ID" as const;
58
+
59
+ /**
60
+ * Platform-managed secret / env names. User `secrets set` / DELETE cannot
61
+ * claim these.
62
+ */
63
+ export const CURSOR_HOSTED_STORAGE_ENV_VARS: readonly [
64
+ typeof AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
65
+ typeof AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
66
+ ] = [
67
+ AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
68
+ AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
69
+ ];
70
+
71
+ export interface CursorHostedStorageOptions {
72
+ /**
73
+ * Control-plane base URL (default: `CURSOR_API_BASE_URL`, else
74
+ * `https://api.cursor.com`).
75
+ */
76
+ baseUrl?: string;
77
+ /** Bearer credential (default: `CURSOR_API_KEY`). */
78
+ apiKey?: string;
79
+ /** Injected for tests. */
80
+ fetch?: typeof fetch;
81
+ }
82
+
83
+ type CursorHostedStorageMethods = Required<
84
+ Pick<StorageConfig, "put" | "get" | "delete" | "list">
85
+ > & {
86
+ evals: StorageEvalsTable;
87
+ abs: StorageABTable;
88
+ };
89
+
90
+ function resolveApiKey(options: CursorHostedStorageOptions): string {
91
+ const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
92
+ if (key === undefined || key === "") {
93
+ throw new Error(
94
+ "cursorHostedStorage requires CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
95
+ );
96
+ }
97
+ return key;
98
+ }
99
+
100
+ function resolveBaseUrl(options: CursorHostedStorageOptions): string {
101
+ const raw = (
102
+ options.baseUrl ??
103
+ process.env.CURSOR_API_BASE_URL ??
104
+ DEFAULT_CURSOR_EXTERNAL_API_URL
105
+ ).trim();
106
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
107
+ }
108
+
109
+ async function storageRpc<T>(
110
+ options: CursorHostedStorageOptions,
111
+ path: string,
112
+ body: Record<string, unknown>
113
+ ): Promise<T> {
114
+ const baseUrl = resolveBaseUrl(options);
115
+ const apiKey = resolveApiKey(options);
116
+ const fetchImpl = options.fetch ?? globalThis.fetch;
117
+ if (typeof fetchImpl !== "function") {
118
+ throw new Error("cursorHostedStorage requires fetch");
119
+ }
120
+ const response = await fetchImpl(`${baseUrl}${path}`, {
121
+ method: "POST",
122
+ headers: {
123
+ Authorization: `Bearer ${apiKey}`,
124
+ "Content-Type": "application/json",
125
+ },
126
+ body: JSON.stringify(body),
127
+ });
128
+ const text = await response.text();
129
+ let parsed: unknown;
130
+ try {
131
+ parsed = text.length === 0 ? {} : JSON.parse(text);
132
+ } catch {
133
+ throw new Error(
134
+ `cursorHostedStorage ${path} returned non-JSON (${response.status})`
135
+ );
136
+ }
137
+ if (!response.ok) {
138
+ const error =
139
+ parsed !== null &&
140
+ typeof parsed === "object" &&
141
+ "error" in parsed &&
142
+ typeof (parsed as { error: unknown }).error === "string"
143
+ ? (parsed as { error: string }).error
144
+ : `HTTP ${response.status}`;
145
+ throw new Error(`cursorHostedStorage ${path}: ${error}`);
146
+ }
147
+ return parsed as T;
148
+ }
149
+
150
+ /**
151
+ * Cursor-hosted `defineStorage` methods (control-plane storage proxy →
152
+ * Bugbot `agent_serve_*` tables). Spread into `defineStorage({...})`.
153
+ */
154
+ export function cursorHostedStorage(
155
+ options: CursorHostedStorageOptions = {}
156
+ ): CursorHostedStorageMethods {
157
+ return {
158
+ async put(key, value): Promise<void> {
159
+ await storageRpc(options, "/internal/agent-serve/storage/kv/put", {
160
+ key,
161
+ value,
162
+ });
163
+ },
164
+ async get(key): Promise<JsonValue | undefined> {
165
+ const result = await storageRpc<{
166
+ found: boolean;
167
+ value: JsonValue | null;
168
+ }>(options, "/internal/agent-serve/storage/kv/get", { key });
169
+ if (!result.found) {
170
+ return undefined;
171
+ }
172
+ return result.value ?? undefined;
173
+ },
174
+ async delete(key): Promise<void> {
175
+ await storageRpc(options, "/internal/agent-serve/storage/kv/delete", {
176
+ key,
177
+ });
178
+ },
179
+ async list(prefix): Promise<StorageEntry[]> {
180
+ const result = await storageRpc<{
181
+ entries: Array<{ key: string; value: JsonValue }>;
182
+ }>(options, "/internal/agent-serve/storage/kv/list", { prefix });
183
+ return result.entries;
184
+ },
185
+ evals: {
186
+ async put(run, ctx): Promise<void> {
187
+ await storageRpc(options, "/internal/agent-serve/storage/evals/put", {
188
+ agentName: ctx.agentName,
189
+ run,
190
+ });
191
+ },
192
+ async delete(runId, ctx): Promise<void> {
193
+ await storageRpc(
194
+ options,
195
+ "/internal/agent-serve/storage/evals/delete",
196
+ {
197
+ agentName: ctx.agentName,
198
+ runId,
199
+ }
200
+ );
201
+ },
202
+ async list(ctx): Promise<EvalRunSnapshot[]> {
203
+ const result = await storageRpc<{ runs: EvalRunSnapshot[] }>(
204
+ options,
205
+ "/internal/agent-serve/storage/evals/list",
206
+ { agentName: ctx.agentName }
207
+ );
208
+ return result.runs;
209
+ },
210
+ },
211
+ abs: {
212
+ async putSample(sample, ctx): Promise<void> {
213
+ await storageRpc(
214
+ options,
215
+ "/internal/agent-serve/storage/abs/put-sample",
216
+ {
217
+ agentName: ctx.agentName,
218
+ sample,
219
+ }
220
+ );
221
+ },
222
+ async putSnapshot(snapshot, ctx): Promise<void> {
223
+ await storageRpc(
224
+ options,
225
+ "/internal/agent-serve/storage/abs/put-snapshot",
226
+ {
227
+ agentName: ctx.agentName,
228
+ snapshot,
229
+ }
230
+ );
231
+ },
232
+ async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
233
+ const result = await storageRpc<{
234
+ found: boolean;
235
+ snapshot: ABSnapshot | null;
236
+ }>(options, "/internal/agent-serve/storage/abs/get-snapshot", {
237
+ agentName: ctx.agentName,
238
+ });
239
+ if (!result.found) {
240
+ return undefined;
241
+ }
242
+ return result.snapshot ?? undefined;
243
+ },
244
+ },
245
+ };
246
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * File-backed `defineStorage` helper: a file-per-key KV store plus
3
+ * file-backed `evals` / `abs` tables.
4
+ *
5
+ * Implements the whole `defineStorage` surface with zero external
6
+ * dependencies: the four KV methods against a flat directory of JSON files
7
+ * (one file per key, filename = URI-encoded key), eval runs as
8
+ * `eval-runs/<agent>/<runId>.json`, and A/B metrics as
9
+ * `ab/<agent>/samples.ndjson` (append-only) plus `ab/<agent>/snapshot.json`
10
+ * (latest aggregate). KV keys carry the agent name inside the framework
11
+ * key scheme, but the evals/abs tables have no such scheme — the agent
12
+ * subdirectory keeps agents sharing one storage `dir` from mixing or
13
+ * overwriting each other's history. Swap this for a real backend
14
+ * (Postgres, S3, …) in production; the contracts are identical.
15
+ *
16
+ * Layout notes:
17
+ * - KV filenames encode the full key (`encodeURIComponent(key) + ".json"`),
18
+ * so `list(prefix)` is a readdir + decode + filter, returned in key
19
+ * order as the contract requires.
20
+ * - `put` writes via tmp + rename: the framework serializes writes, but a
21
+ * restore-path `get`/`list` can run concurrently and must never see a
22
+ * torn file.
23
+ * - `get` distinguishes a definitive miss (ENOENT → `undefined`) from a
24
+ * real failure (anything else throws) — on the lazy-restore path a
25
+ * throw correctly fails the follow-up instead of forking the session.
26
+ */
27
+
28
+ import { randomUUID } from "node:crypto";
29
+ import {
30
+ appendFile,
31
+ mkdir,
32
+ readdir,
33
+ readFile,
34
+ rename,
35
+ rm,
36
+ writeFile,
37
+ } from "node:fs/promises";
38
+ import { join } from "node:path";
39
+ import type { ABSnapshot } from "../ab.js";
40
+ import type { EvalRunSnapshot } from "../evals.js";
41
+ import type {
42
+ StorageABTable,
43
+ StorageConfig,
44
+ StorageContext,
45
+ StorageEntry,
46
+ StorageEvalsTable,
47
+ } from "../storage.js";
48
+ import type { JsonValue } from "../types.js";
49
+
50
+ export interface FileKvOptions {
51
+ /**
52
+ * Directory holding the storage files, relative to the agent project root
53
+ * (default `".agent-serve/kv"`). Absolute paths are used as-is.
54
+ */
55
+ dir?: string;
56
+ }
57
+
58
+ type FileKvMethods = Required<
59
+ Pick<StorageConfig, "put" | "get" | "delete" | "list">
60
+ > & {
61
+ evals: StorageEvalsTable;
62
+ abs: StorageABTable;
63
+ };
64
+
65
+ /** File-backed `defineStorage` methods, ready to spread into `defineStorage({...})`. */
66
+ export function fileKv(options?: FileKvOptions): FileKvMethods {
67
+ const resolveDir = (ctx: StorageContext): string =>
68
+ join(ctx.projectRoot, options?.dir ?? ".agent-serve/kv");
69
+ // The evals/abs tables are scoped per agent (KV keys already carry the
70
+ // agent name via the framework key scheme).
71
+ const evalRunsDir = (ctx: StorageContext): string =>
72
+ join(resolveDir(ctx), "eval-runs", encodeURIComponent(ctx.agentName));
73
+ const abDir = (ctx: StorageContext): string =>
74
+ join(resolveDir(ctx), "ab", encodeURIComponent(ctx.agentName));
75
+ const filePath = (ctx: StorageContext, key: string): string =>
76
+ join(resolveDir(ctx), `${encodeURIComponent(key)}.json`);
77
+ const readKey = async (
78
+ ctx: StorageContext,
79
+ key: string
80
+ ): Promise<JsonValue | undefined> => {
81
+ try {
82
+ return JSON.parse(
83
+ await readFile(filePath(ctx, key), "utf8")
84
+ ) as JsonValue;
85
+ } catch (error) {
86
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
87
+ return undefined; // definitive miss — key was never written
88
+ }
89
+ throw error; // real failure: let lazy restore fail the follow-up
90
+ }
91
+ };
92
+
93
+ return {
94
+ async put(key, value, ctx): Promise<void> {
95
+ await writeJsonAtomic({
96
+ dir: resolveDir(ctx),
97
+ fileName: `${encodeURIComponent(key)}.json`,
98
+ value,
99
+ });
100
+ },
101
+
102
+ get(key, ctx): Promise<JsonValue | undefined> {
103
+ return readKey(ctx, key);
104
+ },
105
+
106
+ async delete(key, ctx): Promise<void> {
107
+ await rm(filePath(ctx, key), { force: true });
108
+ },
109
+
110
+ async list(prefix, ctx): Promise<StorageEntry[]> {
111
+ let names: string[];
112
+ try {
113
+ names = await readdir(resolveDir(ctx));
114
+ } catch (error) {
115
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
116
+ return []; // nothing stored yet
117
+ }
118
+ throw error;
119
+ }
120
+ const keys = names
121
+ .filter((name) => name.endsWith(".json"))
122
+ .map((name) => decodeURIComponent(name.slice(0, -".json".length)))
123
+ .filter((key) => key.startsWith(prefix))
124
+ .sort();
125
+ const entries: StorageEntry[] = [];
126
+ for (const key of keys) {
127
+ const value = await readKey(ctx, key);
128
+ if (value !== undefined) {
129
+ entries.push({ key, value });
130
+ }
131
+ }
132
+ return entries;
133
+ },
134
+
135
+ // Playground eval batches: one JSON file per run under eval-runs/<agent>/.
136
+ evals: {
137
+ async put(run, ctx): Promise<void> {
138
+ await writeJsonAtomic({
139
+ dir: evalRunsDir(ctx),
140
+ fileName: `${encodeURIComponent(run.runId)}.json`,
141
+ value: run,
142
+ });
143
+ },
144
+ async delete(runId, ctx): Promise<void> {
145
+ await rm(join(evalRunsDir(ctx), `${encodeURIComponent(runId)}.json`), {
146
+ force: true,
147
+ });
148
+ },
149
+ async list(ctx): Promise<EvalRunSnapshot[]> {
150
+ const dir = evalRunsDir(ctx);
151
+ let names: string[];
152
+ try {
153
+ names = await readdir(dir);
154
+ } catch (error) {
155
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
156
+ return [];
157
+ }
158
+ throw error;
159
+ }
160
+ const runs: EvalRunSnapshot[] = [];
161
+ for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
162
+ // One unreadable/corrupt file must not wipe the whole history:
163
+ // hydrate treats a throwing list() as "nothing saved".
164
+ try {
165
+ runs.push(
166
+ JSON.parse(
167
+ await readFile(join(dir, name), "utf8")
168
+ ) as EvalRunSnapshot
169
+ );
170
+ } catch {}
171
+ }
172
+ return runs;
173
+ },
174
+ },
175
+
176
+ // A/B metrics: append-only sample log + latest aggregate snapshot,
177
+ // both under ab/<agent>/.
178
+ abs: {
179
+ async putSample(sample, ctx): Promise<void> {
180
+ const dir = abDir(ctx);
181
+ await mkdir(dir, { recursive: true });
182
+ await appendFile(
183
+ join(dir, "samples.ndjson"),
184
+ `${JSON.stringify(sample)}\n`,
185
+ "utf8"
186
+ );
187
+ },
188
+ async putSnapshot(snapshot, ctx): Promise<void> {
189
+ await writeJsonAtomic({
190
+ dir: abDir(ctx),
191
+ fileName: "snapshot.json",
192
+ value: snapshot,
193
+ });
194
+ },
195
+ async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
196
+ try {
197
+ return JSON.parse(
198
+ await readFile(join(abDir(ctx), "snapshot.json"), "utf8")
199
+ ) as ABSnapshot;
200
+ } catch (error) {
201
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
202
+ return undefined;
203
+ }
204
+ throw error;
205
+ }
206
+ },
207
+ },
208
+ };
209
+ }
210
+
211
+ /** tmp + rename write so concurrent readers never see a torn file. */
212
+ async function writeJsonAtomic(args: {
213
+ dir: string;
214
+ fileName: string;
215
+ value: unknown;
216
+ }): Promise<void> {
217
+ await mkdir(args.dir, { recursive: true });
218
+ const tmp = join(args.dir, `.tmp-${randomUUID()}`);
219
+ await writeFile(tmp, JSON.stringify(args.value), "utf8");
220
+ await rename(tmp, join(args.dir, args.fileName));
221
+ }