@cursor/july 0.1.8 → 0.1.10

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 (149) hide show
  1. package/dist/bin/agent-serve.js +9 -0
  2. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  3. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  4. package/dist/docs/404.html +2 -2
  5. package/dist/docs/ab.html +3 -3
  6. package/dist/docs/assets/{app.BP8mAVMW.js → app.CY2buaEE.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.1FXIZbz8.js → VPLocalSearchBox.Ix954SFJ.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.Dm1R02v9.js → theme.PDwuJbyi.js} +2 -2
  10. package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
  11. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
  12. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
  13. package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.js → example-agents_index.md.BPXjUtRB.js} +2 -2
  14. package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.lean.js → example-agents_index.md.BPXjUtRB.lean.js} +1 -1
  15. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
  16. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
  17. package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.CjfiV2Q-.js} +2 -1
  18. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.FbWupyzW.js} +1 -1
  19. package/dist/docs/assets/{storage.md.Cu3MwM2a.js → storage.md.DRTdnFvd.js} +7 -1
  20. package/dist/docs/assets/{storage.md.Cu3MwM2a.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
  21. package/dist/docs/building-with-agents.html +3 -3
  22. package/dist/docs/concepts.html +3 -3
  23. package/dist/docs/deployment.html +5 -5
  24. package/dist/docs/evals.html +3 -3
  25. package/dist/docs/example-agents/approval-buddy.html +5 -5
  26. package/dist/docs/example-agents/benny.html +3 -3
  27. package/dist/docs/example-agents/bugbot.html +3 -3
  28. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  29. package/dist/docs/example-agents/codeowners-review.html +3 -3
  30. package/dist/docs/example-agents/concierge.html +3 -3
  31. package/dist/docs/example-agents/fsd.html +3 -3
  32. package/dist/docs/example-agents/index.html +5 -5
  33. package/dist/docs/example-agents/knowledge-base.html +3 -3
  34. package/dist/docs/example-agents/oncall.html +3 -3
  35. package/dist/docs/example-agents/security-reviewer.html +6 -6
  36. package/dist/docs/example-agents/slack-agent.html +3 -3
  37. package/dist/docs/example-agents/weather-agent.html +3 -3
  38. package/dist/docs/guides/agent-to-agent.html +3 -3
  39. package/dist/docs/guides/cloud-runtime.html +3 -3
  40. package/dist/docs/guides/github.html +3 -3
  41. package/dist/docs/guides/human-in-the-loop.html +3 -3
  42. package/dist/docs/guides/slack.html +3 -3
  43. package/dist/docs/guides/webhooks.html +3 -3
  44. package/dist/docs/hashmap.json +1 -1
  45. package/dist/docs/hillclimbing.html +3 -3
  46. package/dist/docs/index.html +3 -3
  47. package/dist/docs/quickstart.html +3 -3
  48. package/dist/docs/reference/agent-config.html +3 -3
  49. package/dist/docs/reference/channels.html +3 -3
  50. package/dist/docs/reference/cli.html +6 -5
  51. package/dist/docs/reference/connections.html +5 -5
  52. package/dist/docs/reference/hooks.html +3 -3
  53. package/dist/docs/reference/http-api.html +3 -3
  54. package/dist/docs/reference/instructions.html +3 -3
  55. package/dist/docs/reference/playground.html +3 -3
  56. package/dist/docs/reference/project-layout.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 +11 -5
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/internal/cli-ax.d.ts.map +1 -1
  66. package/dist/internal/cli-ax.js +7 -4
  67. package/dist/internal/cli-deploy.d.ts.map +1 -1
  68. package/dist/internal/cli-deploy.js +45 -18
  69. package/dist/internal/deploy-client.d.ts +7 -1
  70. package/dist/internal/deploy-client.d.ts.map +1 -1
  71. package/dist/internal/deploy-client.js +3 -0
  72. package/dist/internal/deploy-source.d.ts +3 -0
  73. package/dist/internal/deploy-source.d.ts.map +1 -1
  74. package/dist/internal/deploy-source.js +25 -7
  75. package/dist/internal/discovery.js +1 -1
  76. package/dist/internal/distribution.d.ts.map +1 -1
  77. package/dist/internal/distribution.js +1 -0
  78. package/dist/internal/hosting.d.ts +12 -2
  79. package/dist/internal/hosting.d.ts.map +1 -1
  80. package/dist/internal/hosting.js +21 -4
  81. package/dist/internal/json-dir-store.d.ts +32 -0
  82. package/dist/internal/json-dir-store.d.ts.map +1 -0
  83. package/dist/internal/logs-client.d.ts +16 -2
  84. package/dist/internal/logs-client.d.ts.map +1 -1
  85. package/dist/internal/logs-client.js +51 -16
  86. package/dist/internal/persistence-coordinator.d.ts +127 -0
  87. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  88. package/dist/internal/resolved-connections.d.ts +6 -0
  89. package/dist/internal/resolved-connections.d.ts.map +1 -1
  90. package/dist/internal/resolved-connections.js +6 -2
  91. package/dist/internal/server.d.ts.map +1 -1
  92. package/dist/internal/server.js +11 -1
  93. package/dist/internal/session-engine.d.ts +23 -1
  94. package/dist/internal/session-engine.d.ts.map +1 -1
  95. package/dist/internal/session-engine.js +100 -26
  96. package/dist/internal/sessions-client.d.ts +16 -2
  97. package/dist/internal/sessions-client.d.ts.map +1 -1
  98. package/dist/internal/sessions-client.js +28 -12
  99. package/dist/internal/storage-coordinator.d.ts +11 -0
  100. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  101. package/dist/internal/storage-coordinator.js +30 -0
  102. package/dist/internal/workspace.d.ts +19 -0
  103. package/dist/internal/workspace.d.ts.map +1 -1
  104. package/dist/internal/workspace.js +47 -13
  105. package/dist/persistence.d.ts +184 -0
  106. package/dist/persistence.d.ts.map +1 -0
  107. package/dist/playground/assets/index-C0-QEG4O.js +86 -0
  108. package/dist/playground/assets/index-z3JY9O2x.css +1 -0
  109. package/dist/playground/index.html +2 -2
  110. package/dist/storage-backends/cursor-hosted.d.ts +75 -0
  111. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
  112. package/dist/storage-backends/cursor-hosted.js +217 -0
  113. package/dist/storage-backends/postgres-kv.d.ts +5 -0
  114. package/dist/storage-backends/postgres-kv.d.ts.map +1 -1
  115. package/dist/storage-backends/postgres-kv.js +5 -0
  116. package/dist/types.d.ts +15 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/docs/deployment.md +7 -3
  119. package/docs/example-agents/approval-buddy.md +1 -1
  120. package/docs/example-agents/index.md +1 -1
  121. package/docs/example-agents/security-reviewer.md +5 -5
  122. package/docs/reference/cli.md +2 -0
  123. package/docs/reference/connections.md +4 -2
  124. package/docs/storage.md +18 -4
  125. package/package.json +8 -1
  126. package/src/bin/agent-serve.ts +10 -0
  127. package/src/internal/cli-ax.ts +10 -4
  128. package/src/internal/cli-deploy.ts +58 -20
  129. package/src/internal/deploy-client.ts +11 -0
  130. package/src/internal/deploy-source.ts +24 -6
  131. package/src/internal/discovery.ts +1 -1
  132. package/src/internal/distribution.ts +1 -0
  133. package/src/internal/hosting.ts +24 -4
  134. package/src/internal/logs-client.ts +67 -21
  135. package/src/internal/resolved-connections.ts +14 -4
  136. package/src/internal/server.ts +15 -1
  137. package/src/internal/session-engine.ts +105 -13
  138. package/src/internal/sessions-client.ts +49 -14
  139. package/src/internal/storage-coordinator.ts +36 -0
  140. package/src/internal/workspace.ts +48 -11
  141. package/src/storage-backends/cursor-hosted.ts +267 -0
  142. package/src/storage-backends/postgres-kv.ts +5 -0
  143. package/src/types.ts +16 -0
  144. package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +0 -1
  145. package/dist/playground/assets/index-7GJIUFQj.css +0 -1
  146. package/dist/playground/assets/index-Bpr4Th4M.js +0 -319
  147. /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
  148. /package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.CjfiV2Q-.lean.js} +0 -0
  149. /package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.FbWupyzW.lean.js} +0 -0
@@ -45,6 +45,7 @@ import {
45
45
  type SessionEvent,
46
46
  type SessionEventPayload,
47
47
  type SessionInfo,
48
+ type SessionListSource,
48
49
  type SessionRecord,
49
50
  type SessionSummary,
50
51
  type SlackNudgeHostApi,
@@ -73,7 +74,6 @@ import {
73
74
  import { validateToolInput } from "./json-schema.js";
74
75
  import { McpHost } from "./mcp-host.js";
75
76
  import {
76
- buildIdentitySection,
77
77
  buildSessionContextBlock,
78
78
  SCHEDULE_CHANNEL_PREFIX,
79
79
  } from "./prompt-context.js";
@@ -83,7 +83,8 @@ import { SessionStore } from "./session-store.js";
83
83
  import { StorageCoordinator } from "./storage-coordinator.js";
84
84
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
85
85
  import {
86
- buildAgentToolsCatalog,
86
+ buildAgentsMdContent,
87
+ isNestedInGitRepo,
87
88
  materializeWorkspace,
88
89
  writeWorkspaceFiles,
89
90
  } from "./workspace.js";
@@ -1019,12 +1020,22 @@ export class SessionEngine {
1019
1020
  return rest;
1020
1021
  });
1021
1022
  }
1023
+ // The harness only loads the scaffolded AGENTS.md when the session
1024
+ // workspace is its own project root. With a per-session cwd override
1025
+ // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1026
+ // inside a git repo re-roots harness rules at that repo — in both
1027
+ // cases deliver instructions inline on the first turn instead.
1028
+ const instructionsInvisibleToHarness =
1029
+ isFirstTurn &&
1030
+ (record.workspaceOverride !== undefined ||
1031
+ (await isNestedInGitRepo(harnessCwd)));
1022
1032
  const request = this.buildTurnRequest(
1023
1033
  record,
1024
1034
  agent,
1025
1035
  turnId,
1026
1036
  message,
1027
1037
  isFirstTurn,
1038
+ instructionsInvisibleToHarness,
1028
1039
  emit,
1029
1040
  async (update) => {
1030
1041
  mapper.handleUpdate(update);
@@ -1120,6 +1131,7 @@ export class SessionEngine {
1120
1131
  turnId: string,
1121
1132
  prompt: string,
1122
1133
  isFirstTurn: boolean,
1134
+ instructionsInvisibleToHarness: boolean,
1123
1135
  emit: (payload: SessionEventPayload) => void,
1124
1136
  onUpdate: RunnerTurnRequest["onUpdate"],
1125
1137
  signal: AbortSignal
@@ -1147,15 +1159,21 @@ export class SessionEngine {
1147
1159
  });
1148
1160
  turnPrompt = `${contextBlock}\n\n${prompt}`;
1149
1161
  }
1150
- if (runtime === "cloud" && isFirstTurn) {
1151
- const preamble = [
1152
- agent.instructions?.trim() ?? "",
1153
- buildIdentitySection(agent),
1154
- buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
1155
- ]
1156
- .filter((part) => part !== "")
1157
- .join("\n\n");
1158
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
1162
+ if (isFirstTurn) {
1163
+ if (runtime === "cloud") {
1164
+ // Cloud VMs have no materialized workspace: ship the full context,
1165
+ // including agent tool script bodies.
1166
+ const preamble = buildAgentsMdContent(agent, { includeScripts: true });
1167
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
1168
+ } else if (instructionsInvisibleToHarness) {
1169
+ // Local workspace whose AGENTS.md the harness will not load. With a
1170
+ // per-session harness cwd override, agent tool scripts live only in
1171
+ // the durable scaffold dir — embed bodies like cloud. Otherwise the
1172
+ // catalog can reference on-disk paths under the harness cwd.
1173
+ const includeScripts = record.workspaceOverride !== undefined;
1174
+ const preamble = buildAgentsMdContent(agent, { includeScripts });
1175
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
1176
+ }
1159
1177
  }
1160
1178
  return {
1161
1179
  sessionId: record.sessionId,
@@ -1810,13 +1828,88 @@ export class SessionEngine {
1810
1828
  * observability surface across every principal (channel-driven runs,
1811
1829
  * schedule/task sessions, other callers). The route auth chain still
1812
1830
  * gates who may call this at all.
1831
+ *
1832
+ * When the project uses `cursorHostedStorage`, prefers durable Bugbot rows
1833
+ * (merged with any newer local records not flushed yet). Falls back to the
1834
+ * local `--state-root` store when the sink is unavailable.
1813
1835
  */
1814
1836
  async listSessions(
1815
1837
  caller: AuthContext | null,
1816
1838
  options?: { includeAll?: boolean }
1817
1839
  ): Promise<SessionSummary[]> {
1840
+ return (await this.listSessionsWithSource(caller, options)).sessions;
1841
+ }
1842
+
1843
+ /**
1844
+ * Same as {@link listSessions}, plus which store answered
1845
+ * (`cursor-hosted` = durable DB via the control-plane proxy; `local` =
1846
+ * `--state-root`).
1847
+ */
1848
+ async listSessionsWithSource(
1849
+ caller: AuthContext | null,
1850
+ options?: { includeAll?: boolean }
1851
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
1818
1852
  const includeAll = options?.includeAll ?? false;
1853
+ if (this.storage?.isCursorHosted === true) {
1854
+ try {
1855
+ const records = await this.mergeStoredAndLocalSessions();
1856
+ return {
1857
+ sessions: this.summarizeSessionRecords(records, caller, includeAll),
1858
+ source: "cursor-hosted",
1859
+ };
1860
+ } catch (error) {
1861
+ this.logger(
1862
+ `[agentkit] cursor-hosted session list failed; falling back to local: ${describeError(error)}`
1863
+ );
1864
+ }
1865
+ }
1819
1866
  const records = await this.sessions.list();
1867
+ // Local store is createdAt-asc; reverse so newest created leads.
1868
+ return {
1869
+ sessions: this.summarizeSessionRecords(
1870
+ [...records].reverse(),
1871
+ caller,
1872
+ includeAll
1873
+ ),
1874
+ source: "local",
1875
+ };
1876
+ }
1877
+
1878
+ /**
1879
+ * Durable sink rows plus local records (local wins on the same id when
1880
+ * newer — covers debounce windows that have not flushed yet). Newest
1881
+ * `updatedAt` first.
1882
+ */
1883
+ private async mergeStoredAndLocalSessions(): Promise<SessionRecord[]> {
1884
+ const storage = this.storage;
1885
+ if (storage === undefined) {
1886
+ throw new Error("storage is not configured");
1887
+ }
1888
+ const stored = await storage.listSessionsFromSink();
1889
+ const local = await this.sessions.list();
1890
+ const byId = new Map<string, SessionRecord>();
1891
+ for (const record of stored) {
1892
+ byId.set(record.sessionId, record);
1893
+ }
1894
+ for (const record of local) {
1895
+ const existing = byId.get(record.sessionId);
1896
+ if (
1897
+ existing === undefined ||
1898
+ record.updatedAt.localeCompare(existing.updatedAt) >= 0
1899
+ ) {
1900
+ byId.set(record.sessionId, record);
1901
+ }
1902
+ }
1903
+ return [...byId.values()].sort((a, b) =>
1904
+ b.updatedAt.localeCompare(a.updatedAt)
1905
+ );
1906
+ }
1907
+
1908
+ private summarizeSessionRecords(
1909
+ records: readonly SessionRecord[],
1910
+ caller: AuthContext | null,
1911
+ includeAll: boolean
1912
+ ): SessionSummary[] {
1820
1913
  return records
1821
1914
  .filter((record) => includeAll || samePrincipal(record.auth, caller))
1822
1915
  .map((record) => {
@@ -1841,8 +1934,7 @@ export class SessionEngine {
1841
1934
  ),
1842
1935
  ...(externalUrl === undefined ? {} : { externalUrl }),
1843
1936
  };
1844
- })
1845
- .reverse();
1937
+ });
1846
1938
  }
1847
1939
 
1848
1940
  /**
@@ -4,15 +4,33 @@
4
4
  * Customer-facing errors never include hosting URLs or infra jargon.
5
5
  */
6
6
 
7
- import type { SessionEvent, SessionSummary } from "../types.js";
7
+ import type {
8
+ SessionEvent,
9
+ SessionListSource,
10
+ SessionSummary,
11
+ } from "../types.js";
8
12
  import { sanitizeCustomerError } from "./logs-client.js";
9
13
  import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
10
14
 
15
+ export interface ListedSessions {
16
+ sessions: SessionSummary[];
17
+ /** Present when the server reports which store answered. */
18
+ source?: SessionListSource;
19
+ }
20
+
11
21
  export async function listSessions(args: {
12
22
  baseUrl: string;
13
23
  auth?: RequestAuth;
14
24
  fetchImpl?: typeof fetch;
15
25
  }): Promise<SessionSummary[]> {
26
+ return (await listSessionsWithSource(args)).sessions;
27
+ }
28
+
29
+ export async function listSessionsWithSource(args: {
30
+ baseUrl: string;
31
+ auth?: RequestAuth;
32
+ fetchImpl?: typeof fetch;
33
+ }): Promise<ListedSessions> {
16
34
  const baseUrl = args.baseUrl.replace(/\/$/, "");
17
35
  const fetchImpl = args.fetchImpl ?? fetch;
18
36
  let resp: Response;
@@ -30,18 +48,21 @@ export async function listSessions(args: {
30
48
  if (!resp.ok) {
31
49
  throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
32
50
  }
33
- const body = (await resp.json()) as { sessions?: unknown };
34
- if (!Array.isArray(body.sessions)) {
35
- return [];
36
- }
37
- const out: SessionSummary[] = [];
38
- for (const row of body.sessions) {
39
- const parsed = parseSessionSummary(row);
40
- if (parsed !== undefined) {
41
- out.push(parsed);
51
+ const body = (await resp.json()) as { sessions?: unknown; source?: unknown };
52
+ const sessions: SessionSummary[] = [];
53
+ if (Array.isArray(body.sessions)) {
54
+ for (const row of body.sessions) {
55
+ const parsed = parseSessionSummary(row);
56
+ if (parsed !== undefined) {
57
+ sessions.push(parsed);
58
+ }
42
59
  }
43
60
  }
44
- return out;
61
+ const source =
62
+ body.source === "cursor-hosted" || body.source === "local"
63
+ ? body.source
64
+ : undefined;
65
+ return source === undefined ? { sessions } : { sessions, source };
45
66
  }
46
67
 
47
68
  export async function fetchSessionEvents(args: {
@@ -186,10 +207,24 @@ export async function resolvePreviousSessionId(args: {
186
207
  return pickPreviousSession(sessions)?.sessionId;
187
208
  }
188
209
 
210
+ /** Human label for {@link SessionListSource} in CLI output. */
211
+ export function formatSessionListSource(source: SessionListSource): string {
212
+ return source === "cursor-hosted"
213
+ ? "Listing from cursor-hosted storage (durable DB)."
214
+ : "Listing from local session store.";
215
+ }
216
+
189
217
  /** Format a sessions table (id, channel, mode, turns, running, updated). */
190
- export function formatSessionsTable(sessions: SessionSummary[]): string {
218
+ export function formatSessionsTable(
219
+ sessions: SessionSummary[],
220
+ options?: { source?: SessionListSource }
221
+ ): string {
222
+ const sourceLine =
223
+ options?.source === undefined
224
+ ? ""
225
+ : `${formatSessionListSource(options.source)}\n`;
191
226
  if (sessions.length === 0) {
192
- return "No sessions.\n";
227
+ return `${sourceLine}No sessions.\n`;
193
228
  }
194
229
  const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
195
230
  const rows = sessions.map((s) => [
@@ -200,7 +235,7 @@ export function formatSessionsTable(sessions: SessionSummary[]): string {
200
235
  s.running ? "yes" : "no",
201
236
  s.updatedAt,
202
237
  ]);
203
- return `${renderTable(header, rows)}\n`;
238
+ return `${sourceLine}${renderTable(header, rows)}\n`;
204
239
  }
205
240
 
206
241
  function renderTable(header: string[], rows: string[][]): string {
@@ -24,6 +24,7 @@ import {
24
24
  type StorageDefinition,
25
25
  storageKeys,
26
26
  } from "../storage.js";
27
+ import { isCursorHostedStorage } from "../storage-backends/cursor-hosted.js";
27
28
  import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
28
29
  import type { ABSnapshot } from "./ab-snapshot.js";
29
30
  import { describeError } from "./describe-error.js";
@@ -261,6 +262,14 @@ export class StorageCoordinator {
261
262
  return this.definition.list !== undefined;
262
263
  }
263
264
 
265
+ /**
266
+ * True when the authored sink is `cursorHostedStorage` (Bugbot via the
267
+ * control-plane proxy). `GET /v1/sessions` prefers this durable list.
268
+ */
269
+ get isCursorHosted(): boolean {
270
+ return isCursorHostedStorage(this.definition);
271
+ }
272
+
264
273
  /**
265
274
  * Saved session records for this agent (startup restore). Errors are
266
275
  * logged and read as "nothing saved" — a broken store must never block
@@ -275,6 +284,33 @@ export class StorageCoordinator {
275
284
  .filter((record) => record != null);
276
285
  }
277
286
 
287
+ /**
288
+ * Session records from the sink for the sessions API. Unlike
289
+ * {@link listSessions}, missing `list` or a thrown sink error **propagates**
290
+ * so the caller can fall back to the local session store.
291
+ */
292
+ async listSessionsFromSink(): Promise<SessionRecord[]> {
293
+ const list = this.definition.list;
294
+ if (list === undefined) {
295
+ throw new Error("storage sink has no list");
296
+ }
297
+ const entries = await list(
298
+ storageKeys.sessionPrefix(this.agentName),
299
+ this.context("restore")
300
+ );
301
+ if (!Array.isArray(entries)) {
302
+ throw new Error("storage list returned a non-array");
303
+ }
304
+ return entries
305
+ .map((entry) => entry.value as unknown as SessionRecord)
306
+ .filter(
307
+ (record) =>
308
+ record != null &&
309
+ typeof record.sessionId === "string" &&
310
+ record.sessionId !== ""
311
+ );
312
+ }
313
+
278
314
  /**
279
315
  * Saved event stream for one session: chunk values concatenated in key
280
316
  * order (chunks are keyed by their first event's index, zero-padded, so
@@ -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,267 @@
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
+ /**
84
+ * Marker stamped on {@link cursorHostedStorage} so the engine can prefer
85
+ * durable Bugbot rows for `GET /v1/sessions` (survives `defineStorage` spread).
86
+ * Explicit `unique symbol` annotation required for `--isolatedDeclarations`
87
+ * (TS9010).
88
+ */
89
+ export const CURSOR_HOSTED_STORAGE: unique symbol = Symbol.for(
90
+ "@cursor/july.storage.cursor-hosted"
91
+ );
92
+
93
+ /** True when `value` is (or was built from) {@link cursorHostedStorage}. */
94
+ export function isCursorHostedStorage(value: unknown): boolean {
95
+ return (
96
+ typeof value === "object" &&
97
+ value !== null &&
98
+ (value as Record<symbol, unknown>)[CURSOR_HOSTED_STORAGE] === true
99
+ );
100
+ }
101
+
102
+ type CursorHostedStorageMethods = Required<
103
+ Pick<StorageConfig, "put" | "get" | "delete" | "list">
104
+ > & {
105
+ readonly [CURSOR_HOSTED_STORAGE]: true;
106
+ evals: StorageEvalsTable;
107
+ abs: StorageABTable;
108
+ };
109
+
110
+ function resolveApiKey(options: CursorHostedStorageOptions): string {
111
+ const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
112
+ if (key === undefined || key === "") {
113
+ throw new Error(
114
+ "cursorHostedStorage requires CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
115
+ );
116
+ }
117
+ return key;
118
+ }
119
+
120
+ function resolveBaseUrl(options: CursorHostedStorageOptions): string {
121
+ const raw = (
122
+ options.baseUrl ??
123
+ process.env.CURSOR_API_BASE_URL ??
124
+ DEFAULT_CURSOR_EXTERNAL_API_URL
125
+ ).trim();
126
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
127
+ }
128
+
129
+ async function storageRpc<T>(
130
+ options: CursorHostedStorageOptions,
131
+ path: string,
132
+ body: Record<string, unknown>
133
+ ): Promise<T> {
134
+ const baseUrl = resolveBaseUrl(options);
135
+ const apiKey = resolveApiKey(options);
136
+ const fetchImpl = options.fetch ?? globalThis.fetch;
137
+ if (typeof fetchImpl !== "function") {
138
+ throw new Error("cursorHostedStorage requires fetch");
139
+ }
140
+ const response = await fetchImpl(`${baseUrl}${path}`, {
141
+ method: "POST",
142
+ headers: {
143
+ Authorization: `Bearer ${apiKey}`,
144
+ "Content-Type": "application/json",
145
+ },
146
+ body: JSON.stringify(body),
147
+ });
148
+ const text = await response.text();
149
+ let parsed: unknown;
150
+ try {
151
+ parsed = text.length === 0 ? {} : JSON.parse(text);
152
+ } catch {
153
+ throw new Error(
154
+ `cursorHostedStorage ${path} returned non-JSON (${response.status})`
155
+ );
156
+ }
157
+ if (!response.ok) {
158
+ const error =
159
+ parsed !== null &&
160
+ typeof parsed === "object" &&
161
+ "error" in parsed &&
162
+ typeof (parsed as { error: unknown }).error === "string"
163
+ ? (parsed as { error: string }).error
164
+ : `HTTP ${response.status}`;
165
+ throw new Error(`cursorHostedStorage ${path}: ${error}`);
166
+ }
167
+ return parsed as T;
168
+ }
169
+
170
+ /**
171
+ * Cursor-hosted `defineStorage` methods (control-plane storage proxy →
172
+ * Bugbot `agent_serve_*` tables). Spread into `defineStorage({...})`.
173
+ */
174
+ export function cursorHostedStorage(
175
+ options: CursorHostedStorageOptions = {}
176
+ ): CursorHostedStorageMethods {
177
+ return {
178
+ [CURSOR_HOSTED_STORAGE]: true,
179
+ async put(key, value): Promise<void> {
180
+ await storageRpc(options, "/internal/agent-serve/storage/kv/put", {
181
+ key,
182
+ value,
183
+ });
184
+ },
185
+ async get(key): Promise<JsonValue | undefined> {
186
+ const result = await storageRpc<{
187
+ found: boolean;
188
+ value: JsonValue | null;
189
+ }>(options, "/internal/agent-serve/storage/kv/get", { key });
190
+ if (!result.found) {
191
+ return undefined;
192
+ }
193
+ return result.value ?? undefined;
194
+ },
195
+ async delete(key): Promise<void> {
196
+ await storageRpc(options, "/internal/agent-serve/storage/kv/delete", {
197
+ key,
198
+ });
199
+ },
200
+ async list(prefix): Promise<StorageEntry[]> {
201
+ const result = await storageRpc<{
202
+ entries: Array<{ key: string; value: JsonValue }>;
203
+ }>(options, "/internal/agent-serve/storage/kv/list", { prefix });
204
+ return result.entries;
205
+ },
206
+ evals: {
207
+ async put(run, ctx): Promise<void> {
208
+ await storageRpc(options, "/internal/agent-serve/storage/evals/put", {
209
+ agentName: ctx.agentName,
210
+ run,
211
+ });
212
+ },
213
+ async delete(runId, ctx): Promise<void> {
214
+ await storageRpc(
215
+ options,
216
+ "/internal/agent-serve/storage/evals/delete",
217
+ {
218
+ agentName: ctx.agentName,
219
+ runId,
220
+ }
221
+ );
222
+ },
223
+ async list(ctx): Promise<EvalRunSnapshot[]> {
224
+ const result = await storageRpc<{ runs: EvalRunSnapshot[] }>(
225
+ options,
226
+ "/internal/agent-serve/storage/evals/list",
227
+ { agentName: ctx.agentName }
228
+ );
229
+ return result.runs;
230
+ },
231
+ },
232
+ abs: {
233
+ async putSample(sample, ctx): Promise<void> {
234
+ await storageRpc(
235
+ options,
236
+ "/internal/agent-serve/storage/abs/put-sample",
237
+ {
238
+ agentName: ctx.agentName,
239
+ sample,
240
+ }
241
+ );
242
+ },
243
+ async putSnapshot(snapshot, ctx): Promise<void> {
244
+ await storageRpc(
245
+ options,
246
+ "/internal/agent-serve/storage/abs/put-snapshot",
247
+ {
248
+ agentName: ctx.agentName,
249
+ snapshot,
250
+ }
251
+ );
252
+ },
253
+ async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
254
+ const result = await storageRpc<{
255
+ found: boolean;
256
+ snapshot: ABSnapshot | null;
257
+ }>(options, "/internal/agent-serve/storage/abs/get-snapshot", {
258
+ agentName: ctx.agentName,
259
+ });
260
+ if (!result.found) {
261
+ return undefined;
262
+ }
263
+ return result.snapshot ?? undefined;
264
+ },
265
+ },
266
+ };
267
+ }