@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.
- package/README.md +1 -1
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +0 -5
- package/dist/bin/agent-serve.d.ts +1 -0
- package/dist/bin/agent-serve.d.ts.map +1 -1
- package/dist/bin/agent-serve.js +32 -2
- package/dist/channels/github/types.d.ts +10 -7
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.CgZqLyWL.js → app.D5Mv1T0U.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.Cu7b6o1D.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.C4KigoQn.js → VPLocalSearchBox.CMq_BQce.js} +1 -1
- package/dist/docs/assets/chunks/{theme.C_Ty4yT7.js → theme.C6D9UPLK.js} +2 -2
- package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
- package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +10 -8
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/files-backends/agent-store-presigned-url.d.ts +3 -11
- package/dist/files.d.ts +20 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +20 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/artifacts-store.d.ts +14 -6
- package/dist/internal/artifacts-store.d.ts.map +1 -1
- package/dist/internal/artifacts-store.js +160 -16
- package/dist/internal/cli-convert-automation.d.ts +39 -0
- package/dist/internal/cli-convert-automation.d.ts.map +1 -0
- package/dist/internal/cli-convert-automation.js +280 -0
- package/dist/internal/cli-deploy.d.ts +2 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +1 -1
- package/dist/internal/cli-mcp-oauth.d.ts +23 -0
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +57 -33
- package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
- package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
- package/dist/internal/convert-automation/convert-workflow.js +998 -0
- package/dist/internal/convert-automation/index.d.ts +3 -0
- package/dist/internal/convert-automation/index.d.ts.map +1 -0
- package/dist/internal/convert-automation/index.js +1 -0
- package/dist/internal/convert-automation/slug.d.ts +11 -0
- package/dist/internal/convert-automation/slug.d.ts.map +1 -0
- package/dist/internal/convert-automation/slug.js +36 -0
- package/dist/internal/convert-automation/types.d.ts +191 -0
- package/dist/internal/convert-automation/types.d.ts.map +1 -0
- package/dist/internal/convert-automation/types.js +13 -0
- package/dist/internal/deploy-client.d.ts +31 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +48 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +3 -32
- package/dist/internal/host-kv.d.ts +6 -2
- package/dist/internal/host-kv.d.ts.map +1 -1
- package/dist/internal/init-project.d.ts +1 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +1 -1
- package/dist/internal/mcp-endpoint.d.ts +6 -0
- package/dist/internal/mcp-endpoint.d.ts.map +1 -1
- package/dist/internal/mcp-endpoint.js +168 -0
- package/dist/internal/resolved-connections.d.ts +15 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +12 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +75 -7
- package/dist/internal/session-engine.d.ts +56 -3
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +262 -84
- package/dist/internal/storage-coordinator.d.ts +9 -16
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +7 -52
- package/dist/internal/storage-roles.d.ts +78 -0
- package/dist/internal/storage-roles.d.ts.map +1 -0
- package/dist/internal/storage-roles.js +24 -0
- package/dist/internal/workspace.d.ts +36 -5
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +66 -0
- package/dist/memory.d.ts +22 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +100 -8
- package/dist/playground/assets/{index-CVlCAcUx.js → index-D7OV8B_H.js} +50 -50
- package/dist/playground/assets/index-DOb96C0M.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +0 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +0 -4
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/example-agents/weather-agent.md +29 -11
- package/docs/guides/cloud-runtime.md +6 -4
- package/package.json +3 -1
- package/src/artifacts.ts +0 -6
- package/src/bin/agent-serve.ts +36 -2
- package/src/channels/github/types.ts +10 -7
- package/src/files.ts +27 -0
- package/src/index.ts +6 -1
- package/src/internal/artifacts-store.ts +185 -22
- package/src/internal/cli-convert-automation.ts +352 -0
- package/src/internal/cli-deploy.ts +1 -1
- package/src/internal/cli-mcp-oauth.ts +69 -31
- package/src/internal/convert-automation/convert-workflow.ts +1422 -0
- package/src/internal/convert-automation/index.ts +10 -0
- package/src/internal/convert-automation/slug.ts +38 -0
- package/src/internal/convert-automation/types.ts +199 -0
- package/src/internal/deploy-client.ts +78 -0
- package/src/internal/discovery.ts +3 -37
- package/src/internal/host-kv.ts +6 -2
- package/src/internal/init-project.ts +1 -1
- package/src/internal/mcp-endpoint.ts +243 -0
- package/src/internal/resolved-connections.ts +32 -6
- package/src/internal/server.ts +99 -1
- package/src/internal/session-engine.ts +345 -103
- package/src/internal/storage-coordinator.ts +15 -63
- package/src/internal/storage-roles.ts +86 -0
- package/src/internal/workspace.ts +81 -6
- package/src/memory.ts +126 -10
- package/src/storage.ts +0 -6
- package/src/types.ts +15 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.B1uHGxBZ.js +0 -1
- package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
- package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
- package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
- package/dist/playground/assets/index-BRhBbnd4.css +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Durable artifact rows behind `ctx.artifacts`
|
|
2
|
+
* Durable artifact rows and payloads behind `ctx.artifacts`
|
|
3
|
+
* ({@link ArtifactsApi}).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
options.
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
308
|
+
await sink.delete(key(id));
|
|
222
309
|
},
|
|
223
310
|
async list(): Promise<ArtifactRecord[]> {
|
|
224
|
-
const
|
|
225
|
-
|
|
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 = "";
|