@productlobster/plob 0.1.2 → 0.1.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
@@ -76,12 +76,31 @@ Use an explicit idempotency key for a scripted retry:
76
76
  plob observe --kind signal --content "Three users asked for CSV export" --idempotency-key "$JOB_ID"
77
77
  ```
78
78
 
79
- Fetch and extract a prototype pack:
79
+ Fetch and extract a prototype pack into a new directory:
80
80
 
81
81
  ```bash
82
82
  plob prototype-pack --prototype-id proto_123 --out ./proto
83
83
  ```
84
84
 
85
+ The output directory must not already exist, and its parent must exist. Choose a
86
+ new name for each extraction; existing files, empty directories, and symlink
87
+ output roots are rejected without being changed. The CLI does not merge or
88
+ overwrite an earlier extraction. Parent path aliases are resolved to their
89
+ canonical location, which is returned as `extraction.outDir`. Create missing
90
+ parent directories yourself before running the command.
91
+
92
+ Extraction creates a fresh tree, uses exclusive file creation, and removes that
93
+ new tree if extraction fails. Unsafe archive paths and colliding filenames are
94
+ rejected, including case or Unicode aliases recognized by the host filesystem.
95
+ Directory-entry paths are validated, but empty directories are not extracted;
96
+ parent directories are created as needed for files. Inconsistent ZIP directory
97
+ counts, payload ranges, and decoded sizes are rejected. On POSIX the new
98
+ directories and files use permissions 0700 and 0600; Windows inherits the
99
+ caller's filesystem permissions. Use a trusted, stable parent directory on a
100
+ local filesystem: this does not defend against another process replacing parent
101
+ directories during extraction, and exclusive creation may not be reliable on
102
+ every network filesystem.
103
+
85
104
  `plob upload` attaches one file to the selected workspace. Use the workspace `id` from `plob workspaces`, not the display name. The connection must include the `write:upload` scope. Supported extensions: `.pdf`, `.docx`, `.txt`, `.md`, `.markdown`, `.png`, `.jpg`, `.jpeg`, and `.webp`; files must be non-empty and at most 10 MB.
86
105
 
87
106
  ```bash
package/dist/cli.js CHANGED
@@ -26,9 +26,9 @@ ProductLobster runs product analyses and keeps a product workspace's documents a
26
26
  A full analysis is asynchronous and durable. The loop is the same for every workflow:
27
27
 
28
28
  1. **Start.** Call \`run-start\` with a \`workflow\` of \`launch\`, \`audit\`, \`focused-analysis\`, \`prototype\`, or \`refinement\`, plus its inputs. It returns a run id. If your request is too thin, it returns a \`needs_sharpening\` result that lists what to add \u2014 add detail and call \`run-start\` again.
29
- 2. **Poll.** Call \`run-status\` with the run id. It reports the current status and returns a \`since\` cursor; pass that cursor on the next poll to get only what changed. Runs take minutes \u2014 poll, do not block.
29
+ 2. **Poll.** Call \`run-status\` with the run id. It reports the current status and returns a \`since\` cursor; pass that cursor on the next poll to get only what changed. Runs take minutes \u2014 poll, do not block. Stop polling when the run's \`status\` is \`completed\`, \`failed\`, or \`cancelled\`. Do not wait for every stage to leave \`pending\`: a run that fails or is cancelled part-way leaves its unstarted stages \`pending\` permanently, and on a finished run \`pending\` means the stage never ran.
30
30
  3. **Resolve checkpoints.** When \`run-status\` reports that the run is waiting at a checkpoint, it lists the options. Pick one and call \`checkpoint-resolve\` with the run id and an \`optionId\` taken from that list. An invented or stale id is rejected.
31
- 4. **Read the result.** When the run is complete, \`run-output\` returns the structured result. Use \`document-list\` and \`document-get\` to read the documents the run produced, and \`prototype-list\` / \`prototype-pack\` for any prototype.
31
+ 4. **Read the result.** When the run is complete, \`run-output\` returns the structured result as generated. Its \`stageEdits\` list marks each stage whose document the user has changed since (\`hasUserEdits: true\`). When such an entry also carries a \`documentId\`, read that document with \`document-get\` rather than relying on the structured result. An entry with \`hasUserEdits: true\` and no \`documentId\` means the stage was edited but its document is not available through \`document-get\`; use \`output\` for that stage. Use \`document-list\` and \`document-get\` to read the documents the run produced, and \`prototype-list\` / \`prototype-pack\` for any prototype.
32
32
 
33
33
  Other run controls: \`run-cancel\` stops a run; \`run-replay\` re-runs one, optionally from a stage.
34
34
 
@@ -61,6 +61,7 @@ Every failure is a structured object: \`{ code, message, requestId, retryHint }\
61
61
  | \`invalid_arguments\` | A field was the wrong shape | Fix the named fields and retry |
62
62
  | \`conflict\` | State changed under you (e.g. a stale document token) | Re-read the current state, then retry |
63
63
  | \`rate_limited\` | Daily call budget or a short spike | Wait for the reset the result reports |
64
+ | \`allotment_exhausted\` | Included outcomes have been used; the key is read-only | Continue with read tools or ask the person to upgrade |
64
65
 
65
66
  Always keep \`requestId\` \u2014 it is the one join key for support.
66
67
 
@@ -14601,6 +14602,8 @@ var RUN_START_REFINEMENT_INSTRUCTION_MAX_LENGTH = 2e3;
14601
14602
  var runStartWorkflowSchema = external_exports.enum(["audit", "launch", "focused-analysis", "prototype", "refinement"]).describe("Which Brain action to start.");
14602
14603
  var runStartInputSchema = external_exports.object({
14603
14604
  workflow: runStartWorkflowSchema,
14605
+ /** `audit`: deliberate acknowledgement when server-owned readiness has gaps. */
14606
+ proceedWithGaps: external_exports.boolean().optional().describe("Proceed with server-recorded audit evidence gaps."),
14604
14607
  /** `launch`: the product idea to take through the launch pipeline. */
14605
14608
  idea: external_exports.string().optional().describe("Product idea for the launch workflow."),
14606
14609
  /** `focused-analysis`: the question to analyze against workspace context. */
@@ -14685,6 +14688,7 @@ var DOCUMENT_TYPE_VALUES = [
14685
14688
  "customer-research",
14686
14689
  "competitive-analysis",
14687
14690
  "customer-demand-analysis",
14691
+ "kill-shot-refutation",
14688
14692
  "strategic-framing",
14689
14693
  "solution-design",
14690
14694
  "prd-generation",
@@ -14705,7 +14709,7 @@ var documentContentInputSchema = documentContentSchema.refine(
14705
14709
  "Document content must not be empty"
14706
14710
  ).describe("Document content to write.");
14707
14711
  var documentListInputSchema = external_exports.object({
14708
- cursor: external_exports.string().optional().describe("Opaque cursor from a previous page."),
14712
+ cursor: external_exports.string().max(4096).optional().describe("Opaque cursor from a previous page."),
14709
14713
  latest: external_exports.boolean().optional().describe("Return only the newest matching document."),
14710
14714
  limit: external_exports.number().int().min(1).max(100).optional().describe("Maximum number of documents to return; server default is 20."),
14711
14715
  runId: external_exports.string().optional().describe("Identifier of the run to include."),
@@ -14753,7 +14757,7 @@ var documentPutOutputSchema = external_exports.object({
14753
14757
 
14754
14758
  // ../agentic-contract/prototype-list.ts
14755
14759
  var prototypeListInputSchema = external_exports.object({
14756
- cursor: external_exports.string().optional().describe("Opaque cursor from a previous response.")
14760
+ cursor: external_exports.string().max(4096).optional().describe("Opaque cursor from a previous response.")
14757
14761
  });
14758
14762
  var prototypeStatusEnum = external_exports.enum(["generating", "complete", "failed"]).describe("Coarse prototype done-ness status.");
14759
14763
  var prototypeSummarySchema = external_exports.object({
@@ -14949,7 +14953,10 @@ var ERROR_CODES = [
14949
14953
  "version_revoked",
14950
14954
  // Appended (PRO-1484) — append-only so older clients never see a leading
14951
14955
  // member shift; no AGENTIC_CONTRACT_SCHEMA_VERSION bump (additive code).
14952
- "service_unavailable"
14956
+ "service_unavailable",
14957
+ // Appended (PRO-1920) — distinct from a periodic rate limit so clients can
14958
+ // render the durable read-only conversion state without parsing prose.
14959
+ "allotment_exhausted"
14953
14960
  ];
14954
14961
  var errorCodeSchema = external_exports.enum(ERROR_CODES);
14955
14962
  var agenticErrorSchema = external_exports.object({
@@ -15088,25 +15095,90 @@ var runReplayOutputSchema = external_exports.object({
15088
15095
  });
15089
15096
 
15090
15097
  // ../agentic-contract/run-output.ts
15098
+ var RUN_OUTPUT_MAX_OUTPUT_BYTES = 65536;
15099
+ var RUN_OUTPUT_MAX_RESPONSE_BYTES = 98304;
15100
+ var RUN_OUTPUT_ENVELOPE_RESERVE_BYTES = 1024;
15101
+ var RUN_OUTPUT_MAX_DOCUMENT_IDS = 200;
15102
+ var RUN_OUTPUT_MAX_OMITTED_KEYS = 64;
15103
+ var RUN_OUTPUT_MAX_OMITTED_KEY_BYTES = 128;
15104
+ var RUN_OUTPUT_MAX_STAGE_EDITS = 32;
15105
+ var utf8Encoder2 = new TextEncoder();
15106
+ function measureRunOutputBytes(value) {
15107
+ const json2 = JSON.stringify(value);
15108
+ return json2 === void 0 ? 0 : utf8Encoder2.encode(json2).byteLength;
15109
+ }
15091
15110
  var runOutputInputSchema = external_exports.object({
15092
15111
  runId: external_exports.string().describe("Identifier of the run to read output from."),
15093
- stage: external_exports.string().optional().describe("Stage name to scope output to; omit for run-level output.")
15112
+ stage: external_exports.string().trim().min(1).optional().describe("Stage name to scope output to; omit for run-level output.")
15094
15113
  });
15095
- var runOutputOutputSchema = external_exports.object({
15114
+ var runOutputStageEditFields = {
15115
+ stage: external_exports.string().describe("Stage name, as passed to run-output's stage."),
15116
+ hasUserEdits: external_exports.boolean().describe(
15117
+ "True when the user changed this stage's document after it was generated, so output is out of date for this stage."
15118
+ ),
15119
+ documentId: external_exports.string().optional().describe(
15120
+ "The changed document, to read with document-get. Set only when hasUserEdits is true and document-get can read it; absent on an edited stage, use output."
15121
+ )
15122
+ };
15123
+ var runOutputFieldsSchema = external_exports.object({
15096
15124
  runId: external_exports.string().describe("Identifier of the run."),
15097
15125
  stage: external_exports.string().nullable().describe("Stage this output is scoped to, or null for run-level."),
15098
- output: external_exports.record(external_exports.string(), external_exports.unknown()).describe(
15099
- "Structured output: summaries and IDs, never large inline bodies."
15126
+ output: external_exports.record(external_exports.string(), external_exports.unknown()).refine(
15127
+ (value) => measureRunOutputBytes(value) <= RUN_OUTPUT_MAX_OUTPUT_BYTES,
15128
+ "Run output must not exceed 64 KiB"
15129
+ ).describe(
15130
+ "Structured output, at most 64 KiB of JSON: summaries and IDs, never large inline bodies."
15131
+ ),
15132
+ documentIds: external_exports.array(external_exports.string()).max(RUN_OUTPUT_MAX_DOCUMENT_IDS).describe("IDs of referenced documents to fetch via document-get."),
15133
+ truncated: external_exports.boolean().optional().describe(
15134
+ "True when entries were left out of output to stay within the size limit."
15135
+ ),
15136
+ omittedKeys: external_exports.array(
15137
+ external_exports.string().refine(
15138
+ (key) => measureRunOutputBytes(key) <= RUN_OUTPUT_MAX_OMITTED_KEY_BYTES,
15139
+ "Omitted key must not exceed 128 bytes"
15140
+ )
15141
+ ).max(RUN_OUTPUT_MAX_OMITTED_KEYS).optional().describe(
15142
+ "Top-level output keys left out for size, in order and at most 64: stage names for run-level output (request each by stage), field names for one stage (read the document via document-get)."
15143
+ ),
15144
+ omittedKeyCount: external_exports.number().int().nonnegative().optional().describe("How many top-level output keys were left out for size."),
15145
+ omittedKeysTruncated: external_exports.boolean().optional().describe(
15146
+ "True when omittedKeys lists fewer keys than omittedKeyCount (too many, or a key too long to list)."
15147
+ ),
15148
+ prototypeId: external_exports.string().optional().describe(
15149
+ "Set when prototype content was left out for size: the run's prototype, to fetch with prototype-pack."
15150
+ ),
15151
+ stageEdits: external_exports.array(
15152
+ external_exports.object({
15153
+ ...runOutputStageEditFields,
15154
+ stage: runOutputStageEditFields.stage.refine(
15155
+ (stage) => measureRunOutputBytes(stage) <= RUN_OUTPUT_MAX_OMITTED_KEY_BYTES,
15156
+ "Stage name must not exceed 128 bytes"
15157
+ )
15158
+ })
15159
+ ).max(RUN_OUTPUT_MAX_STAGE_EDITS).optional().describe(
15160
+ "One entry per stage with output, at most 32: whether the user changed that stage's document, and if so the document to read with document-get."
15100
15161
  ),
15101
- documentIds: external_exports.array(external_exports.string()).describe("IDs of referenced documents to fetch via document-get."),
15102
15162
  schemaVersion: schemaVersionSchema
15103
15163
  });
15164
+ var runOutputOutputSchema = runOutputFieldsSchema.refine(
15165
+ (response) => measureRunOutputBytes(response) <= RUN_OUTPUT_MAX_RESPONSE_BYTES - RUN_OUTPUT_ENVELOPE_RESERVE_BYTES,
15166
+ "Run output response must not exceed 96 KiB with its envelope"
15167
+ );
15168
+ var runOutputParseSchema = runOutputFieldsSchema.extend({
15169
+ output: external_exports.record(external_exports.string(), external_exports.unknown()),
15170
+ documentIds: external_exports.array(external_exports.string()),
15171
+ omittedKeys: external_exports.array(external_exports.string()).optional(),
15172
+ stageEdits: external_exports.array(external_exports.object(runOutputStageEditFields)).optional()
15173
+ });
15104
15174
 
15105
15175
  // ../agentic-contract/run-status.ts
15106
15176
  var runStageProgressSchema = external_exports.object({
15107
15177
  name: external_exports.string().describe("Stage identifier."),
15108
15178
  order: external_exports.number().int().positive().describe("1-based position in the run."),
15109
- status: external_exports.enum(["pending", "running", "completed", "failed"]).describe("Coarse per-stage status.")
15179
+ status: external_exports.enum(["pending", "running", "completed", "failed"]).describe(
15180
+ "Coarse per-stage status. On a run whose status is completed, failed, or cancelled, pending means the stage never ran; stop polling on the run status, not on stage statuses."
15181
+ )
15110
15182
  });
15111
15183
  var runTransitionSchema = external_exports.object({
15112
15184
  from: runStatusEnum.nullable().describe("Prior status, or null for the first transition."),
@@ -15180,6 +15252,9 @@ var AGENTIC_TOOL_NAMES = [
15180
15252
  ];
15181
15253
 
15182
15254
  // ../agentic-contract/workspace-identity.ts
15255
+ var workspaceListInputSchema = external_exports.object({
15256
+ cursor: external_exports.string().optional().describe("Opaque cursor from a previous response (reserved for Phase 2).")
15257
+ });
15183
15258
  var workspacePipelineTypeSchema = external_exports.enum(["LAUNCH", "AUDIT"]);
15184
15259
  var workspaceIdentitySchema = external_exports.object({
15185
15260
  id: external_exports.string().describe("Stable workspace identifier."),
@@ -15294,6 +15369,7 @@ var EXIT_CODES = {
15294
15369
  rateLimited: 16,
15295
15370
  versionRevoked: 17,
15296
15371
  serviceUnavailable: 18,
15372
+ allotmentExhausted: 19,
15297
15373
  network: 70
15298
15374
  };
15299
15375
  var ERROR_CODE_EXIT_CODES = {
@@ -15308,7 +15384,8 @@ var ERROR_CODE_EXIT_CODES = {
15308
15384
  version_revoked: EXIT_CODES.versionRevoked,
15309
15385
  // PRO-1484: transient backend outage — its own non-zero code so scripts can
15310
15386
  // distinguish a retryable outage from a hard internal fault (exit 1).
15311
- service_unavailable: EXIT_CODES.serviceUnavailable
15387
+ service_unavailable: EXIT_CODES.serviceUnavailable,
15388
+ allotment_exhausted: EXIT_CODES.allotmentExhausted
15312
15389
  };
15313
15390
  function mapErrorCodeToExitCode(code) {
15314
15391
  return ERROR_CODE_EXIT_CODES[code];
@@ -15864,6 +15941,17 @@ function parseCommaSeparatedList(value) {
15864
15941
  const items = value.split(",").map((item) => item.trim()).filter(Boolean);
15865
15942
  return items.length > 0 ? items : void 0;
15866
15943
  }
15944
+ function isUnsafeTerminalCodePoint(codePoint) {
15945
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 1564 || codePoint === 8206 || codePoint === 8207 || codePoint === 8232 || codePoint === 8233 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
15946
+ }
15947
+ function escapeTerminalText(value, maxCodePoints) {
15948
+ const codePoints = Array.from(value);
15949
+ const escaped = codePoints.slice(0, maxCodePoints).map((character) => {
15950
+ const codePoint = character.codePointAt(0) ?? 0;
15951
+ return isUnsafeTerminalCodePoint(codePoint) ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
15952
+ }).join("");
15953
+ return codePoints.length > maxCodePoints ? `${escaped}\u2026` : escaped;
15954
+ }
15867
15955
  function isRecord2(value) {
15868
15956
  return typeof value === "object" && value !== null && !Array.isArray(value);
15869
15957
  }
@@ -16811,44 +16899,86 @@ function renderPrototypeList(response) {
16811
16899
  }
16812
16900
 
16813
16901
  // src/runtime/zip-extract.ts
16814
- import { mkdir, writeFile } from "fs/promises";
16902
+ import { mkdir, realpath, rm, writeFile } from "fs/promises";
16815
16903
  import path2 from "path";
16816
16904
  import { inflateRawSync } from "zlib";
16817
16905
  var END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
16818
16906
  var CENTRAL_DIRECTORY_FILE_SIGNATURE = 33639248;
16907
+ var CENTRAL_DIRECTORY_DIGITAL_SIGNATURE = 84233040;
16819
16908
  var LOCAL_FILE_HEADER_SIGNATURE = 67324752;
16820
16909
  var ZIP64_SENTINEL = 65535;
16821
16910
  var ZIP64_SIZE_SENTINEL = 4294967295;
16911
+ var TRAILING_WINDOWS_ALIAS = /[ .]$/;
16912
+ var RESERVED_WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i;
16822
16913
  var MAX_ENTRY_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
16823
16914
  var MAX_TOTAL_DECOMPRESSED_BYTES = 256 * 1024 * 1024;
16824
16915
  var MAX_ENTRY_COUNT = 4096;
16916
+ var ZipOutputDirectoryError = class extends Error {
16917
+ constructor(message) {
16918
+ super(message);
16919
+ this.name = "ZipOutputDirectoryError";
16920
+ }
16921
+ };
16825
16922
  async function extractZipToDirectory(bytes, outDir) {
16826
16923
  const archive = Buffer.from(bytes);
16827
- const root = path2.resolve(outDir);
16828
- await mkdir(root, { recursive: true });
16829
- const entries = readCentralDirectory(archive);
16830
- if (entries.length > MAX_ENTRY_COUNT) {
16831
- throw new Error(
16832
- `ZIP archive exceeds the maximum of ${MAX_ENTRY_COUNT} entries.`
16833
- );
16924
+ const directory = readCentralDirectory(archive);
16925
+ const root = await createOutputRoot(outDir);
16926
+ try {
16927
+ return await extractEntries(archive, directory, root);
16928
+ } catch (error51) {
16929
+ await rm(root, { recursive: true, force: true });
16930
+ throw error51;
16931
+ }
16932
+ }
16933
+ async function createOutputRoot(outDir) {
16934
+ const requestedRoot = path2.resolve(outDir);
16935
+ let parent;
16936
+ try {
16937
+ parent = await realpath(path2.dirname(requestedRoot));
16938
+ } catch (error51) {
16939
+ if (hasFilesystemCode(error51, "ENOENT") || hasFilesystemCode(error51, "ENOTDIR")) {
16940
+ throw new ZipOutputDirectoryError(
16941
+ "The output parent directory must already exist. Create the parent, then choose a new --out directory."
16942
+ );
16943
+ }
16944
+ throw error51;
16945
+ }
16946
+ const root = path2.join(parent, path2.basename(requestedRoot));
16947
+ try {
16948
+ await mkdir(root, { mode: 448 });
16949
+ } catch (error51) {
16950
+ if (hasFilesystemCode(error51, "EEXIST")) {
16951
+ throw new ZipOutputDirectoryError(
16952
+ "The output directory must not already exist. Choose a new --out directory; existing files were not changed."
16953
+ );
16954
+ }
16955
+ if (hasFilesystemCode(error51, "ENOTDIR")) {
16956
+ throw new ZipOutputDirectoryError(
16957
+ "The output parent directory must already exist. Create the parent, then choose a new --out directory."
16958
+ );
16959
+ }
16960
+ throw error51;
16834
16961
  }
16962
+ return root;
16963
+ }
16964
+ async function extractEntries(archive, directory, root) {
16835
16965
  const files = [];
16836
16966
  let totalDecompressedBytes = 0;
16837
- for (const entry of entries) {
16967
+ for (const entry of directory.entries) {
16838
16968
  const normalizedName = normalizeZipEntryName(entry.name);
16839
- if (!normalizedName || normalizedName.endsWith("/")) {
16969
+ const targetPath = resolveSafeTarget(root, normalizedName);
16970
+ if (normalizedName.endsWith("/")) {
16840
16971
  continue;
16841
16972
  }
16842
- const targetPath = resolveSafeTarget(root, normalizedName);
16843
- const contents = readEntryContents(archive, entry);
16973
+ const contents = readEntryContents(archive, entry, directory.offset);
16844
16974
  totalDecompressedBytes += contents.length;
16845
16975
  if (totalDecompressedBytes > MAX_TOTAL_DECOMPRESSED_BYTES) {
16846
16976
  throw new Error(
16847
16977
  `ZIP archive exceeds the maximum decompressed size of ${MAX_TOTAL_DECOMPRESSED_BYTES} bytes.`
16848
16978
  );
16849
16979
  }
16850
- await mkdir(path2.dirname(targetPath), { recursive: true });
16851
- await writeFile(targetPath, contents);
16980
+ await mkdir(path2.dirname(targetPath), { recursive: true, mode: 448 });
16981
+ await writeFile(targetPath, contents, { flag: "wx", mode: 384 });
16852
16982
  files.push(normalizedName);
16853
16983
  }
16854
16984
  return {
@@ -16857,39 +16987,60 @@ async function extractZipToDirectory(bytes, outDir) {
16857
16987
  outDir: root
16858
16988
  };
16859
16989
  }
16990
+ function hasFilesystemCode(error51, code) {
16991
+ return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === code;
16992
+ }
16860
16993
  function readCentralDirectory(archive) {
16861
16994
  const eocdOffset = findEndOfCentralDirectory(archive);
16862
16995
  const entryCount = archive.readUInt16LE(eocdOffset + 10);
16863
16996
  const centralDirectorySize = archive.readUInt32LE(eocdOffset + 12);
16864
- let offset = archive.readUInt32LE(eocdOffset + 16);
16865
- if (entryCount === ZIP64_SENTINEL || centralDirectorySize === ZIP64_SIZE_SENTINEL || offset === ZIP64_SIZE_SENTINEL) {
16997
+ const directoryOffset = archive.readUInt32LE(eocdOffset + 16);
16998
+ if (entryCount === ZIP64_SENTINEL || centralDirectorySize === ZIP64_SIZE_SENTINEL || directoryOffset === ZIP64_SIZE_SENTINEL) {
16866
16999
  throw new Error("ZIP64 prototype packs are not supported.");
16867
17000
  }
16868
- const endOffset = offset + centralDirectorySize;
17001
+ if (entryCount > MAX_ENTRY_COUNT) {
17002
+ throw new Error(
17003
+ `ZIP archive exceeds the maximum of ${MAX_ENTRY_COUNT} entries.`
17004
+ );
17005
+ }
17006
+ const endOffset = directoryOffset + centralDirectorySize;
17007
+ if (endOffset > eocdOffset) {
17008
+ throw new Error("Invalid ZIP central directory.");
17009
+ }
16869
17010
  const entries = [];
16870
- while (offset < endOffset && entries.length < entryCount) {
16871
- if (archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_FILE_SIGNATURE) {
17011
+ let offset = directoryOffset;
17012
+ while (entries.length < entryCount) {
17013
+ if (offset + 46 > endOffset || archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_FILE_SIGNATURE) {
16872
17014
  throw new Error("Invalid ZIP central directory.");
16873
17015
  }
16874
17016
  const flags = archive.readUInt16LE(offset + 8);
16875
17017
  const compressionMethod = archive.readUInt16LE(offset + 10);
16876
17018
  const compressedSize = archive.readUInt32LE(offset + 20);
17019
+ const uncompressedSize = archive.readUInt32LE(offset + 24);
16877
17020
  const localHeaderOffset = archive.readUInt32LE(offset + 42);
16878
17021
  const nameLength = archive.readUInt16LE(offset + 28);
16879
17022
  const extraLength = archive.readUInt16LE(offset + 30);
16880
17023
  const commentLength = archive.readUInt16LE(offset + 32);
16881
17024
  const nameStart = offset + 46;
17025
+ const nextOffset = nameStart + nameLength + extraLength + commentLength;
17026
+ if (nextOffset > endOffset) {
17027
+ throw new Error("Invalid ZIP central directory.");
17028
+ }
16882
17029
  const name = archive.subarray(nameStart, nameStart + nameLength).toString("utf8");
16883
17030
  entries.push({
16884
17031
  compressedSize,
16885
17032
  compressionMethod,
16886
17033
  flags,
16887
17034
  localHeaderOffset,
16888
- name
17035
+ name,
17036
+ uncompressedSize
16889
17037
  });
16890
- offset = nameStart + nameLength + extraLength + commentLength;
17038
+ offset = nextOffset;
17039
+ }
17040
+ if (offset !== endOffset && (offset + 6 > endOffset || archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_DIGITAL_SIGNATURE || offset + 6 + archive.readUInt16LE(offset + 4) !== endOffset)) {
17041
+ throw new Error("Invalid ZIP central directory.");
16891
17042
  }
16892
- return entries;
17043
+ return { entries, offset: directoryOffset };
16893
17044
  }
16894
17045
  function findEndOfCentralDirectory(archive) {
16895
17046
  const minOffset = Math.max(0, archive.length - 65557);
@@ -16900,16 +17051,19 @@ function findEndOfCentralDirectory(archive) {
16900
17051
  }
16901
17052
  throw new Error("Invalid ZIP archive.");
16902
17053
  }
16903
- function readEntryContents(archive, entry) {
17054
+ function readEntryContents(archive, entry, directoryOffset) {
16904
17055
  if (entry.flags % 2 === 1) {
16905
17056
  throw new Error("Encrypted ZIP entries are not supported.");
16906
17057
  }
16907
- if (archive.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_HEADER_SIGNATURE) {
17058
+ if (entry.localHeaderOffset + 30 > directoryOffset || archive.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_HEADER_SIGNATURE) {
16908
17059
  throw new Error("Invalid ZIP local file header.");
16909
17060
  }
16910
17061
  const localNameLength = archive.readUInt16LE(entry.localHeaderOffset + 26);
16911
17062
  const localExtraLength = archive.readUInt16LE(entry.localHeaderOffset + 28);
16912
17063
  const dataStart = entry.localHeaderOffset + 30 + localNameLength + localExtraLength;
17064
+ if (dataStart + entry.compressedSize > directoryOffset) {
17065
+ throw new Error("Invalid ZIP entry data range.");
17066
+ }
16913
17067
  const compressed = archive.subarray(
16914
17068
  dataStart,
16915
17069
  dataStart + entry.compressedSize
@@ -16920,13 +17074,20 @@ function readEntryContents(archive, entry) {
16920
17074
  `ZIP entry exceeds the maximum decompressed size of ${MAX_ENTRY_DECOMPRESSED_BYTES} bytes.`
16921
17075
  );
16922
17076
  }
17077
+ if (compressed.length !== entry.uncompressedSize) {
17078
+ throw new Error("Invalid ZIP entry decompressed size.");
17079
+ }
16923
17080
  return Buffer.from(compressed);
16924
17081
  }
16925
17082
  if (entry.compressionMethod === 8) {
16926
17083
  try {
16927
- return inflateRawSync(compressed, {
17084
+ const contents = inflateRawSync(compressed, {
16928
17085
  maxOutputLength: MAX_ENTRY_DECOMPRESSED_BYTES
16929
17086
  });
17087
+ if (contents.length !== entry.uncompressedSize) {
17088
+ throw new Error("Invalid ZIP entry decompressed size.");
17089
+ }
17090
+ return contents;
16930
17091
  } catch (error51) {
16931
17092
  if (isMaxOutputLengthError(error51)) {
16932
17093
  throw new Error(
@@ -16945,15 +17106,21 @@ function isMaxOutputLengthError(error51) {
16945
17106
  }
16946
17107
  function normalizeZipEntryName(name) {
16947
17108
  const normalized = name.replaceAll("\\", "/");
16948
- if (normalized.length === 0 || normalized.startsWith("/") || normalized.includes("\0")) {
16949
- return null;
17109
+ if (normalized.length === 0 || normalized.startsWith("/") || normalized.includes(":") || normalized.includes("\0") || normalized.split("/").some(isUnsafeWindowsComponent)) {
17110
+ throw new Error(`Unsafe ZIP entry path: ${name}`);
16950
17111
  }
16951
17112
  return normalized;
16952
17113
  }
17114
+ function isUnsafeWindowsComponent(component) {
17115
+ if (component === "." || component === "..") {
17116
+ return false;
17117
+ }
17118
+ return TRAILING_WINDOWS_ALIAS.test(component) || RESERVED_WINDOWS_DEVICE.test(component);
17119
+ }
16953
17120
  function resolveSafeTarget(root, entryName) {
16954
17121
  const targetPath = path2.resolve(root, entryName);
16955
17122
  const relative = path2.relative(root, targetPath);
16956
- if (relative.startsWith("..") || path2.isAbsolute(relative)) {
17123
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
16957
17124
  throw new Error(`Unsafe ZIP entry path: ${entryName}`);
16958
17125
  }
16959
17126
  return targetPath;
@@ -16961,7 +17128,7 @@ function resolveSafeTarget(root, entryName) {
16961
17128
 
16962
17129
  // src/commands/prototype-pack.ts
16963
17130
  function registerPrototypePackCommand(program, options) {
16964
- const command = program.command("prototype-pack").description("Fetch a prototype ZIP reference.").requiredOption("--prototype-id <id>", "Prototype id.").option("--out <dir>", "Directory to extract the ZIP into.");
17131
+ const command = program.command("prototype-pack").description("Fetch a prototype ZIP reference.").requiredOption("--prototype-id <id>", "Prototype id.").option("--out <dir>", "New output directory (parent must already exist).");
16965
17132
  command.action(async (commandOptions) => {
16966
17133
  const exitCode2 = await runPrototypePackCommand({
16967
17134
  commandOptions,
@@ -17061,7 +17228,20 @@ async function fetchAndExtractPack(input) {
17061
17228
  });
17062
17229
  }
17063
17230
  const archive = new Uint8Array(await response.arrayBuffer());
17064
- return await extractZipToDirectory(archive, input.outDir);
17231
+ try {
17232
+ return await extractZipToDirectory(archive, input.outDir);
17233
+ } catch (error51) {
17234
+ if (error51 instanceof ZipOutputDirectoryError) {
17235
+ throw createCliOperationalError({
17236
+ code: "internal",
17237
+ message: error51.message
17238
+ });
17239
+ }
17240
+ throw createCliOperationalError({
17241
+ code: "internal",
17242
+ message: "The prototype ZIP could not be safely extracted. Check the output parent permissions and retry with a new --out directory; if the failure persists, report it."
17243
+ });
17244
+ }
17065
17245
  }
17066
17246
  function isSameOrigin(packUrl, endpoint) {
17067
17247
  try {
@@ -17231,7 +17411,9 @@ function runRunOutputCommand(options) {
17231
17411
  }
17232
17412
  return runCommand(
17233
17413
  {
17234
- outputSchema: runOutputOutputSchema,
17414
+ // The tolerant consumer schema: a newer plob still reads a server that
17415
+ // predates the run-output size bounds.
17416
+ outputSchema: runOutputParseSchema,
17235
17417
  renderHuman: renderRunOutput,
17236
17418
  toolArguments: compactRecord({
17237
17419
  runId: runId.value,
@@ -17246,16 +17428,69 @@ function runRunOutputCommand(options) {
17246
17428
  }
17247
17429
  );
17248
17430
  }
17431
+ var OMITTED_KEY_DISPLAY_MAX = 80;
17432
+ var PROTOTYPE_ID_DISPLAY_MAX = 80;
17249
17433
  function renderRunOutput(response) {
17250
17434
  const scope = response.stage ? ` stage ${response.stage}` : "";
17251
17435
  const lines = [`Run ${response.runId}${scope} output`];
17252
17436
  lines.push(`Fields: ${Object.keys(response.output).length}`);
17437
+ const omitted = renderOmittedKeys(response);
17438
+ if (omitted) {
17439
+ lines.push(`Omitted for size: ${omitted}`);
17440
+ }
17441
+ if (response.prototypeId !== void 0) {
17442
+ const prototypeId = escapeTerminalText(
17443
+ response.prototypeId,
17444
+ PROTOTYPE_ID_DISPLAY_MAX
17445
+ );
17446
+ lines.push(
17447
+ `Prototype: ${prototypeId} (fetch with \`plob prototype-pack --prototype-id ${prototypeId}\`)`
17448
+ );
17449
+ }
17450
+ const edited = renderEditedStages(response);
17451
+ if (edited) {
17452
+ lines.push(
17453
+ `Edited by the user (read with \`plob document-get\`): ${edited}`
17454
+ );
17455
+ }
17253
17456
  if (response.documentIds.length > 0) {
17254
- lines.push(`Documents: ${response.documentIds.join(", ")}`);
17457
+ lines.push(
17458
+ `Documents: ${response.documentIds.map((id) => escapeTerminalText(id, DOCUMENT_ID_DISPLAY_MAX)).join(", ")}`
17459
+ );
17255
17460
  }
17256
17461
  return `${lines.join("\n")}
17257
17462
  `;
17258
17463
  }
17464
+ var DOCUMENT_ID_DISPLAY_MAX = 80;
17465
+ function renderEditedStages(response) {
17466
+ const edited = (response.stageEdits ?? []).filter(
17467
+ (entry) => entry.hasUserEdits
17468
+ );
17469
+ if (edited.length === 0) {
17470
+ return;
17471
+ }
17472
+ return edited.map((entry) => {
17473
+ const stage = escapeTerminalText(entry.stage, OMITTED_KEY_DISPLAY_MAX);
17474
+ return entry.documentId === void 0 ? stage : `${stage} (${escapeTerminalText(entry.documentId, DOCUMENT_ID_DISPLAY_MAX)})`;
17475
+ }).join(", ");
17476
+ }
17477
+ function renderOmittedKeys(response) {
17478
+ if (response.truncated !== true) {
17479
+ return;
17480
+ }
17481
+ const listed = (response.omittedKeys ?? []).map(
17482
+ (key) => escapeTerminalText(key, OMITTED_KEY_DISPLAY_MAX)
17483
+ );
17484
+ const unlisted = Math.max(
17485
+ 0,
17486
+ (response.omittedKeyCount ?? listed.length) - listed.length
17487
+ );
17488
+ if (listed.length === 0) {
17489
+ return unlisted > 0 ? `${unlisted} ${unlisted === 1 ? "entry" : "entries"}` : void 0;
17490
+ }
17491
+ const names = listed.join(", ");
17492
+ return unlisted > 0 ? `${names} (and ${unlisted} more)` : names;
17493
+ }
17259
17494
 
17260
17495
  // src/commands/run-replay.ts
17261
17496
  function registerRunReplayCommand(program, options) {
@@ -17368,7 +17603,7 @@ function renderRunStatus(response) {
17368
17603
  // src/commands/upload.ts
17369
17604
  import { readFile as readFile2, stat } from "fs/promises";
17370
17605
  import path3 from "path";
17371
- var MAX_UPLOAD_FILE_SIZE_BYTES = 10 * 1024 * 1024;
17606
+ var MAX_UPLOAD_FILE_SIZE_BYTES = 25 * 1024 * 1024;
17372
17607
  var DOCUMENT_UPLOAD_FIELD_NAME = "files";
17373
17608
  var MIME_TYPES_BY_EXTENSION = /* @__PURE__ */ new Map([
17374
17609
  [
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ declare const EXIT_CODES: {
19
19
  readonly rateLimited: 16;
20
20
  readonly versionRevoked: 17;
21
21
  readonly serviceUnavailable: 18;
22
+ readonly allotmentExhausted: 19;
22
23
  readonly network: 70;
23
24
  };
24
25
  type CliExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
package/dist/index.js CHANGED
@@ -25,9 +25,9 @@ ProductLobster runs product analyses and keeps a product workspace's documents a
25
25
  A full analysis is asynchronous and durable. The loop is the same for every workflow:
26
26
 
27
27
  1. **Start.** Call \`run-start\` with a \`workflow\` of \`launch\`, \`audit\`, \`focused-analysis\`, \`prototype\`, or \`refinement\`, plus its inputs. It returns a run id. If your request is too thin, it returns a \`needs_sharpening\` result that lists what to add \u2014 add detail and call \`run-start\` again.
28
- 2. **Poll.** Call \`run-status\` with the run id. It reports the current status and returns a \`since\` cursor; pass that cursor on the next poll to get only what changed. Runs take minutes \u2014 poll, do not block.
28
+ 2. **Poll.** Call \`run-status\` with the run id. It reports the current status and returns a \`since\` cursor; pass that cursor on the next poll to get only what changed. Runs take minutes \u2014 poll, do not block. Stop polling when the run's \`status\` is \`completed\`, \`failed\`, or \`cancelled\`. Do not wait for every stage to leave \`pending\`: a run that fails or is cancelled part-way leaves its unstarted stages \`pending\` permanently, and on a finished run \`pending\` means the stage never ran.
29
29
  3. **Resolve checkpoints.** When \`run-status\` reports that the run is waiting at a checkpoint, it lists the options. Pick one and call \`checkpoint-resolve\` with the run id and an \`optionId\` taken from that list. An invented or stale id is rejected.
30
- 4. **Read the result.** When the run is complete, \`run-output\` returns the structured result. Use \`document-list\` and \`document-get\` to read the documents the run produced, and \`prototype-list\` / \`prototype-pack\` for any prototype.
30
+ 4. **Read the result.** When the run is complete, \`run-output\` returns the structured result as generated. Its \`stageEdits\` list marks each stage whose document the user has changed since (\`hasUserEdits: true\`). When such an entry also carries a \`documentId\`, read that document with \`document-get\` rather than relying on the structured result. An entry with \`hasUserEdits: true\` and no \`documentId\` means the stage was edited but its document is not available through \`document-get\`; use \`output\` for that stage. Use \`document-list\` and \`document-get\` to read the documents the run produced, and \`prototype-list\` / \`prototype-pack\` for any prototype.
31
31
 
32
32
  Other run controls: \`run-cancel\` stops a run; \`run-replay\` re-runs one, optionally from a stage.
33
33
 
@@ -60,6 +60,7 @@ Every failure is a structured object: \`{ code, message, requestId, retryHint }\
60
60
  | \`invalid_arguments\` | A field was the wrong shape | Fix the named fields and retry |
61
61
  | \`conflict\` | State changed under you (e.g. a stale document token) | Re-read the current state, then retry |
62
62
  | \`rate_limited\` | Daily call budget or a short spike | Wait for the reset the result reports |
63
+ | \`allotment_exhausted\` | Included outcomes have been used; the key is read-only | Continue with read tools or ask the person to upgrade |
63
64
 
64
65
  Always keep \`requestId\` \u2014 it is the one join key for support.
65
66
 
@@ -14600,6 +14601,8 @@ var RUN_START_REFINEMENT_INSTRUCTION_MAX_LENGTH = 2e3;
14600
14601
  var runStartWorkflowSchema = external_exports.enum(["audit", "launch", "focused-analysis", "prototype", "refinement"]).describe("Which Brain action to start.");
14601
14602
  var runStartInputSchema = external_exports.object({
14602
14603
  workflow: runStartWorkflowSchema,
14604
+ /** `audit`: deliberate acknowledgement when server-owned readiness has gaps. */
14605
+ proceedWithGaps: external_exports.boolean().optional().describe("Proceed with server-recorded audit evidence gaps."),
14603
14606
  /** `launch`: the product idea to take through the launch pipeline. */
14604
14607
  idea: external_exports.string().optional().describe("Product idea for the launch workflow."),
14605
14608
  /** `focused-analysis`: the question to analyze against workspace context. */
@@ -14684,6 +14687,7 @@ var DOCUMENT_TYPE_VALUES = [
14684
14687
  "customer-research",
14685
14688
  "competitive-analysis",
14686
14689
  "customer-demand-analysis",
14690
+ "kill-shot-refutation",
14687
14691
  "strategic-framing",
14688
14692
  "solution-design",
14689
14693
  "prd-generation",
@@ -14704,7 +14708,7 @@ var documentContentInputSchema = documentContentSchema.refine(
14704
14708
  "Document content must not be empty"
14705
14709
  ).describe("Document content to write.");
14706
14710
  var documentListInputSchema = external_exports.object({
14707
- cursor: external_exports.string().optional().describe("Opaque cursor from a previous page."),
14711
+ cursor: external_exports.string().max(4096).optional().describe("Opaque cursor from a previous page."),
14708
14712
  latest: external_exports.boolean().optional().describe("Return only the newest matching document."),
14709
14713
  limit: external_exports.number().int().min(1).max(100).optional().describe("Maximum number of documents to return; server default is 20."),
14710
14714
  runId: external_exports.string().optional().describe("Identifier of the run to include."),
@@ -14752,7 +14756,7 @@ var documentPutOutputSchema = external_exports.object({
14752
14756
 
14753
14757
  // ../agentic-contract/prototype-list.ts
14754
14758
  var prototypeListInputSchema = external_exports.object({
14755
- cursor: external_exports.string().optional().describe("Opaque cursor from a previous response.")
14759
+ cursor: external_exports.string().max(4096).optional().describe("Opaque cursor from a previous response.")
14756
14760
  });
14757
14761
  var prototypeStatusEnum = external_exports.enum(["generating", "complete", "failed"]).describe("Coarse prototype done-ness status.");
14758
14762
  var prototypeSummarySchema = external_exports.object({
@@ -14948,7 +14952,10 @@ var ERROR_CODES = [
14948
14952
  "version_revoked",
14949
14953
  // Appended (PRO-1484) — append-only so older clients never see a leading
14950
14954
  // member shift; no AGENTIC_CONTRACT_SCHEMA_VERSION bump (additive code).
14951
- "service_unavailable"
14955
+ "service_unavailable",
14956
+ // Appended (PRO-1920) — distinct from a periodic rate limit so clients can
14957
+ // render the durable read-only conversion state without parsing prose.
14958
+ "allotment_exhausted"
14952
14959
  ];
14953
14960
  var errorCodeSchema = external_exports.enum(ERROR_CODES);
14954
14961
  var agenticErrorSchema = external_exports.object({
@@ -15087,25 +15094,90 @@ var runReplayOutputSchema = external_exports.object({
15087
15094
  });
15088
15095
 
15089
15096
  // ../agentic-contract/run-output.ts
15097
+ var RUN_OUTPUT_MAX_OUTPUT_BYTES = 65536;
15098
+ var RUN_OUTPUT_MAX_RESPONSE_BYTES = 98304;
15099
+ var RUN_OUTPUT_ENVELOPE_RESERVE_BYTES = 1024;
15100
+ var RUN_OUTPUT_MAX_DOCUMENT_IDS = 200;
15101
+ var RUN_OUTPUT_MAX_OMITTED_KEYS = 64;
15102
+ var RUN_OUTPUT_MAX_OMITTED_KEY_BYTES = 128;
15103
+ var RUN_OUTPUT_MAX_STAGE_EDITS = 32;
15104
+ var utf8Encoder2 = new TextEncoder();
15105
+ function measureRunOutputBytes(value) {
15106
+ const json2 = JSON.stringify(value);
15107
+ return json2 === void 0 ? 0 : utf8Encoder2.encode(json2).byteLength;
15108
+ }
15090
15109
  var runOutputInputSchema = external_exports.object({
15091
15110
  runId: external_exports.string().describe("Identifier of the run to read output from."),
15092
- stage: external_exports.string().optional().describe("Stage name to scope output to; omit for run-level output.")
15111
+ stage: external_exports.string().trim().min(1).optional().describe("Stage name to scope output to; omit for run-level output.")
15093
15112
  });
15094
- var runOutputOutputSchema = external_exports.object({
15113
+ var runOutputStageEditFields = {
15114
+ stage: external_exports.string().describe("Stage name, as passed to run-output's stage."),
15115
+ hasUserEdits: external_exports.boolean().describe(
15116
+ "True when the user changed this stage's document after it was generated, so output is out of date for this stage."
15117
+ ),
15118
+ documentId: external_exports.string().optional().describe(
15119
+ "The changed document, to read with document-get. Set only when hasUserEdits is true and document-get can read it; absent on an edited stage, use output."
15120
+ )
15121
+ };
15122
+ var runOutputFieldsSchema = external_exports.object({
15095
15123
  runId: external_exports.string().describe("Identifier of the run."),
15096
15124
  stage: external_exports.string().nullable().describe("Stage this output is scoped to, or null for run-level."),
15097
- output: external_exports.record(external_exports.string(), external_exports.unknown()).describe(
15098
- "Structured output: summaries and IDs, never large inline bodies."
15125
+ output: external_exports.record(external_exports.string(), external_exports.unknown()).refine(
15126
+ (value) => measureRunOutputBytes(value) <= RUN_OUTPUT_MAX_OUTPUT_BYTES,
15127
+ "Run output must not exceed 64 KiB"
15128
+ ).describe(
15129
+ "Structured output, at most 64 KiB of JSON: summaries and IDs, never large inline bodies."
15130
+ ),
15131
+ documentIds: external_exports.array(external_exports.string()).max(RUN_OUTPUT_MAX_DOCUMENT_IDS).describe("IDs of referenced documents to fetch via document-get."),
15132
+ truncated: external_exports.boolean().optional().describe(
15133
+ "True when entries were left out of output to stay within the size limit."
15134
+ ),
15135
+ omittedKeys: external_exports.array(
15136
+ external_exports.string().refine(
15137
+ (key) => measureRunOutputBytes(key) <= RUN_OUTPUT_MAX_OMITTED_KEY_BYTES,
15138
+ "Omitted key must not exceed 128 bytes"
15139
+ )
15140
+ ).max(RUN_OUTPUT_MAX_OMITTED_KEYS).optional().describe(
15141
+ "Top-level output keys left out for size, in order and at most 64: stage names for run-level output (request each by stage), field names for one stage (read the document via document-get)."
15142
+ ),
15143
+ omittedKeyCount: external_exports.number().int().nonnegative().optional().describe("How many top-level output keys were left out for size."),
15144
+ omittedKeysTruncated: external_exports.boolean().optional().describe(
15145
+ "True when omittedKeys lists fewer keys than omittedKeyCount (too many, or a key too long to list)."
15146
+ ),
15147
+ prototypeId: external_exports.string().optional().describe(
15148
+ "Set when prototype content was left out for size: the run's prototype, to fetch with prototype-pack."
15149
+ ),
15150
+ stageEdits: external_exports.array(
15151
+ external_exports.object({
15152
+ ...runOutputStageEditFields,
15153
+ stage: runOutputStageEditFields.stage.refine(
15154
+ (stage) => measureRunOutputBytes(stage) <= RUN_OUTPUT_MAX_OMITTED_KEY_BYTES,
15155
+ "Stage name must not exceed 128 bytes"
15156
+ )
15157
+ })
15158
+ ).max(RUN_OUTPUT_MAX_STAGE_EDITS).optional().describe(
15159
+ "One entry per stage with output, at most 32: whether the user changed that stage's document, and if so the document to read with document-get."
15099
15160
  ),
15100
- documentIds: external_exports.array(external_exports.string()).describe("IDs of referenced documents to fetch via document-get."),
15101
15161
  schemaVersion: schemaVersionSchema
15102
15162
  });
15163
+ var runOutputOutputSchema = runOutputFieldsSchema.refine(
15164
+ (response) => measureRunOutputBytes(response) <= RUN_OUTPUT_MAX_RESPONSE_BYTES - RUN_OUTPUT_ENVELOPE_RESERVE_BYTES,
15165
+ "Run output response must not exceed 96 KiB with its envelope"
15166
+ );
15167
+ var runOutputParseSchema = runOutputFieldsSchema.extend({
15168
+ output: external_exports.record(external_exports.string(), external_exports.unknown()),
15169
+ documentIds: external_exports.array(external_exports.string()),
15170
+ omittedKeys: external_exports.array(external_exports.string()).optional(),
15171
+ stageEdits: external_exports.array(external_exports.object(runOutputStageEditFields)).optional()
15172
+ });
15103
15173
 
15104
15174
  // ../agentic-contract/run-status.ts
15105
15175
  var runStageProgressSchema = external_exports.object({
15106
15176
  name: external_exports.string().describe("Stage identifier."),
15107
15177
  order: external_exports.number().int().positive().describe("1-based position in the run."),
15108
- status: external_exports.enum(["pending", "running", "completed", "failed"]).describe("Coarse per-stage status.")
15178
+ status: external_exports.enum(["pending", "running", "completed", "failed"]).describe(
15179
+ "Coarse per-stage status. On a run whose status is completed, failed, or cancelled, pending means the stage never ran; stop polling on the run status, not on stage statuses."
15180
+ )
15109
15181
  });
15110
15182
  var runTransitionSchema = external_exports.object({
15111
15183
  from: runStatusEnum.nullable().describe("Prior status, or null for the first transition."),
@@ -15179,6 +15251,9 @@ var AGENTIC_TOOL_NAMES = [
15179
15251
  ];
15180
15252
 
15181
15253
  // ../agentic-contract/workspace-identity.ts
15254
+ var workspaceListInputSchema = external_exports.object({
15255
+ cursor: external_exports.string().optional().describe("Opaque cursor from a previous response (reserved for Phase 2).")
15256
+ });
15182
15257
  var workspacePipelineTypeSchema = external_exports.enum(["LAUNCH", "AUDIT"]);
15183
15258
  var workspaceIdentitySchema = external_exports.object({
15184
15259
  id: external_exports.string().describe("Stable workspace identifier."),
@@ -15293,6 +15368,7 @@ var EXIT_CODES = {
15293
15368
  rateLimited: 16,
15294
15369
  versionRevoked: 17,
15295
15370
  serviceUnavailable: 18,
15371
+ allotmentExhausted: 19,
15296
15372
  network: 70
15297
15373
  };
15298
15374
  var ERROR_CODE_EXIT_CODES = {
@@ -15307,7 +15383,8 @@ var ERROR_CODE_EXIT_CODES = {
15307
15383
  version_revoked: EXIT_CODES.versionRevoked,
15308
15384
  // PRO-1484: transient backend outage — its own non-zero code so scripts can
15309
15385
  // distinguish a retryable outage from a hard internal fault (exit 1).
15310
- service_unavailable: EXIT_CODES.serviceUnavailable
15386
+ service_unavailable: EXIT_CODES.serviceUnavailable,
15387
+ allotment_exhausted: EXIT_CODES.allotmentExhausted
15311
15388
  };
15312
15389
  function mapErrorCodeToExitCode(code) {
15313
15390
  return ERROR_CODE_EXIT_CODES[code];
@@ -15863,6 +15940,17 @@ function parseCommaSeparatedList(value) {
15863
15940
  const items = value.split(",").map((item) => item.trim()).filter(Boolean);
15864
15941
  return items.length > 0 ? items : void 0;
15865
15942
  }
15943
+ function isUnsafeTerminalCodePoint(codePoint) {
15944
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 1564 || codePoint === 8206 || codePoint === 8207 || codePoint === 8232 || codePoint === 8233 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
15945
+ }
15946
+ function escapeTerminalText(value, maxCodePoints) {
15947
+ const codePoints = Array.from(value);
15948
+ const escaped = codePoints.slice(0, maxCodePoints).map((character) => {
15949
+ const codePoint = character.codePointAt(0) ?? 0;
15950
+ return isUnsafeTerminalCodePoint(codePoint) ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
15951
+ }).join("");
15952
+ return codePoints.length > maxCodePoints ? `${escaped}\u2026` : escaped;
15953
+ }
15866
15954
  function isRecord2(value) {
15867
15955
  return typeof value === "object" && value !== null && !Array.isArray(value);
15868
15956
  }
@@ -16810,44 +16898,86 @@ function renderPrototypeList(response) {
16810
16898
  }
16811
16899
 
16812
16900
  // src/runtime/zip-extract.ts
16813
- import { mkdir, writeFile } from "fs/promises";
16901
+ import { mkdir, realpath, rm, writeFile } from "fs/promises";
16814
16902
  import path2 from "path";
16815
16903
  import { inflateRawSync } from "zlib";
16816
16904
  var END_OF_CENTRAL_DIRECTORY_SIGNATURE = 101010256;
16817
16905
  var CENTRAL_DIRECTORY_FILE_SIGNATURE = 33639248;
16906
+ var CENTRAL_DIRECTORY_DIGITAL_SIGNATURE = 84233040;
16818
16907
  var LOCAL_FILE_HEADER_SIGNATURE = 67324752;
16819
16908
  var ZIP64_SENTINEL = 65535;
16820
16909
  var ZIP64_SIZE_SENTINEL = 4294967295;
16910
+ var TRAILING_WINDOWS_ALIAS = /[ .]$/;
16911
+ var RESERVED_WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i;
16821
16912
  var MAX_ENTRY_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
16822
16913
  var MAX_TOTAL_DECOMPRESSED_BYTES = 256 * 1024 * 1024;
16823
16914
  var MAX_ENTRY_COUNT = 4096;
16915
+ var ZipOutputDirectoryError = class extends Error {
16916
+ constructor(message) {
16917
+ super(message);
16918
+ this.name = "ZipOutputDirectoryError";
16919
+ }
16920
+ };
16824
16921
  async function extractZipToDirectory(bytes, outDir) {
16825
16922
  const archive = Buffer.from(bytes);
16826
- const root = path2.resolve(outDir);
16827
- await mkdir(root, { recursive: true });
16828
- const entries = readCentralDirectory(archive);
16829
- if (entries.length > MAX_ENTRY_COUNT) {
16830
- throw new Error(
16831
- `ZIP archive exceeds the maximum of ${MAX_ENTRY_COUNT} entries.`
16832
- );
16923
+ const directory = readCentralDirectory(archive);
16924
+ const root = await createOutputRoot(outDir);
16925
+ try {
16926
+ return await extractEntries(archive, directory, root);
16927
+ } catch (error51) {
16928
+ await rm(root, { recursive: true, force: true });
16929
+ throw error51;
16930
+ }
16931
+ }
16932
+ async function createOutputRoot(outDir) {
16933
+ const requestedRoot = path2.resolve(outDir);
16934
+ let parent;
16935
+ try {
16936
+ parent = await realpath(path2.dirname(requestedRoot));
16937
+ } catch (error51) {
16938
+ if (hasFilesystemCode(error51, "ENOENT") || hasFilesystemCode(error51, "ENOTDIR")) {
16939
+ throw new ZipOutputDirectoryError(
16940
+ "The output parent directory must already exist. Create the parent, then choose a new --out directory."
16941
+ );
16942
+ }
16943
+ throw error51;
16944
+ }
16945
+ const root = path2.join(parent, path2.basename(requestedRoot));
16946
+ try {
16947
+ await mkdir(root, { mode: 448 });
16948
+ } catch (error51) {
16949
+ if (hasFilesystemCode(error51, "EEXIST")) {
16950
+ throw new ZipOutputDirectoryError(
16951
+ "The output directory must not already exist. Choose a new --out directory; existing files were not changed."
16952
+ );
16953
+ }
16954
+ if (hasFilesystemCode(error51, "ENOTDIR")) {
16955
+ throw new ZipOutputDirectoryError(
16956
+ "The output parent directory must already exist. Create the parent, then choose a new --out directory."
16957
+ );
16958
+ }
16959
+ throw error51;
16833
16960
  }
16961
+ return root;
16962
+ }
16963
+ async function extractEntries(archive, directory, root) {
16834
16964
  const files = [];
16835
16965
  let totalDecompressedBytes = 0;
16836
- for (const entry of entries) {
16966
+ for (const entry of directory.entries) {
16837
16967
  const normalizedName = normalizeZipEntryName(entry.name);
16838
- if (!normalizedName || normalizedName.endsWith("/")) {
16968
+ const targetPath = resolveSafeTarget(root, normalizedName);
16969
+ if (normalizedName.endsWith("/")) {
16839
16970
  continue;
16840
16971
  }
16841
- const targetPath = resolveSafeTarget(root, normalizedName);
16842
- const contents = readEntryContents(archive, entry);
16972
+ const contents = readEntryContents(archive, entry, directory.offset);
16843
16973
  totalDecompressedBytes += contents.length;
16844
16974
  if (totalDecompressedBytes > MAX_TOTAL_DECOMPRESSED_BYTES) {
16845
16975
  throw new Error(
16846
16976
  `ZIP archive exceeds the maximum decompressed size of ${MAX_TOTAL_DECOMPRESSED_BYTES} bytes.`
16847
16977
  );
16848
16978
  }
16849
- await mkdir(path2.dirname(targetPath), { recursive: true });
16850
- await writeFile(targetPath, contents);
16979
+ await mkdir(path2.dirname(targetPath), { recursive: true, mode: 448 });
16980
+ await writeFile(targetPath, contents, { flag: "wx", mode: 384 });
16851
16981
  files.push(normalizedName);
16852
16982
  }
16853
16983
  return {
@@ -16856,39 +16986,60 @@ async function extractZipToDirectory(bytes, outDir) {
16856
16986
  outDir: root
16857
16987
  };
16858
16988
  }
16989
+ function hasFilesystemCode(error51, code) {
16990
+ return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === code;
16991
+ }
16859
16992
  function readCentralDirectory(archive) {
16860
16993
  const eocdOffset = findEndOfCentralDirectory(archive);
16861
16994
  const entryCount = archive.readUInt16LE(eocdOffset + 10);
16862
16995
  const centralDirectorySize = archive.readUInt32LE(eocdOffset + 12);
16863
- let offset = archive.readUInt32LE(eocdOffset + 16);
16864
- if (entryCount === ZIP64_SENTINEL || centralDirectorySize === ZIP64_SIZE_SENTINEL || offset === ZIP64_SIZE_SENTINEL) {
16996
+ const directoryOffset = archive.readUInt32LE(eocdOffset + 16);
16997
+ if (entryCount === ZIP64_SENTINEL || centralDirectorySize === ZIP64_SIZE_SENTINEL || directoryOffset === ZIP64_SIZE_SENTINEL) {
16865
16998
  throw new Error("ZIP64 prototype packs are not supported.");
16866
16999
  }
16867
- const endOffset = offset + centralDirectorySize;
17000
+ if (entryCount > MAX_ENTRY_COUNT) {
17001
+ throw new Error(
17002
+ `ZIP archive exceeds the maximum of ${MAX_ENTRY_COUNT} entries.`
17003
+ );
17004
+ }
17005
+ const endOffset = directoryOffset + centralDirectorySize;
17006
+ if (endOffset > eocdOffset) {
17007
+ throw new Error("Invalid ZIP central directory.");
17008
+ }
16868
17009
  const entries = [];
16869
- while (offset < endOffset && entries.length < entryCount) {
16870
- if (archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_FILE_SIGNATURE) {
17010
+ let offset = directoryOffset;
17011
+ while (entries.length < entryCount) {
17012
+ if (offset + 46 > endOffset || archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_FILE_SIGNATURE) {
16871
17013
  throw new Error("Invalid ZIP central directory.");
16872
17014
  }
16873
17015
  const flags = archive.readUInt16LE(offset + 8);
16874
17016
  const compressionMethod = archive.readUInt16LE(offset + 10);
16875
17017
  const compressedSize = archive.readUInt32LE(offset + 20);
17018
+ const uncompressedSize = archive.readUInt32LE(offset + 24);
16876
17019
  const localHeaderOffset = archive.readUInt32LE(offset + 42);
16877
17020
  const nameLength = archive.readUInt16LE(offset + 28);
16878
17021
  const extraLength = archive.readUInt16LE(offset + 30);
16879
17022
  const commentLength = archive.readUInt16LE(offset + 32);
16880
17023
  const nameStart = offset + 46;
17024
+ const nextOffset = nameStart + nameLength + extraLength + commentLength;
17025
+ if (nextOffset > endOffset) {
17026
+ throw new Error("Invalid ZIP central directory.");
17027
+ }
16881
17028
  const name = archive.subarray(nameStart, nameStart + nameLength).toString("utf8");
16882
17029
  entries.push({
16883
17030
  compressedSize,
16884
17031
  compressionMethod,
16885
17032
  flags,
16886
17033
  localHeaderOffset,
16887
- name
17034
+ name,
17035
+ uncompressedSize
16888
17036
  });
16889
- offset = nameStart + nameLength + extraLength + commentLength;
17037
+ offset = nextOffset;
17038
+ }
17039
+ if (offset !== endOffset && (offset + 6 > endOffset || archive.readUInt32LE(offset) !== CENTRAL_DIRECTORY_DIGITAL_SIGNATURE || offset + 6 + archive.readUInt16LE(offset + 4) !== endOffset)) {
17040
+ throw new Error("Invalid ZIP central directory.");
16890
17041
  }
16891
- return entries;
17042
+ return { entries, offset: directoryOffset };
16892
17043
  }
16893
17044
  function findEndOfCentralDirectory(archive) {
16894
17045
  const minOffset = Math.max(0, archive.length - 65557);
@@ -16899,16 +17050,19 @@ function findEndOfCentralDirectory(archive) {
16899
17050
  }
16900
17051
  throw new Error("Invalid ZIP archive.");
16901
17052
  }
16902
- function readEntryContents(archive, entry) {
17053
+ function readEntryContents(archive, entry, directoryOffset) {
16903
17054
  if (entry.flags % 2 === 1) {
16904
17055
  throw new Error("Encrypted ZIP entries are not supported.");
16905
17056
  }
16906
- if (archive.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_HEADER_SIGNATURE) {
17057
+ if (entry.localHeaderOffset + 30 > directoryOffset || archive.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_HEADER_SIGNATURE) {
16907
17058
  throw new Error("Invalid ZIP local file header.");
16908
17059
  }
16909
17060
  const localNameLength = archive.readUInt16LE(entry.localHeaderOffset + 26);
16910
17061
  const localExtraLength = archive.readUInt16LE(entry.localHeaderOffset + 28);
16911
17062
  const dataStart = entry.localHeaderOffset + 30 + localNameLength + localExtraLength;
17063
+ if (dataStart + entry.compressedSize > directoryOffset) {
17064
+ throw new Error("Invalid ZIP entry data range.");
17065
+ }
16912
17066
  const compressed = archive.subarray(
16913
17067
  dataStart,
16914
17068
  dataStart + entry.compressedSize
@@ -16919,13 +17073,20 @@ function readEntryContents(archive, entry) {
16919
17073
  `ZIP entry exceeds the maximum decompressed size of ${MAX_ENTRY_DECOMPRESSED_BYTES} bytes.`
16920
17074
  );
16921
17075
  }
17076
+ if (compressed.length !== entry.uncompressedSize) {
17077
+ throw new Error("Invalid ZIP entry decompressed size.");
17078
+ }
16922
17079
  return Buffer.from(compressed);
16923
17080
  }
16924
17081
  if (entry.compressionMethod === 8) {
16925
17082
  try {
16926
- return inflateRawSync(compressed, {
17083
+ const contents = inflateRawSync(compressed, {
16927
17084
  maxOutputLength: MAX_ENTRY_DECOMPRESSED_BYTES
16928
17085
  });
17086
+ if (contents.length !== entry.uncompressedSize) {
17087
+ throw new Error("Invalid ZIP entry decompressed size.");
17088
+ }
17089
+ return contents;
16929
17090
  } catch (error51) {
16930
17091
  if (isMaxOutputLengthError(error51)) {
16931
17092
  throw new Error(
@@ -16944,15 +17105,21 @@ function isMaxOutputLengthError(error51) {
16944
17105
  }
16945
17106
  function normalizeZipEntryName(name) {
16946
17107
  const normalized = name.replaceAll("\\", "/");
16947
- if (normalized.length === 0 || normalized.startsWith("/") || normalized.includes("\0")) {
16948
- return null;
17108
+ if (normalized.length === 0 || normalized.startsWith("/") || normalized.includes(":") || normalized.includes("\0") || normalized.split("/").some(isUnsafeWindowsComponent)) {
17109
+ throw new Error(`Unsafe ZIP entry path: ${name}`);
16949
17110
  }
16950
17111
  return normalized;
16951
17112
  }
17113
+ function isUnsafeWindowsComponent(component) {
17114
+ if (component === "." || component === "..") {
17115
+ return false;
17116
+ }
17117
+ return TRAILING_WINDOWS_ALIAS.test(component) || RESERVED_WINDOWS_DEVICE.test(component);
17118
+ }
16952
17119
  function resolveSafeTarget(root, entryName) {
16953
17120
  const targetPath = path2.resolve(root, entryName);
16954
17121
  const relative = path2.relative(root, targetPath);
16955
- if (relative.startsWith("..") || path2.isAbsolute(relative)) {
17122
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
16956
17123
  throw new Error(`Unsafe ZIP entry path: ${entryName}`);
16957
17124
  }
16958
17125
  return targetPath;
@@ -16960,7 +17127,7 @@ function resolveSafeTarget(root, entryName) {
16960
17127
 
16961
17128
  // src/commands/prototype-pack.ts
16962
17129
  function registerPrototypePackCommand(program, options) {
16963
- const command = program.command("prototype-pack").description("Fetch a prototype ZIP reference.").requiredOption("--prototype-id <id>", "Prototype id.").option("--out <dir>", "Directory to extract the ZIP into.");
17130
+ const command = program.command("prototype-pack").description("Fetch a prototype ZIP reference.").requiredOption("--prototype-id <id>", "Prototype id.").option("--out <dir>", "New output directory (parent must already exist).");
16964
17131
  command.action(async (commandOptions) => {
16965
17132
  const exitCode = await runPrototypePackCommand({
16966
17133
  commandOptions,
@@ -17060,7 +17227,20 @@ async function fetchAndExtractPack(input) {
17060
17227
  });
17061
17228
  }
17062
17229
  const archive = new Uint8Array(await response.arrayBuffer());
17063
- return await extractZipToDirectory(archive, input.outDir);
17230
+ try {
17231
+ return await extractZipToDirectory(archive, input.outDir);
17232
+ } catch (error51) {
17233
+ if (error51 instanceof ZipOutputDirectoryError) {
17234
+ throw createCliOperationalError({
17235
+ code: "internal",
17236
+ message: error51.message
17237
+ });
17238
+ }
17239
+ throw createCliOperationalError({
17240
+ code: "internal",
17241
+ message: "The prototype ZIP could not be safely extracted. Check the output parent permissions and retry with a new --out directory; if the failure persists, report it."
17242
+ });
17243
+ }
17064
17244
  }
17065
17245
  function isSameOrigin(packUrl, endpoint) {
17066
17246
  try {
@@ -17230,7 +17410,9 @@ function runRunOutputCommand(options) {
17230
17410
  }
17231
17411
  return runCommand(
17232
17412
  {
17233
- outputSchema: runOutputOutputSchema,
17413
+ // The tolerant consumer schema: a newer plob still reads a server that
17414
+ // predates the run-output size bounds.
17415
+ outputSchema: runOutputParseSchema,
17234
17416
  renderHuman: renderRunOutput,
17235
17417
  toolArguments: compactRecord({
17236
17418
  runId: runId.value,
@@ -17245,16 +17427,69 @@ function runRunOutputCommand(options) {
17245
17427
  }
17246
17428
  );
17247
17429
  }
17430
+ var OMITTED_KEY_DISPLAY_MAX = 80;
17431
+ var PROTOTYPE_ID_DISPLAY_MAX = 80;
17248
17432
  function renderRunOutput(response) {
17249
17433
  const scope = response.stage ? ` stage ${response.stage}` : "";
17250
17434
  const lines = [`Run ${response.runId}${scope} output`];
17251
17435
  lines.push(`Fields: ${Object.keys(response.output).length}`);
17436
+ const omitted = renderOmittedKeys(response);
17437
+ if (omitted) {
17438
+ lines.push(`Omitted for size: ${omitted}`);
17439
+ }
17440
+ if (response.prototypeId !== void 0) {
17441
+ const prototypeId = escapeTerminalText(
17442
+ response.prototypeId,
17443
+ PROTOTYPE_ID_DISPLAY_MAX
17444
+ );
17445
+ lines.push(
17446
+ `Prototype: ${prototypeId} (fetch with \`plob prototype-pack --prototype-id ${prototypeId}\`)`
17447
+ );
17448
+ }
17449
+ const edited = renderEditedStages(response);
17450
+ if (edited) {
17451
+ lines.push(
17452
+ `Edited by the user (read with \`plob document-get\`): ${edited}`
17453
+ );
17454
+ }
17252
17455
  if (response.documentIds.length > 0) {
17253
- lines.push(`Documents: ${response.documentIds.join(", ")}`);
17456
+ lines.push(
17457
+ `Documents: ${response.documentIds.map((id) => escapeTerminalText(id, DOCUMENT_ID_DISPLAY_MAX)).join(", ")}`
17458
+ );
17254
17459
  }
17255
17460
  return `${lines.join("\n")}
17256
17461
  `;
17257
17462
  }
17463
+ var DOCUMENT_ID_DISPLAY_MAX = 80;
17464
+ function renderEditedStages(response) {
17465
+ const edited = (response.stageEdits ?? []).filter(
17466
+ (entry) => entry.hasUserEdits
17467
+ );
17468
+ if (edited.length === 0) {
17469
+ return;
17470
+ }
17471
+ return edited.map((entry) => {
17472
+ const stage = escapeTerminalText(entry.stage, OMITTED_KEY_DISPLAY_MAX);
17473
+ return entry.documentId === void 0 ? stage : `${stage} (${escapeTerminalText(entry.documentId, DOCUMENT_ID_DISPLAY_MAX)})`;
17474
+ }).join(", ");
17475
+ }
17476
+ function renderOmittedKeys(response) {
17477
+ if (response.truncated !== true) {
17478
+ return;
17479
+ }
17480
+ const listed = (response.omittedKeys ?? []).map(
17481
+ (key) => escapeTerminalText(key, OMITTED_KEY_DISPLAY_MAX)
17482
+ );
17483
+ const unlisted = Math.max(
17484
+ 0,
17485
+ (response.omittedKeyCount ?? listed.length) - listed.length
17486
+ );
17487
+ if (listed.length === 0) {
17488
+ return unlisted > 0 ? `${unlisted} ${unlisted === 1 ? "entry" : "entries"}` : void 0;
17489
+ }
17490
+ const names = listed.join(", ");
17491
+ return unlisted > 0 ? `${names} (and ${unlisted} more)` : names;
17492
+ }
17258
17493
 
17259
17494
  // src/commands/run-replay.ts
17260
17495
  function registerRunReplayCommand(program, options) {
@@ -17367,7 +17602,7 @@ function renderRunStatus(response) {
17367
17602
  // src/commands/upload.ts
17368
17603
  import { readFile as readFile2, stat } from "fs/promises";
17369
17604
  import path3 from "path";
17370
- var MAX_UPLOAD_FILE_SIZE_BYTES = 10 * 1024 * 1024;
17605
+ var MAX_UPLOAD_FILE_SIZE_BYTES = 25 * 1024 * 1024;
17371
17606
  var DOCUMENT_UPLOAD_FIELD_NAME = "files";
17372
17607
  var MIME_TYPES_BY_EXTENSION = /* @__PURE__ */ new Map([
17373
17608
  [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productlobster/plob",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "ProductLobster command-line tool — run PM analyses, manage workspaces, read run output, and upload documents from your terminal. A thin MCP client for the ProductLobster agentic surface.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ProductLobster",
@@ -58,10 +58,10 @@
58
58
  "devDependencies": {
59
59
  "@repo/agentic-contract": "workspace:*",
60
60
  "@repo/typescript-config": "workspace:*",
61
- "@types/node": "25.9.1",
62
- "@vitest/coverage-v8": "^4.1.8",
61
+ "@types/node": "26.6.3",
62
+ "@vitest/coverage-v8": "^5.0.3",
63
63
  "tsup": "^8.5.1",
64
64
  "typescript": "^6.0.3",
65
- "vitest": "^4.1.8"
65
+ "vitest": "^5.0.3"
66
66
  }
67
67
  }