@standardagents/builder 0.20.1 → 0.21.1

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.
@@ -2521,7 +2521,9 @@ __export(files_exports, {
2521
2521
  dirname: () => dirname,
2522
2522
  isTextMimeType: () => isTextMimeType,
2523
2523
  normalizePath: () => normalizePath,
2524
- rowToFileRecord: () => rowToFileRecord
2524
+ resolveRenameDestination: () => resolveRenameDestination,
2525
+ rowToFileRecord: () => rowToFileRecord,
2526
+ validateMovePaths: () => validateMovePaths
2525
2527
  });
2526
2528
  function isTextMimeType(mimeType) {
2527
2529
  if (mimeType.startsWith("text/")) return true;
@@ -2554,6 +2556,33 @@ function dirname(path9) {
2554
2556
  if (lastSlash <= 0) return "/";
2555
2557
  return normalized.slice(0, lastSlash);
2556
2558
  }
2559
+ function validateMovePaths(source, destination, isDirectory) {
2560
+ if (source === "/") {
2561
+ throw new Error("Cannot move the root directory");
2562
+ }
2563
+ if (destination === "/") {
2564
+ throw new Error("Cannot move over the root directory");
2565
+ }
2566
+ if (isDirectory && destination.startsWith(source + "/")) {
2567
+ throw new Error(
2568
+ "Cannot move a directory into itself or one of its descendants"
2569
+ );
2570
+ }
2571
+ }
2572
+ function resolveRenameDestination(path9, newName) {
2573
+ if (typeof newName !== "string" || newName.length === 0) {
2574
+ throw new Error("New name must be a non-empty string");
2575
+ }
2576
+ if (newName.includes("/")) {
2577
+ throw new Error("New name must not contain path separators");
2578
+ }
2579
+ if (newName === "." || newName === "..") {
2580
+ throw new Error("New name must not be '.' or '..'");
2581
+ }
2582
+ const normalized = normalizePath(path9);
2583
+ const parent = dirname(normalized);
2584
+ return parent === "/" ? `/${newName}` : `${parent}/${newName}`;
2585
+ }
2557
2586
  function rowToFileRecord(row) {
2558
2587
  return {
2559
2588
  path: row.path,
@@ -3030,6 +3059,94 @@ var init_files = __esm({
3030
3059
  normalizedPath
3031
3060
  );
3032
3061
  }
3062
+ /**
3063
+ * Move (or rename) a file or directory from one path to another.
3064
+ *
3065
+ * Works for inline files, chunked files, and directories. Moving a
3066
+ * directory relocates the directory marker and every descendant entry,
3067
+ * preserving their relative layout. The destination must not already
3068
+ * exist, and a directory cannot be moved into itself or one of its own
3069
+ * descendants.
3070
+ *
3071
+ * Implementation note: the `file_chunks` table has a foreign key on
3072
+ * `files(path)` with `ON DELETE CASCADE`, and there is no `ON UPDATE`
3073
+ * action. To keep referential integrity intact (foreign key enforcement
3074
+ * is on in Durable Object SQLite), we copy the affected rows to their new
3075
+ * paths first, then delete the originals — the cascade removes the old
3076
+ * chunks, while the freshly copied chunks already point at the new file
3077
+ * rows. This never leaves an orphaned chunk mid-operation.
3078
+ */
3079
+ async move(sourcePath, destinationPath) {
3080
+ const source = normalizePath(sourcePath);
3081
+ const destination = normalizePath(destinationPath);
3082
+ if (source === destination) {
3083
+ const existing = await this.stat(source);
3084
+ if (!existing) {
3085
+ throw new Error(`Source path does not exist: ${source}`);
3086
+ }
3087
+ return existing;
3088
+ }
3089
+ const record24 = await this.stat(source);
3090
+ if (!record24) {
3091
+ throw new Error(`Source path does not exist: ${source}`);
3092
+ }
3093
+ validateMovePaths(source, destination, record24.isDirectory);
3094
+ const destPrefix = destination + "/";
3095
+ const collision = await this.sql.exec(
3096
+ `SELECT COUNT(*) as count FROM files WHERE path = ? OR path LIKE ? || '%'`,
3097
+ destination,
3098
+ destPrefix
3099
+ );
3100
+ if (collision.one().count > 0) {
3101
+ throw new Error(`Destination already exists: ${destination}`);
3102
+ }
3103
+ const sourcePrefix = source + "/";
3104
+ const sourceLen = source.length;
3105
+ const destName = basename(destination);
3106
+ await this.sql.exec(
3107
+ `INSERT INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at, width, height, is_chunked, chunk_count)
3108
+ SELECT ? || SUBSTR(path, ? + 1), name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at, width, height, is_chunked, chunk_count
3109
+ FROM files
3110
+ WHERE path = ? OR path LIKE ? || '%'`,
3111
+ destination,
3112
+ sourceLen,
3113
+ source,
3114
+ sourcePrefix
3115
+ );
3116
+ await this.sql.exec(
3117
+ `INSERT INTO file_chunks (file_path, chunk_index, data)
3118
+ SELECT ? || SUBSTR(file_path, ? + 1), chunk_index, data
3119
+ FROM file_chunks
3120
+ WHERE file_path = ? OR file_path LIKE ? || '%'`,
3121
+ destination,
3122
+ sourceLen,
3123
+ source,
3124
+ sourcePrefix
3125
+ );
3126
+ await this.sql.exec(
3127
+ `DELETE FROM files WHERE path = ? OR path LIKE ? || '%'`,
3128
+ source,
3129
+ sourcePrefix
3130
+ );
3131
+ await this.sql.exec(
3132
+ `UPDATE files SET name = ? WHERE path = ?`,
3133
+ destName,
3134
+ destination
3135
+ );
3136
+ const moved = await this.stat(destination);
3137
+ if (!moved) {
3138
+ throw new Error(`Failed to move ${source} to ${destination}`);
3139
+ }
3140
+ return moved;
3141
+ }
3142
+ /**
3143
+ * Rename a file or directory in place, keeping it in the same parent
3144
+ * directory. `newName` must be a bare file name with no path separators.
3145
+ */
3146
+ async rename(path9, newName) {
3147
+ const destination = resolveRenameDestination(path9, newName);
3148
+ return this.move(normalizePath(path9), destination);
3149
+ }
3033
3150
  /**
3034
3151
  * Get storage statistics
3035
3152
  */
@@ -12127,6 +12244,20 @@ var init_ThreadStateImpl = __esm({
12127
12244
  async rmdirFile(path9) {
12128
12245
  await this._threadInstance.rmdirFile(path9);
12129
12246
  }
12247
+ async moveFile(sourcePath, destinationPath) {
12248
+ const result = await this._threadInstance.moveFile(sourcePath, destinationPath);
12249
+ if (!result.success || !result.file) {
12250
+ throw new Error(result.error || "Failed to move file");
12251
+ }
12252
+ return this._mapFileRecord(result.file);
12253
+ }
12254
+ async renameFile(path9, newName) {
12255
+ const result = await this._threadInstance.renameFile(path9, newName);
12256
+ if (!result.success || !result.file) {
12257
+ throw new Error(result.error || "Failed to rename file");
12258
+ }
12259
+ return this._mapFileRecord(result.file);
12260
+ }
12130
12261
  async getFileStats() {
12131
12262
  const stats = await this._threadInstance.getFileStats();
12132
12263
  return {
@@ -66375,6 +66506,56 @@ async function verifyPlatformSignedPayload(token, publicJwkValue, options) {
66375
66506
  return verified ? payload : null;
66376
66507
  }
66377
66508
 
66509
+ // src/api/auth/platform-cleanup.post.ts
66510
+ var platform_cleanup_post_default = defineController2(async ({ req, env: env2 }) => {
66511
+ const body = await req.json().catch(() => null);
66512
+ const token = typeof body?.cleanup === "string" ? body.cleanup : req.headers.get("Authorization")?.startsWith("Bearer ") ? req.headers.get("Authorization").slice("Bearer ".length) : null;
66513
+ if (!token) {
66514
+ return Response.json({ error: "Missing cleanup token" }, { status: 400 });
66515
+ }
66516
+ const payload = await verifyPlatformSignedPayload(
66517
+ token,
66518
+ env2.STANDARD_AGENTS_AUTH_PUBLIC_KEY_JWK ?? env2.STANDARD_AGENTS_AUTH_PUBLIC_KEY,
66519
+ {
66520
+ kind: "instance_cleanup",
66521
+ projectId: typeof env2.STANDARD_AGENTS_PROJECT_ID === "string" ? env2.STANDARD_AGENTS_PROJECT_ID : void 0
66522
+ }
66523
+ );
66524
+ if (!payload) {
66525
+ return Response.json({ error: "Invalid cleanup token" }, { status: 401 });
66526
+ }
66527
+ const agentBuilder = env2.AGENT_BUILDER.get(env2.AGENT_BUILDER.idFromName("singleton"));
66528
+ const deletedThreadIds = /* @__PURE__ */ new Set();
66529
+ for (let guard = 0; guard < 1e3; guard += 1) {
66530
+ const batch = await agentBuilder.listThreads({ limit: 100, offset: 0 });
66531
+ const next = batch.threads.filter((thread) => thread.id && !deletedThreadIds.has(thread.id));
66532
+ if (next.length === 0) break;
66533
+ for (const thread of next) {
66534
+ const treeIds = typeof agentBuilder.getThreadTreeIds === "function" ? await agentBuilder.getThreadTreeIds(thread.id) : [thread.id];
66535
+ const ids = Array.from(new Set(treeIds.length > 0 ? treeIds : [thread.id]));
66536
+ if (!ids.includes(thread.id)) ids.push(thread.id);
66537
+ for (const id of ids) {
66538
+ if (deletedThreadIds.has(id)) continue;
66539
+ const durableId = env2.AGENT_BUILDER_THREAD.idFromName(id);
66540
+ const stub = env2.AGENT_BUILDER_THREAD.get(durableId);
66541
+ await stub.deleteThread();
66542
+ }
66543
+ for (const id of ids) {
66544
+ if (deletedThreadIds.has(id)) continue;
66545
+ await agentBuilder.deleteThread(id);
66546
+ deletedThreadIds.add(id);
66547
+ }
66548
+ }
66549
+ }
66550
+ if (typeof agentBuilder.deleteAllStorage === "function") {
66551
+ await agentBuilder.deleteAllStorage();
66552
+ }
66553
+ return Response.json({
66554
+ ok: true,
66555
+ deleted_threads: deletedThreadIds.size
66556
+ });
66557
+ });
66558
+
66378
66559
  // src/api/auth/platform-replica.post.ts
66379
66560
  var platform_replica_post_default = defineController2(async ({ req, env: env2 }) => {
66380
66561
  const body = await req.json().catch(() => null);
@@ -69648,6 +69829,13 @@ async function handlePost2(stub, path9, req) {
69648
69829
  const contentType = req.headers.get("Content-Type") || "";
69649
69830
  if (contentType.includes("application/json")) {
69650
69831
  const body = await req.json();
69832
+ if (body.action === "move" || body.action === "rename") {
69833
+ const result2 = body.action === "move" ? await stub.moveFile(path9, body.destination) : await stub.renameFile(path9, body.newName);
69834
+ if (!result2.success) {
69835
+ return Response.json({ error: result2.error }, { status: 400 });
69836
+ }
69837
+ return Response.json(toAttachmentRef(result2.file), { status: 200 });
69838
+ }
69651
69839
  if (body.type === "directory") {
69652
69840
  const result2 = await stub.mkdirFile(path9);
69653
69841
  if (!result2.success) {
@@ -70502,6 +70690,7 @@ var routeHandlers = {
70502
70690
  "GET:/auth/logout": logout_get_default,
70503
70691
  "POST:/auth/logout": logout_post_default,
70504
70692
  "GET:/auth/me": me_get_default,
70693
+ "POST:/auth/platform-cleanup": platform_cleanup_post_default,
70505
70694
  "POST:/auth/platform-replica": platform_replica_post_default,
70506
70695
  "GET:/envs/instance": instance_get_default,
70507
70696
  "PATCH:/envs/instance": instance_patch_default,