@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.
- package/dist/bin/agent-serve.js +31 -2
- package/dist/channels/slack/channel-watch.d.ts +6 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +22 -1
- package/dist/channels/slack/cursor-account.d.ts +4 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +12 -2
- package/dist/channels/slack/defaults.d.ts +10 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +39 -17
- package/dist/channels/slack/dispatch.d.ts +18 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +141 -38
- package/dist/channels/slack/inbound.d.ts +13 -3
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +17 -9
- package/dist/channels/slack/index.d.ts +4 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +4 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +1 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +3 -0
- package/dist/channels/slack/nudge-carrier.d.ts +19 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
- package/dist/channels/slack/nudge-carrier.js +166 -0
- package/dist/channels/slack/nudge-store.d.ts +61 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -0
- package/dist/channels/slack/nudge-store.js +266 -0
- package/dist/channels/slack/post-update-delivery.js +318 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +74 -25
- package/dist/channels/slack/stop.d.ts +14 -0
- package/dist/channels/slack/stop.d.ts.map +1 -0
- package/dist/channels/slack/stop.js +22 -0
- package/dist/channels/slack/thread-context.d.ts +81 -0
- package/dist/channels/slack/thread-context.d.ts.map +1 -0
- package/dist/channels/slack/thread-context.js +470 -0
- package/dist/channels/slack/types.d.ts +27 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.COTN7wgo.js → app.Ciosf24M.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.CMpbjdfx.js} +2 -2
- package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.js +2 -0
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.lean.js +1 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.DRTdnFvd.js} +9 -2
- package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +5 -5
- package/dist/docs/example-agents/benny.html +5 -5
- package/dist/docs/example-agents/bugbot.html +5 -5
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +6 -6
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +34 -0
- package/dist/docs/example-agents/security-reviewer.html +6 -6
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/slack.html +7 -6
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +4 -4
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +13 -6
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +45 -18
- package/dist/internal/cli-mcp.d.ts +64 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -0
- package/dist/internal/cli-mcp.js +570 -0
- package/dist/internal/deploy-client.d.ts +7 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-source.d.ts +3 -0
- package/dist/internal/deploy-source.d.ts.map +1 -1
- package/dist/internal/deploy-source.js +25 -7
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +3 -0
- package/dist/internal/host-kv.d.ts +17 -0
- package/dist/internal/host-kv.d.ts.map +1 -0
- package/dist/internal/host-kv.js +77 -0
- package/dist/internal/hosting.d.ts +12 -2
- package/dist/internal/hosting.d.ts.map +1 -1
- package/dist/internal/hosting.js +21 -4
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/logs-client.d.ts +16 -2
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +51 -16
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +8 -0
- package/dist/internal/session-engine.d.ts +42 -7
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +276 -66
- package/dist/internal/storage-coordinator.d.ts +18 -1
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +43 -0
- package/dist/internal/workspace.d.ts +19 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +47 -13
- package/dist/playground/assets/{index-72vCOBWO.js → index-C0-QEG4O.js} +42 -42
- package/dist/playground/assets/index-z3JY9O2x.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +65 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.js +203 -0
- package/dist/storage-backends/file-kv.d.ts +42 -0
- package/dist/storage-backends/file-kv.d.ts.map +1 -0
- package/dist/storage-backends/file-kv.js +190 -0
- package/dist/storage-backends/postgres-kv.d.ts +82 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
- package/dist/storage-backends/postgres-kv.js +185 -0
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +10 -1
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/deployment.md +7 -3
- package/docs/example-agents/approval-buddy.md +1 -1
- package/docs/example-agents/index.md +15 -10
- package/docs/example-agents/oncall.md +216 -0
- package/docs/example-agents/security-reviewer.md +5 -5
- package/docs/guides/slack.md +11 -2
- package/docs/reference/sessions.md +13 -4
- package/docs/storage.md +41 -0
- package/package.json +22 -1
- package/skills/framework-map/SKILL.md +6 -4
- package/src/bin/agent-serve.ts +32 -2
- package/src/channels/slack/channel-watch.ts +28 -1
- package/src/channels/slack/cursor-account.ts +20 -0
- package/src/channels/slack/defaults.ts +51 -20
- package/src/channels/slack/dispatch.ts +215 -40
- package/src/channels/slack/inbound.ts +30 -10
- package/src/channels/slack/index.ts +4 -0
- package/src/channels/slack/live-delivery.ts +1 -0
- package/src/channels/slack/manifest.ts +3 -0
- package/src/channels/slack/nudge-carrier.ts +182 -0
- package/src/channels/slack/nudge-store.ts +308 -0
- package/src/channels/slack/slack-channel.ts +106 -31
- package/src/channels/slack/stop.ts +25 -0
- package/src/channels/slack/thread-context.ts +572 -0
- package/src/channels/slack/types.ts +27 -0
- package/src/internal/cli-deploy.ts +58 -20
- package/src/internal/cli-mcp.ts +708 -0
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/deploy-source.ts +24 -6
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +3 -0
- package/src/internal/host-kv.ts +85 -0
- package/src/internal/hosting.ts +24 -4
- package/src/internal/logs-client.ts +67 -21
- package/src/internal/server.ts +31 -0
- package/src/internal/session-engine.ts +379 -61
- package/src/internal/storage-coordinator.ts +41 -0
- package/src/internal/workspace.ts +48 -11
- package/src/storage-backends/cursor-hosted.ts +246 -0
- package/src/storage-backends/file-kv.ts +221 -0
- package/src/storage-backends/postgres-kv.ts +253 -0
- package/src/storage.ts +11 -1
- package/src/types.ts +83 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-BjnMwYoR.css +0 -1
- /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
|
|
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
|
+
}
|