@kubb/studio 5.11.2 → 5.11.3

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 CHANGED
@@ -85,9 +85,9 @@ running in a developer's project and a container running a fixed plugin set.
85
85
  the agent is a sandbox and carries its slug and Studio's version. From then on the agent pings every
86
86
  30 seconds, and Studio schedules one job at a time on the socket.
87
87
 
88
- With `allowRead`, the connect payload also includes a kept copy of the current `output.path` files.
89
- Studio can browse these files through `readFiles` before running a generation. Sandbox agents have
90
- no disk output, and output paths outside the project root are excluded.
88
+ With `allowRead`, the connect payload includes the current `output.path` file list. Studio reads
89
+ file contents from disk through `readFiles` when a user opens a file. Sandbox agents have no disk
90
+ output, and output paths outside the project root are excluded.
91
91
 
92
92
  Studio ends a socket on purpose with one of these close codes:
93
93
 
package/dist/index.cjs CHANGED
@@ -199,7 +199,7 @@ function getElapsedMs(hrStart) {
199
199
  }
200
200
  //#endregion
201
201
  //#region package.json
202
- var version = "5.11.2";
202
+ var version = "5.11.3";
203
203
  //#endregion
204
204
  //#region src/resolveConfig.ts
205
205
  /**
@@ -1473,6 +1473,33 @@ async function listDisk({ root, outputPath, maxFiles }) {
1473
1473
  paths: new Set(paths)
1474
1474
  };
1475
1475
  }
1476
+ async function readDiskFiles({ files: disk, paths, cursor = 0, limit }) {
1477
+ const files = {};
1478
+ const pages = {};
1479
+ await inParallel({
1480
+ items: paths.filter((path) => disk.paths.has(path)),
1481
+ limit: READ_CONCURRENCY,
1482
+ run: async (path) => {
1483
+ const content = await disk.storage.readItem((0, node_path.resolve)(disk.root, path));
1484
+ if (content === null) return;
1485
+ if (limit === void 0) {
1486
+ files[path] = content;
1487
+ return;
1488
+ }
1489
+ const lines = content.split(/\r\n|\r|\n/);
1490
+ const end = Math.min(cursor + limit, lines.length);
1491
+ files[path] = lines.slice(cursor, end).join("\n");
1492
+ pages[path] = {
1493
+ nextCursor: end < lines.length ? end : null,
1494
+ totalLines: lines.length
1495
+ };
1496
+ }
1497
+ });
1498
+ return limit === void 0 ? { files } : {
1499
+ files,
1500
+ pages
1501
+ };
1502
+ }
1476
1503
  /**
1477
1504
  * Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
1478
1505
  * survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
@@ -2022,23 +2049,7 @@ var Agent = class extends capnweb.RpcTarget {
2022
2049
  maxFiles: DISK_SNAPSHOT_MAX_FILES
2023
2050
  });
2024
2051
  if (!files) return void 0;
2025
- const jobId = (0, node_crypto.randomUUID)();
2026
- const output = await this.#generations.keep({
2027
- jobId,
2028
- source: require_protocol.FileSource.OUTPUT,
2029
- files,
2030
- maxSetMb: this.#limits.maxSnapshotMb,
2031
- normalize: false
2032
- });
2033
- await this.#generations.add({
2034
- jobId,
2035
- output
2036
- });
2037
- return {
2038
- jobId,
2039
- files: output.paths,
2040
- hashes: output.hashes
2041
- };
2052
+ return { files: [...files.paths] };
2042
2053
  }
2043
2054
  startGeneration(input) {
2044
2055
  const stream = createGenerationStream(this.#options.hooks, input.jobId);
@@ -2065,7 +2076,7 @@ var Agent = class extends capnweb.RpcTarget {
2065
2076
  severity: "error",
2066
2077
  message: "Connection is closed"
2067
2078
  });
2068
- if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
2079
+ if (this.#session.active) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_IN_PROGRESS });
2069
2080
  const settled = Promise.withResolvers();
2070
2081
  this.#session.active = {
2071
2082
  jobId: input.jobId,
@@ -2096,7 +2107,7 @@ var Agent = class extends capnweb.RpcTarget {
2096
2107
  async #generate({ input, root, signal }) {
2097
2108
  const { jobId, config: patch } = input;
2098
2109
  const studioInput = this.#sandbox ? patch?.input : void 0;
2099
- if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: "A sandbox agent generates only from the OpenAPI document content Studio sends, not from a file path or URL" });
2110
+ if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_INPUT_ONLY });
2100
2111
  const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
2101
2112
  const [plugins, adapter, parsers] = await Promise.all([
2102
2113
  mergePlugins({
@@ -2229,7 +2240,7 @@ var Agent = class extends capnweb.RpcTarget {
2229
2240
  message: require_protocol.AgentMessage.READ_AST_NOT_ALLOWED,
2230
2241
  permission: require_protocol.AgentPermission.READ_AST
2231
2242
  });
2232
- if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
2243
+ if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: require_protocol.AgentMessage.JOB_REQUIRED });
2233
2244
  const generation = await this.#generations.get(input.jobId);
2234
2245
  if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2235
2246
  const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
@@ -2246,14 +2257,32 @@ var Agent = class extends capnweb.RpcTarget {
2246
2257
  message: require_protocol.AgentMessage.READ_NOT_ALLOWED,
2247
2258
  permission: require_protocol.AgentPermission.READ
2248
2259
  });
2249
- if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
2260
+ if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId || !["generation", "currentDisk"].includes(input.jobType)) return this.#refuse({ message: require_protocol.AgentMessage.FILE_REQUEST_INVALID });
2250
2261
  const { paths } = input;
2251
- if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
2262
+ if (paths.length > 50) return this.#refuse({ message: require_protocol.AgentMessage.TOO_MANY_FILES });
2263
+ if (!isValidPage(input)) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
2264
+ if (input.jobType === "currentDisk") {
2265
+ if (this.#sandbox) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_DISK_UNAVAILABLE });
2266
+ const config = await this.#options.loadConfig();
2267
+ const disk = await listDisk({
2268
+ root: this.#options.root,
2269
+ outputPath: config.output.path,
2270
+ maxFiles: DISK_SNAPSHOT_MAX_FILES
2271
+ });
2272
+ if (!disk) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2273
+ const result = await readDiskFiles({
2274
+ files: disk,
2275
+ paths,
2276
+ cursor: input.cursor,
2277
+ limit: input.limit
2278
+ });
2279
+ await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
2280
+ return result;
2281
+ }
2252
2282
  const generation = await this.#generations.get(input.jobId);
2253
2283
  if (!generation) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2254
- if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
2255
2284
  const source = input.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
2256
- if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2285
+ if (!generation[source]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
2257
2286
  const result = await this.#generations.read({
2258
2287
  generation,
2259
2288
  source,
@@ -2274,15 +2303,15 @@ var Agent = class extends capnweb.RpcTarget {
2274
2303
  permission: require_protocol.AgentPermission.READ
2275
2304
  });
2276
2305
  const { jobId, path, base, cursor = 0, limit } = input;
2277
- if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: "The request must name a job, a base, and a path" });
2306
+ if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: require_protocol.AgentMessage.DIFF_REQUEST_INVALID });
2278
2307
  if (!isValidPage({
2279
2308
  cursor,
2280
2309
  limit
2281
- })) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
2310
+ })) return this.#refuse({ message: require_protocol.AgentMessage.INVALID_PAGE });
2282
2311
  const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
2283
2312
  if (!generation || !baseGeneration) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_GONE });
2284
2313
  const baseSource = base.source === require_protocol.FileSource.DISK ? require_protocol.FileSource.DISK : require_protocol.FileSource.OUTPUT;
2285
- if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
2314
+ if (!baseGeneration[baseSource]) return this.#refuse({ message: require_protocol.AgentMessage.NO_DISK_SNAPSHOT });
2286
2315
  const [after, before] = await Promise.all([this.#generations.read({
2287
2316
  generation,
2288
2317
  source: require_protocol.FileSource.OUTPUT,
@@ -2293,7 +2322,7 @@ var Agent = class extends capnweb.RpcTarget {
2293
2322
  paths: [path]
2294
2323
  })]);
2295
2324
  const afterContent = after.files[path];
2296
- if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
2325
+ if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: require_protocol.AgentMessage.HASHES_ONLY });
2297
2326
  const beforeContent = before.files[path];
2298
2327
  if (beforeContent === void 0) return {
2299
2328
  hunks: [],