@cursor/july 0.1.33 → 0.1.35

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 (171) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts.map +1 -1
  3. package/dist/artifacts.js +0 -5
  4. package/dist/bin/agent-serve.d.ts +1 -0
  5. package/dist/bin/agent-serve.d.ts.map +1 -1
  6. package/dist/bin/agent-serve.js +32 -2
  7. package/dist/channels/github/types.d.ts +10 -7
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.CgZqLyWL.js → app.D5Mv1T0U.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.Cu7b6o1D.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.C4KigoQn.js → VPLocalSearchBox.CMq_BQce.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.C_Ty4yT7.js → theme.C6D9UPLK.js} +2 -2
  15. package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
  16. package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
  17. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
  18. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +3 -3
  20. package/dist/docs/concepts.html +3 -3
  21. package/dist/docs/deployment.html +3 -3
  22. package/dist/docs/evals.html +3 -3
  23. package/dist/docs/example-agents/approval-buddy.html +3 -3
  24. package/dist/docs/example-agents/benny.html +3 -3
  25. package/dist/docs/example-agents/bugbot.html +3 -3
  26. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  27. package/dist/docs/example-agents/codeowners-review.html +3 -3
  28. package/dist/docs/example-agents/concierge.html +3 -3
  29. package/dist/docs/example-agents/fsd.html +3 -3
  30. package/dist/docs/example-agents/index.html +3 -3
  31. package/dist/docs/example-agents/knowledge-base.html +3 -3
  32. package/dist/docs/example-agents/oncall.html +3 -3
  33. package/dist/docs/example-agents/security-reviewer.html +3 -3
  34. package/dist/docs/example-agents/slack-agent.html +3 -3
  35. package/dist/docs/example-agents/weather-agent.html +10 -8
  36. package/dist/docs/guides/agent-to-agent.html +3 -3
  37. package/dist/docs/guides/cloud-runtime.html +5 -5
  38. package/dist/docs/guides/github.html +3 -3
  39. package/dist/docs/guides/human-in-the-loop.html +3 -3
  40. package/dist/docs/guides/mcp-oauth.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/prompt.html +3 -3
  57. package/dist/docs/reference/schedules.html +3 -3
  58. package/dist/docs/reference/sessions.html +3 -3
  59. package/dist/docs/reference/skills.html +3 -3
  60. package/dist/docs/reference/subagents.html +3 -3
  61. package/dist/docs/reference/tools.html +3 -3
  62. package/dist/docs/scaffolding-agents.html +3 -3
  63. package/dist/docs/storage.html +3 -3
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/files-backends/agent-store-presigned-url.d.ts +3 -11
  66. package/dist/files.d.ts +20 -0
  67. package/dist/files.d.ts.map +1 -1
  68. package/dist/files.js +20 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -2
  72. package/dist/internal/artifacts-store.d.ts +14 -6
  73. package/dist/internal/artifacts-store.d.ts.map +1 -1
  74. package/dist/internal/artifacts-store.js +160 -16
  75. package/dist/internal/cli-convert-automation.d.ts +39 -0
  76. package/dist/internal/cli-convert-automation.d.ts.map +1 -0
  77. package/dist/internal/cli-convert-automation.js +280 -0
  78. package/dist/internal/cli-deploy.d.ts +2 -0
  79. package/dist/internal/cli-deploy.d.ts.map +1 -1
  80. package/dist/internal/cli-deploy.js +1 -1
  81. package/dist/internal/cli-mcp-oauth.d.ts +23 -0
  82. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  83. package/dist/internal/cli-mcp-oauth.js +57 -33
  84. package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
  85. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
  86. package/dist/internal/convert-automation/convert-workflow.js +998 -0
  87. package/dist/internal/convert-automation/index.d.ts +3 -0
  88. package/dist/internal/convert-automation/index.d.ts.map +1 -0
  89. package/dist/internal/convert-automation/index.js +1 -0
  90. package/dist/internal/convert-automation/slug.d.ts +11 -0
  91. package/dist/internal/convert-automation/slug.d.ts.map +1 -0
  92. package/dist/internal/convert-automation/slug.js +36 -0
  93. package/dist/internal/convert-automation/types.d.ts +191 -0
  94. package/dist/internal/convert-automation/types.d.ts.map +1 -0
  95. package/dist/internal/convert-automation/types.js +13 -0
  96. package/dist/internal/deploy-client.d.ts +31 -0
  97. package/dist/internal/deploy-client.d.ts.map +1 -1
  98. package/dist/internal/deploy-client.js +48 -0
  99. package/dist/internal/discovery.d.ts.map +1 -1
  100. package/dist/internal/discovery.js +3 -32
  101. package/dist/internal/host-kv.d.ts +6 -2
  102. package/dist/internal/host-kv.d.ts.map +1 -1
  103. package/dist/internal/init-project.d.ts +1 -0
  104. package/dist/internal/init-project.d.ts.map +1 -1
  105. package/dist/internal/init-project.js +1 -1
  106. package/dist/internal/mcp-endpoint.d.ts +6 -0
  107. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  108. package/dist/internal/mcp-endpoint.js +168 -0
  109. package/dist/internal/resolved-connections.d.ts +15 -0
  110. package/dist/internal/resolved-connections.d.ts.map +1 -1
  111. package/dist/internal/resolved-connections.js +12 -5
  112. package/dist/internal/server.d.ts.map +1 -1
  113. package/dist/internal/server.js +75 -7
  114. package/dist/internal/session-engine.d.ts +56 -3
  115. package/dist/internal/session-engine.d.ts.map +1 -1
  116. package/dist/internal/session-engine.js +262 -84
  117. package/dist/internal/storage-coordinator.d.ts +9 -16
  118. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  119. package/dist/internal/storage-coordinator.js +7 -52
  120. package/dist/internal/storage-roles.d.ts +78 -0
  121. package/dist/internal/storage-roles.d.ts.map +1 -0
  122. package/dist/internal/storage-roles.js +24 -0
  123. package/dist/internal/workspace.d.ts +36 -5
  124. package/dist/internal/workspace.d.ts.map +1 -1
  125. package/dist/internal/workspace.js +66 -0
  126. package/dist/memory.d.ts +22 -6
  127. package/dist/memory.d.ts.map +1 -1
  128. package/dist/memory.js +100 -8
  129. package/dist/playground/assets/{index-CVlCAcUx.js → index-D7OV8B_H.js} +50 -50
  130. package/dist/playground/assets/index-DOb96C0M.css +1 -0
  131. package/dist/playground/index.html +2 -2
  132. package/dist/storage.d.ts +0 -4
  133. package/dist/storage.d.ts.map +1 -1
  134. package/dist/storage.js +0 -4
  135. package/dist/types.d.ts +18 -0
  136. package/dist/types.d.ts.map +1 -1
  137. package/docs/example-agents/weather-agent.md +29 -11
  138. package/docs/guides/cloud-runtime.md +6 -4
  139. package/package.json +3 -1
  140. package/src/artifacts.ts +0 -6
  141. package/src/bin/agent-serve.ts +36 -2
  142. package/src/channels/github/types.ts +10 -7
  143. package/src/files.ts +27 -0
  144. package/src/index.ts +6 -1
  145. package/src/internal/artifacts-store.ts +185 -22
  146. package/src/internal/cli-convert-automation.ts +352 -0
  147. package/src/internal/cli-deploy.ts +1 -1
  148. package/src/internal/cli-mcp-oauth.ts +69 -31
  149. package/src/internal/convert-automation/convert-workflow.ts +1422 -0
  150. package/src/internal/convert-automation/index.ts +10 -0
  151. package/src/internal/convert-automation/slug.ts +38 -0
  152. package/src/internal/convert-automation/types.ts +199 -0
  153. package/src/internal/deploy-client.ts +78 -0
  154. package/src/internal/discovery.ts +3 -37
  155. package/src/internal/host-kv.ts +6 -2
  156. package/src/internal/init-project.ts +1 -1
  157. package/src/internal/mcp-endpoint.ts +243 -0
  158. package/src/internal/resolved-connections.ts +32 -6
  159. package/src/internal/server.ts +99 -1
  160. package/src/internal/session-engine.ts +345 -103
  161. package/src/internal/storage-coordinator.ts +15 -63
  162. package/src/internal/storage-roles.ts +86 -0
  163. package/src/internal/workspace.ts +81 -6
  164. package/src/memory.ts +126 -10
  165. package/src/storage.ts +0 -6
  166. package/src/types.ts +15 -0
  167. package/dist/docs/assets/chunks/@localSearchIndexroot.B1uHGxBZ.js +0 -1
  168. package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
  169. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
  170. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
  171. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Durable artifact rows behind `ctx.artifacts` ({@link ArtifactsApi}).
2
+ * Durable artifact rows and payloads behind `ctx.artifacts`
3
+ * ({@link ArtifactsApi}).
3
4
  *
4
- * Mirror of host-kv: when `defineStorage` is authored with `get` + `list`,
5
- * rows live in that sink under `storageKeys.artifact`; otherwise one
6
- * JSON file per row under `--state-root/artifacts`.
5
+ * On Cursor-managed hosting rows and file payloads live on the deployment's
6
+ * Agent Store (the same store behind `ctx.host.files`, so they survive
7
+ * deploys and are visible on cloud VMs under the store mount); otherwise
8
+ * they land under `--state-root/artifacts` on local disk.
7
9
  */
8
10
 
9
11
  import { createHash, randomUUID } from "node:crypto";
@@ -17,6 +19,11 @@ import {
17
19
  } from "node:fs/promises";
18
20
  import { join } from "node:path";
19
21
  import { resolveArtifactsMax } from "../artifacts.js";
22
+ import { agentStoreKeys, type FileSink } from "../files.js";
23
+ import {
24
+ cursorHostedFiles,
25
+ isCursorHostedFilesAvailable,
26
+ } from "../files-backends/cursor-hosted.js";
20
27
  import type {
21
28
  ArtifactListFilter,
22
29
  ArtifactRecord,
@@ -24,23 +31,30 @@ import type {
24
31
  ArtifactTagInput,
25
32
  JsonValue,
26
33
  } from "../types.js";
27
- import type { StorageCoordinator } from "./storage-coordinator.js";
28
34
 
29
- // TODO(agent-stores): the KV backing store is migrating to Agent Stores.
30
- // Swap only this adapter to the Agent Stores API when that lands; the
31
- // engine, tool, HTTP, and playground layers must not change.
32
35
  interface ArtifactRowStore {
33
36
  get(id: string): Promise<ArtifactRecord | undefined>;
34
37
  put(record: ArtifactRecord): Promise<void>;
35
38
  delete(id: string): Promise<void>;
36
39
  list(): Promise<ArtifactRecord[]>;
40
+ /** Row count without hydrating bodies — the per-insert cap check. */
41
+ count(): Promise<number>;
42
+ }
43
+
44
+ /** File/blob payload bytes, keyed by artifact id (metadata lives on the row). */
45
+ interface ArtifactContentStore {
46
+ get(id: string): Promise<Uint8Array | undefined>;
47
+ put(id: string, body: Uint8Array): Promise<void>;
48
+ delete(id: string): Promise<void>;
37
49
  }
38
50
 
39
51
  export interface CreateArtifactsStoreOptions {
40
52
  stateRoot: string;
41
- storage?: StorageCoordinator;
53
+ agentName: string;
42
54
  /** `agent/artifacts.ts` config, when authored. */
43
55
  config?: ArtifactsConfig;
56
+ /** Explicit sink override (tests). Omitted → hosted store or local disk. */
57
+ sink?: FileSink;
44
58
  }
45
59
 
46
60
  /** Kind used by freeform tags when no kinds are declared. */
@@ -54,16 +68,23 @@ export const ARTIFACT_DEFAULT_KIND = "artifact";
54
68
  */
55
69
  export class ArtifactsStore {
56
70
  private readonly rows: ArtifactRowStore;
71
+ private readonly content: ArtifactContentStore;
57
72
  private readonly config: ArtifactsConfig | undefined;
58
73
  private readonly max: number;
59
74
  /** Serializes tags so read-modify-write upserts and eviction don't race. */
60
75
  private tagChain: Promise<unknown> = Promise.resolve();
61
76
 
62
77
  constructor(options: CreateArtifactsStoreOptions) {
63
- this.rows =
64
- options.storage?.supportsArtifacts === true
65
- ? createCoordinatorRowStore(options.storage)
66
- : createFilesystemRowStore(options.stateRoot);
78
+ const sink =
79
+ options.sink ??
80
+ (isCursorHostedFilesAvailable() ? cursorHostedFiles() : undefined);
81
+ if (sink === undefined) {
82
+ this.rows = createFilesystemRowStore(options.stateRoot);
83
+ this.content = createFilesystemContentStore(options.stateRoot);
84
+ } else {
85
+ this.rows = createSinkRowStore(sink, options.agentName);
86
+ this.content = createSinkContentStore(sink, options.agentName);
87
+ }
67
88
  this.config = options.config;
68
89
  this.max = resolveArtifactsMax(options.config);
69
90
  }
@@ -82,7 +103,9 @@ export class ArtifactsStore {
82
103
  input.key === undefined
83
104
  ? `art_${randomUUID()}`
84
105
  : artifactIdForKey(input.key);
85
- const existing = await this.rows.get(id);
106
+ // Fresh random ids can't collide; only keyed tags upsert.
107
+ const existing =
108
+ input.key === undefined ? undefined : await this.rows.get(id);
86
109
  const now = new Date().toISOString();
87
110
  const record: ArtifactRecord = {
88
111
  id,
@@ -96,6 +119,25 @@ export class ArtifactsStore {
96
119
  createdAt: existing?.createdAt ?? now,
97
120
  updatedAt: now,
98
121
  };
122
+ if (input.contents !== undefined) {
123
+ const body =
124
+ typeof input.contents === "string"
125
+ ? Buffer.from(input.contents, "utf8")
126
+ : input.contents;
127
+ await this.content.put(id, body);
128
+ record.content = {
129
+ size: body.byteLength,
130
+ contentType: input.contentType,
131
+ };
132
+ } else if (existing?.content !== undefined) {
133
+ if (existing.sessionId === record.sessionId) {
134
+ record.content = existing.content;
135
+ } else {
136
+ // Keys are deployment-global: rebinding a keyed row to another
137
+ // session must not carry the prior session's payload with it.
138
+ await this.content.delete(id);
139
+ }
140
+ }
99
141
  await this.rows.put(record);
100
142
  if (existing === undefined) {
101
143
  await this.evictPastCap();
@@ -103,6 +145,15 @@ export class ArtifactsStore {
103
145
  return record;
104
146
  }
105
147
 
148
+ get(id: string): Promise<ArtifactRecord | undefined> {
149
+ return this.rows.get(id);
150
+ }
151
+
152
+ /** Payload bytes for one artifact; undefined when it carries none. */
153
+ getContent(id: string): Promise<Uint8Array | undefined> {
154
+ return this.content.get(id);
155
+ }
156
+
106
157
  async list(filter?: ArtifactListFilter): Promise<ArtifactRecord[]> {
107
158
  const records = await this.rows.list();
108
159
  return records
@@ -152,6 +203,9 @@ export class ArtifactsStore {
152
203
  }
153
204
 
154
205
  private async evictPastCap(): Promise<void> {
206
+ if ((await this.rows.count()) <= this.max) {
207
+ return;
208
+ }
155
209
  const records = await this.rows.list();
156
210
  if (records.length <= this.max) {
157
211
  return;
@@ -159,6 +213,9 @@ export class ArtifactsStore {
159
213
  const oldestFirst = records.sort((a, b) => byNewestUpdated(b, a));
160
214
  for (const record of oldestFirst.slice(0, records.length - this.max)) {
161
215
  await this.rows.delete(record.id);
216
+ if (record.content !== undefined) {
217
+ await this.content.delete(record.id);
218
+ }
162
219
  }
163
220
  }
164
221
  }
@@ -206,23 +263,90 @@ function isArtifactRecord(
206
263
  return true;
207
264
  }
208
265
 
209
- function createCoordinatorRowStore(
210
- storage: StorageCoordinator
266
+ function parseRecordBytes(
267
+ bytes: Uint8Array | undefined
268
+ ): ArtifactRecord | undefined {
269
+ if (bytes === undefined) {
270
+ return undefined;
271
+ }
272
+ let value: JsonValue;
273
+ try {
274
+ value = JSON.parse(Buffer.from(bytes).toString("utf8")) as JsonValue;
275
+ } catch {
276
+ // A corrupt row must not poison list().
277
+ return undefined;
278
+ }
279
+ return isArtifactRecord(value) ? value : undefined;
280
+ }
281
+
282
+ function sinkBody(
283
+ found: Awaited<ReturnType<FileSink["get"]>>
284
+ ): Uint8Array | undefined {
285
+ if (found === undefined) {
286
+ return undefined;
287
+ }
288
+ return found instanceof Uint8Array ? found : found.body;
289
+ }
290
+
291
+ /** Rows as one JSON file each on the deployment's Agent Store. */
292
+ function createSinkRowStore(
293
+ sink: FileSink,
294
+ agentName: string
211
295
  ): ArtifactRowStore {
296
+ const key = (id: string): string => agentStoreKeys.artifactRow(agentName, id);
212
297
  return {
213
298
  async get(id): Promise<ArtifactRecord | undefined> {
214
- const value = await storage.artifactGet(id);
215
- return isArtifactRecord(value) ? value : undefined;
299
+ return parseRecordBytes(sinkBody(await sink.get(key(id))));
216
300
  },
217
301
  async put(record): Promise<void> {
218
- await storage.artifactPut(record.id, record as unknown as JsonValue);
302
+ await sink.put(
303
+ key(record.id),
304
+ Buffer.from(JSON.stringify(record), "utf8")
305
+ );
219
306
  },
220
307
  async delete(id): Promise<void> {
221
- await storage.artifactDelete(id);
308
+ await sink.delete(key(id));
222
309
  },
223
310
  async list(): Promise<ArtifactRecord[]> {
224
- const values = await storage.artifactList();
225
- return values.filter(isArtifactRecord);
311
+ const entries = await sink.list(
312
+ agentStoreKeys.artifactRowPrefix(agentName)
313
+ );
314
+ const bodies = await Promise.all(
315
+ entries.map((entry) => sink.get(entry.key))
316
+ );
317
+ const records: ArtifactRecord[] = [];
318
+ for (const body of bodies) {
319
+ const record = parseRecordBytes(sinkBody(body));
320
+ if (record !== undefined) {
321
+ records.push(record);
322
+ }
323
+ }
324
+ return records;
325
+ },
326
+ async count(): Promise<number> {
327
+ const entries = await sink.list(
328
+ agentStoreKeys.artifactRowPrefix(agentName)
329
+ );
330
+ return entries.length;
331
+ },
332
+ };
333
+ }
334
+
335
+ function createSinkContentStore(
336
+ sink: FileSink,
337
+ agentName: string
338
+ ): ArtifactContentStore {
339
+ const key = (id: string): string =>
340
+ agentStoreKeys.artifactContent(agentName, id);
341
+ return {
342
+ async get(id): Promise<Uint8Array | undefined> {
343
+ return sinkBody(await sink.get(key(id)));
344
+ },
345
+ async put(id, body): Promise<void> {
346
+ await sink.put(key(id), body);
347
+ },
348
+ async delete(id): Promise<void> {
349
+ await sink.delete(key(id));
226
350
  },
227
351
  };
228
352
  }
@@ -286,5 +410,44 @@ function createFilesystemRowStore(stateRoot: string): ArtifactRowStore {
286
410
  }
287
411
  return records;
288
412
  },
413
+ async count(): Promise<number> {
414
+ try {
415
+ const names = await readdir(dir);
416
+ return names.filter(
417
+ (name) => !name.startsWith(".") && name.endsWith(".json")
418
+ ).length;
419
+ } catch (error) {
420
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
421
+ return 0;
422
+ }
423
+ throw error;
424
+ }
425
+ },
426
+ };
427
+ }
428
+
429
+ function createFilesystemContentStore(stateRoot: string): ArtifactContentStore {
430
+ const dir = join(stateRoot, "artifacts", "content");
431
+ const filePath = (id: string): string => join(dir, encodeURIComponent(id));
432
+ return {
433
+ async get(id): Promise<Uint8Array | undefined> {
434
+ try {
435
+ return new Uint8Array(await readFile(filePath(id)));
436
+ } catch (error) {
437
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
438
+ return undefined;
439
+ }
440
+ throw error;
441
+ }
442
+ },
443
+ async put(id, body): Promise<void> {
444
+ await mkdir(dir, { recursive: true });
445
+ const tmp = join(dir, `.tmp-${randomUUID()}`);
446
+ await writeFile(tmp, body);
447
+ await rename(tmp, filePath(id));
448
+ },
449
+ async delete(id): Promise<void> {
450
+ await rm(filePath(id), { force: true });
451
+ },
289
452
  };
290
453
  }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * `agent-serve convert-automation <url>` — export a Cursor Automation into
3
+ * an agentkit project.
4
+ *
5
+ * Fetches the automation's workflow from the backend export endpoint
6
+ * (Bearer user API key, same credential chain as deploy), runs the pure
7
+ * converter, writes the converted files (wx — existing files are kept),
8
+ * fills in the init scaffold around them, and installs npm dependencies
9
+ * (like `init`). MCP servers convert to Cursor-account connections resolved
10
+ * at runtime (no URLs or credentials in the project; authorization is
11
+ * managed in Cursor, dashboard → MCP). The generated project is NEVER
12
+ * imported/executed here — it embeds server-derived data, so checking it is
13
+ * left to an explicit `validate` next step the user runs after review.
14
+ * Exit 0 even with warnings; nonzero only for bad args, auth/fetch
15
+ * failures, or filesystem write failures.
16
+ */
17
+
18
+ import { mkdir, rm, writeFile } from "node:fs/promises";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import {
21
+ type ConvertedMcpConnection,
22
+ type ConvertWorkflowResult,
23
+ convertWorkflow,
24
+ type WorkflowJson,
25
+ } from "./convert-automation/index.js";
26
+ import { slugify } from "./convert-automation/slug.js";
27
+ import {
28
+ cursorExternalApiUrl,
29
+ resolveApiKey,
30
+ SIGN_IN_HINT,
31
+ } from "./cursor/credentials.js";
32
+ import {
33
+ AgentServeDeployClient,
34
+ type AutomationExport,
35
+ DeployApiError,
36
+ } from "./deploy-client.js";
37
+ import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
38
+ import {
39
+ formatCdCommand,
40
+ type InitFileEntry,
41
+ initProject,
42
+ installScaffoldDependencies,
43
+ isEexist,
44
+ } from "./init-project.js";
45
+ import { stdoutPalette } from "./terminal-style.js";
46
+
47
+ export interface ConvertAutomationCliOptions {
48
+ /** Positional: automation URL or bare uuid. */
49
+ url?: string;
50
+ /** Target directory; defaults to ./<automation-name-slug>. */
51
+ dir?: string;
52
+ json?: boolean;
53
+ apiKey?: string;
54
+ /** Test seams. */
55
+ fetchImpl?: typeof fetch;
56
+ out?: (text: string) => void;
57
+ err?: (text: string) => void;
58
+ installImpl?: typeof installScaffoldDependencies;
59
+ cwd?: string;
60
+ }
61
+
62
+ const UUID_PATTERN =
63
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
64
+
65
+ /**
66
+ * Extract the automation uuid from a dashboard URL
67
+ * (`/automations/<uuid>` or `/custom-agents/<uuid>`, any origin — dev
68
+ * hosts included) or a bare uuid. Undefined when nothing matches.
69
+ */
70
+ export function parseAutomationRef(input: string): string | undefined {
71
+ const trimmed = input.trim();
72
+ if (UUID_PATTERN.test(trimmed)) {
73
+ return trimmed.toLowerCase();
74
+ }
75
+ let parsed: URL;
76
+ try {
77
+ parsed = new URL(trimmed);
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
82
+ return undefined;
83
+ }
84
+ const segments = parsed.pathname.split("/").filter((s) => s !== "");
85
+ if (segments.length !== 2) {
86
+ return undefined;
87
+ }
88
+ const [kind, uuid] = segments;
89
+ if (kind !== "automations" && kind !== "custom-agents") {
90
+ return undefined;
91
+ }
92
+ if (uuid === undefined || !UUID_PATTERN.test(uuid)) {
93
+ return undefined;
94
+ }
95
+ return uuid.toLowerCase();
96
+ }
97
+
98
+ export async function cmdConvertAutomation(
99
+ options: ConvertAutomationCliOptions
100
+ ): Promise<number> {
101
+ const out =
102
+ options.out ?? ((text: string) => void process.stdout.write(text));
103
+ const err =
104
+ options.err ?? ((text: string) => void process.stderr.write(text));
105
+ const json = options.json === true;
106
+ // Failures stay machine-readable under --json (stderr keeps the prose).
107
+ const fail = (message: string): 1 => {
108
+ if (json) {
109
+ out(`${JSON.stringify({ ok: false, error: message })}\n`);
110
+ }
111
+ err(`${message}\n`);
112
+ return 1;
113
+ };
114
+
115
+ if (options.url === undefined || options.url.trim() === "") {
116
+ return fail(
117
+ `Usage: ${CLI} convert-automation <automation-url-or-uuid> [--dir <path>] [--json]`
118
+ );
119
+ }
120
+ const uuid = parseAutomationRef(options.url);
121
+ if (uuid === undefined) {
122
+ return fail(
123
+ `Could not parse ${JSON.stringify(options.url)} — expected an automation URL (https://cursor.com/automations/<uuid> or .../custom-agents/<uuid>) or a bare uuid.`
124
+ );
125
+ }
126
+
127
+ const resolved = await resolveApiKey({ apiKey: options.apiKey });
128
+ if (resolved === undefined) {
129
+ return fail(`Not signed in to Cursor. ${SIGN_IN_HINT}`);
130
+ }
131
+ const client = new AgentServeDeployClient({
132
+ backendUrl: cursorExternalApiUrl(),
133
+ apiKey: resolved.apiKey,
134
+ fetchImpl: options.fetchImpl,
135
+ });
136
+
137
+ let payload: AutomationExport;
138
+ try {
139
+ payload = await client.exportAutomation({ uuid });
140
+ } catch (error) {
141
+ if (error instanceof DeployApiError) {
142
+ return fail(error.message);
143
+ }
144
+ throw error;
145
+ }
146
+
147
+ const result = convertWorkflow({
148
+ automation: payload.automation,
149
+ workflow: payload.workflow as WorkflowJson,
150
+ mcpServers: payload.mcpServers,
151
+ exportedAt: new Date().toISOString(),
152
+ });
153
+
154
+ const displayDir =
155
+ options.dir ?? `./${slugify(payload.automation.name, "automation")}`;
156
+ const rootDir = resolve(options.cwd ?? process.cwd(), displayDir);
157
+
158
+ let files: InitFileEntry[];
159
+ try {
160
+ files = await writeProjectFiles(rootDir, result);
161
+ } catch (error) {
162
+ return fail(
163
+ `Could not write project files under ${rootDir}: ${describe(error)}`
164
+ );
165
+ }
166
+
167
+ // Same npm install as `init`, so the printed `validate` / `dev` next steps
168
+ // work immediately. Failure (npm exit or spawn error) is loud but never
169
+ // fails the conversion.
170
+ const install = options.installImpl ?? installScaffoldDependencies;
171
+ if (!json) {
172
+ out("\nInstalling npm packages\n");
173
+ }
174
+ let installed: Awaited<ReturnType<typeof installScaffoldDependencies>>;
175
+ try {
176
+ installed = await install(rootDir, {
177
+ stdio: json ? "pipe" : "inherit",
178
+ });
179
+ } catch (error) {
180
+ installed = { ok: false, exitCode: -1, stderr: describe(error) };
181
+ }
182
+ if (!installed.ok && !json) {
183
+ const reason = installed.stderr.trim() || `exit ${installed.exitCode}`;
184
+ err(
185
+ `npm install failed (${reason}); run \`npm install\` in ${displayDir} before \`${CLI} dev\`.\n`
186
+ );
187
+ }
188
+
189
+ if (json) {
190
+ out(
191
+ `${JSON.stringify(
192
+ {
193
+ ok: true,
194
+ directory: rootDir,
195
+ files: files.map(({ path, action }) => ({ path, action })),
196
+ warnings: result.warnings,
197
+ setupSteps: result.setupSteps,
198
+ installed: installed.ok,
199
+ installError: installed.ok
200
+ ? null
201
+ : installed.stderr.trim() ||
202
+ `npm install exited ${installed.exitCode}`,
203
+ mcpConnections: result.mcpConnections.map((connection) => ({
204
+ name: connection.name,
205
+ kind: connection.kind,
206
+ })),
207
+ },
208
+ null,
209
+ 2
210
+ )}\n`
211
+ );
212
+ return 0;
213
+ }
214
+
215
+ out(
216
+ renderSummary({
217
+ automationName: payload.automation.name,
218
+ displayDir,
219
+ rootDir,
220
+ cwd: options.cwd ?? process.cwd(),
221
+ files,
222
+ result,
223
+ })
224
+ );
225
+ return 0;
226
+ }
227
+
228
+ /**
229
+ * Converter files first (wx — existing files are kept), then the init
230
+ * scaffold fills in around them with the same skip-existing semantics.
231
+ */
232
+ async function writeProjectFiles(
233
+ rootDir: string,
234
+ result: ConvertWorkflowResult
235
+ ): Promise<InitFileEntry[]> {
236
+ const entries: InitFileEntry[] = [];
237
+ const converterPaths = new Set<string>();
238
+ await mkdir(rootDir, { recursive: true });
239
+ for (const file of result.files) {
240
+ converterPaths.add(file.path);
241
+ const abs = join(rootDir, file.path);
242
+ await mkdir(dirname(abs), { recursive: true });
243
+ try {
244
+ await writeFile(abs, file.contents, { flag: "wx" });
245
+ entries.push({ path: file.path, action: "create" });
246
+ } catch (error) {
247
+ if (!isEexist(error)) {
248
+ throw error;
249
+ }
250
+ entries.push({ path: file.path, action: "exist" });
251
+ }
252
+ }
253
+
254
+ const init = await initProject({ dir: rootDir });
255
+
256
+ // Scaffold files that make sense in a fresh `init` but not in an exported
257
+ // automation: the default memory hook (only when the automation had memory
258
+ // enabled — the converter emits its own copy then) and the echo demo tool.
259
+ const dropPaths = new Set<string>();
260
+ for (const scaffoldPath of ["agent/hooks/memory.ts", "agent/tools/echo.ts"]) {
261
+ if (
262
+ !converterPaths.has(scaffoldPath) &&
263
+ init.created.includes(scaffoldPath)
264
+ ) {
265
+ dropPaths.add(scaffoldPath);
266
+ await rm(join(rootDir, scaffoldPath));
267
+ }
268
+ }
269
+ for (const entry of init.files) {
270
+ if (converterPaths.has(entry.path) || dropPaths.has(entry.path)) {
271
+ continue;
272
+ }
273
+ entries.push(entry);
274
+ }
275
+ return entries;
276
+ }
277
+
278
+ function renderSummary(args: {
279
+ automationName: string;
280
+ displayDir: string;
281
+ rootDir: string;
282
+ cwd: string;
283
+ files: InitFileEntry[];
284
+ result: ConvertWorkflowResult;
285
+ }): string {
286
+ const c = stdoutPalette();
287
+ const lines: string[] = [];
288
+ lines.push("");
289
+ lines.push(`Converted "${args.automationName}" → ${args.displayDir}`);
290
+ lines.push("");
291
+ let anyExisting = false;
292
+ for (const file of args.files) {
293
+ if (file.path.endsWith(".gitkeep")) {
294
+ continue;
295
+ }
296
+ anyExisting ||= file.action === "exist";
297
+ lines.push(c.dim(`${file.action.padStart(12)} ${file.path}`));
298
+ }
299
+ if (anyExisting) {
300
+ lines.push(c.dim(`\n "exist" files already existed and were kept as-is.`));
301
+ }
302
+
303
+ const warnings = args.result.warnings;
304
+ if (warnings.length > 0) {
305
+ lines.push("");
306
+ lines.push("Warnings:");
307
+ for (const warning of warnings) {
308
+ lines.push(` - ${warning}`);
309
+ }
310
+ }
311
+
312
+ if (args.result.setupSteps.length > 0) {
313
+ lines.push("");
314
+ lines.push("Setup steps:");
315
+ for (const [index, step] of args.result.setupSteps.entries()) {
316
+ lines.push(` ${index + 1}. ${step}`);
317
+ }
318
+ }
319
+
320
+ if (args.result.mcpConnections.length > 0) {
321
+ lines.push("");
322
+ lines.push("MCP connections:");
323
+ for (const connection of args.result.mcpConnections) {
324
+ lines.push(` - ${connection.name}: ${connectionStatus(connection)}`);
325
+ }
326
+ }
327
+
328
+ lines.push("");
329
+ lines.push("Next steps:");
330
+ const cd = formatCdCommand(args.cwd, args.rootDir);
331
+ const steps = [
332
+ ...(cd === undefined ? [] : [cd]),
333
+ `${CLI} validate`,
334
+ `${CLI} dev`,
335
+ ];
336
+ for (const [index, step] of steps.entries()) {
337
+ lines.push(` ${index + 1}. ${step}`);
338
+ }
339
+ lines.push("");
340
+ return `${lines.join("\n")}\n`;
341
+ }
342
+
343
+ function connectionStatus(connection: ConvertedMcpConnection): string {
344
+ if (connection.kind === "cursorAccount") {
345
+ return "resolved from your Cursor account at runtime — manage authorization in Cursor (dashboard → MCP)";
346
+ }
347
+ return "needs manual setup — see the stub file and warnings above";
348
+ }
349
+
350
+ function describe(error: unknown): string {
351
+ return error instanceof Error ? error.message : String(error);
352
+ }
@@ -1345,7 +1345,7 @@ function promptHidden(
1345
1345
  }
1346
1346
 
1347
1347
  /** Buffered line reader over a piped stream; undefined once it ends. */
1348
- function createLineReader(
1348
+ export function createLineReader(
1349
1349
  stream: NodeJS.ReadableStream
1350
1350
  ): () => Promise<string | undefined> {
1351
1351
  let buffer = "";