@productlobster/plob 0.1.4 → 0.1.5

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/dist/cli.js CHANGED
@@ -24,10 +24,10 @@ A full analysis is asynchronous and durable. The loop is the same for every work
24
24
 
25
25
  1. **Start.** Call \`run-start\` with a \`workflow\` of \`launch\`, \`audit\`, \`focused-analysis\`, \`prototype\`, or \`refinement\`, plus its inputs. It returns a run id. For \`prototype\`, the returned id is a prototype id, not a run id: track it with \`prototype-list\` and fetch it with \`prototype-pack\`, not \`run-status\`. 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.
26
26
  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.
27
- 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.
27
+ 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. The run resumes in the background, so the answer and the next few \`run-status\` polls (often a minute or more) can still say \`checkpoint_pending\`. \`resumePending: true\` (on the answer, or on the \`checkpoint\` block of \`run-status\`) means your selection is recorded and the run is resuming: keep polling, and do not resolve again. A successful answer with \`resumePending: false\` usually means another accept of the same decision is still being recorded, so keep polling; if \`run-status\` keeps listing the options without \`resumePending: true\`, resolving again with the same \`optionId\` is safe.
28
28
  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.
29
29
 
30
- Other run controls: \`run-cancel\` stops a run that is still in progress and acknowledges every call, a finished run included, so confirm the outcome with \`run-status\`; \`run-replay\` re-runs one, optionally from a stage.
30
+ Other run controls: \`run-cancel\` stops a run that is still in progress. It answers \`cancelled: true\` for every accepted call, a finished run included, so check \`alreadyTerminal\`: \`true\` means the run had already stopped and this call changed nothing. Its \`status\` is the run's status after the call. \`run-replay\` re-runs a run, optionally from a stage.
31
31
 
32
32
  ## Reading and writing workspace context
33
33
 
@@ -53,9 +53,9 @@ Every failure is a structured object: \`{ code, message, requestId, retryHint }\
53
53
  | --- | --- | --- |
54
54
  | \`unauthorized\` | Key missing, revoked, or wrong | Ask the person to check or rotate the key |
55
55
  | \`unauthorized_scope\` | Key lacks the scope this tool needs | Ask the person to create a key with that permission |
56
- | \`workspace_mismatch\` | Argument points outside the bound workspace (or a fabricated id) | Use ids returned by this surface; do not invent them |
56
+ | \`workspace_mismatch\` | Argument points outside the bound workspace | Use ids returned by this surface; do not invent them |
57
57
  | \`needs_sharpening\` | A run request was too thin | Add the detail the result lists, then retry |
58
- | \`invalid_arguments\` | A field was the wrong shape | Fix the named fields and retry |
58
+ | \`invalid_arguments\` | A field was the wrong shape, or a checkpoint \`optionId\` was not one of the options offered | Fix the named fields and retry; for \`optionId\`, copy an id from the options \`run-status\` lists |
59
59
  | \`idempotency_key_reuse\` | The idempotency key was already used for a different write | Use a new key for a new write; reuse a key only to retry the same write |
60
60
  | \`conflict\` | State changed under you (e.g. a stale document token) | Re-read the current state, then retry |
61
61
  | \`rate_limited\` | Daily call budget or a short spike | Wait for the reset the result reports |
@@ -19868,7 +19868,12 @@ var checkpointResolveInputSchema = external_exports.object({
19868
19868
  var checkpointResolveOutputSchema = external_exports.object({
19869
19869
  runId: external_exports.string().describe("Identifier of the resolved run."),
19870
19870
  resolved: external_exports.literal(true).describe("The checkpoint was resolved; resume polling run-status."),
19871
- resumedStatus: runStatusEnum.describe("Coarse status the run resumed into."),
19871
+ resumedStatus: runStatusEnum.describe(
19872
+ "Coarse status at the time of the answer; usually checkpoint_pending while the resume is in flight."
19873
+ ),
19874
+ resumePending: external_exports.boolean().optional().describe(
19875
+ "True when the selection is recorded and the run is resuming in the background: keep polling run-status and do not resolve again. If false, keep polling; if later polls keep listing the options without resumePending true, resolving again with the same optionId is safe."
19876
+ ),
19872
19877
  schemaVersion: schemaVersionSchema
19873
19878
  });
19874
19879
 
@@ -20304,7 +20309,13 @@ var runCancelInputSchema = external_exports.object({
20304
20309
  });
20305
20310
  var runCancelOutputSchema = external_exports.object({
20306
20311
  runId: external_exports.string().describe("Identifier of the cancelled run."),
20307
- cancelled: external_exports.literal(true).describe("The cancel was accepted; confirm via run-status."),
20312
+ cancelled: external_exports.literal(true).describe(
20313
+ "The cancel was accepted, or the run is already stopped; check alreadyTerminal."
20314
+ ),
20315
+ alreadyTerminal: external_exports.boolean().optional().describe(
20316
+ "True when the run had already stopped and this call changed nothing."
20317
+ ),
20318
+ status: runStatusEnum.optional().describe("Coarse status of the run after this call."),
20308
20319
  schemaVersion: schemaVersionSchema
20309
20320
  });
20310
20321
  var runReplayInputSchema = external_exports.object({
@@ -20421,7 +20432,10 @@ var checkpointPayloadSchema = external_exports.object({
20421
20432
  options: external_exports.array(checkpointOptionSchema).describe("The strategic options to choose between."),
20422
20433
  keyDifferences: external_exports.string().describe("How the options differ from one another."),
20423
20434
  recommendationJustification: external_exports.string().describe("Why the recommended option is recommended."),
20424
- refineUrl: external_exports.string().describe("Hosted-app URL to refine this checkpoint set.")
20435
+ refineUrl: external_exports.string().describe("Hosted-app URL to refine this checkpoint set."),
20436
+ resumePending: external_exports.boolean().optional().describe(
20437
+ "True when a selection is recorded and the run is resuming in the background: keep polling and do not resolve again. False after a successful checkpoint-resolve: keep polling; if later polls keep listing the options without resumePending true, resolving again with the same optionId is safe."
20438
+ )
20425
20439
  });
20426
20440
  var runStatusInputSchema = external_exports.object({
20427
20441
  runId: external_exports.string().describe("Identifier of the run to poll."),
@@ -20509,12 +20523,14 @@ var API_KEY_SECRET_PATTERN = /^ak_[\x21-\x7E]+$/;
20509
20523
  var KeyringCredentialStore = class {
20510
20524
  async deleteBearerToken() {
20511
20525
  const keyring = await import("@napi-rs/keyring");
20512
- const removed = await deleteKeyringPassword(keyring, KEYCHAIN_SERVICE);
20513
- const legacyRemoved = await deleteKeyringPassword(
20514
- keyring,
20515
- LEGACY_KEYCHAIN_SERVICE
20516
- );
20517
- return removed || legacyRemoved;
20526
+ const removed = [];
20527
+ if (await deleteKeyringPassword(keyring, KEYCHAIN_SERVICE)) {
20528
+ removed.push(keychainCredentialLocation);
20529
+ }
20530
+ if (await deleteKeyringPassword(keyring, LEGACY_KEYCHAIN_SERVICE)) {
20531
+ removed.push(legacyKeychainCredentialLocation);
20532
+ }
20533
+ return removed;
20518
20534
  }
20519
20535
  async loadBearerToken() {
20520
20536
  const keyring = await import("@napi-rs/keyring");
@@ -20599,6 +20615,10 @@ var keychainCredentialLocation = {
20599
20615
  account: KEYCHAIN_ACCOUNT,
20600
20616
  service: KEYCHAIN_SERVICE
20601
20617
  };
20618
+ var legacyKeychainCredentialLocation = {
20619
+ account: KEYCHAIN_ACCOUNT,
20620
+ service: LEGACY_KEYCHAIN_SERVICE
20621
+ };
20602
20622
 
20603
20623
  // src/runtime/exit-codes.ts
20604
20624
  var EXIT_CODES = {
@@ -20706,11 +20726,13 @@ async function createSdkMcpClient(config2) {
20706
20726
  function isUnsafeTerminalCodePoint(codePoint) {
20707
20727
  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;
20708
20728
  }
20709
- function escapeTerminalText(value, maxCodePoints) {
20729
+ var LINE_FEED_CODE_POINT = 10;
20730
+ function escapeTerminalText(value, maxCodePoints, options = {}) {
20710
20731
  const codePoints = Array.from(value);
20711
20732
  const escaped = codePoints.slice(0, maxCodePoints).map((character) => {
20712
20733
  const codePoint = character.codePointAt(0) ?? 0;
20713
- return isUnsafeTerminalCodePoint(codePoint) ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
20734
+ const keep = options.keepLineBreaks && codePoint === LINE_FEED_CODE_POINT;
20735
+ return isUnsafeTerminalCodePoint(codePoint) && !keep ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
20714
20736
  }).join("");
20715
20737
  return codePoints.length > maxCodePoints ? `${escaped}\u2026` : escaped;
20716
20738
  }
@@ -20734,6 +20756,9 @@ var API_KEY_IN_TEXT_PATTERN = /ak_[^\s"'\\]+/g;
20734
20756
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
20735
20757
  var API_KEY_MARKER_PATTERN = /ak_/i;
20736
20758
  var DEBUG_DIAGNOSTIC_MAX_CODE_POINTS = 300;
20759
+ var FAILURE_MESSAGE_MAX_CODE_POINTS = 1e3;
20760
+ var FAILURE_RETRY_HINT_MAX_CODE_POINTS = 500;
20761
+ var FAILURE_UPGRADE_COMMAND_MAX_CODE_POINTS = 200;
20737
20762
  var defaultIo = {
20738
20763
  stdin: process.stdin,
20739
20764
  stderr: process.stderr,
@@ -20865,9 +20890,11 @@ function createCliOperationalError(input2) {
20865
20890
  input2.diagnostic
20866
20891
  );
20867
20892
  }
20868
- function renderCliError(document, options, io, diagnostic) {
20893
+ function renderCliError(document, options, io, diagnostic, { keepMessageLineBreaks = false } = {}) {
20869
20894
  const exitCode2 = exitCodeForCliError(document.code);
20870
- renderFailure({ diagnostic, document, exitCode: exitCode2 }, options, io);
20895
+ renderFailure({ diagnostic, document, exitCode: exitCode2 }, options, io, {
20896
+ keepMessageLineBreaks
20897
+ });
20871
20898
  return exitCode2;
20872
20899
  }
20873
20900
  function resolveRunnerDeps(overrides, io) {
@@ -21209,21 +21236,27 @@ function renderSuccess(response, spec, options, io) {
21209
21236
  io.stdout.write(`${JSON.stringify(response)}
21210
21237
  `);
21211
21238
  }
21212
- function renderFailure(failure2, options, io) {
21239
+ function renderFailure(failure2, options, io, { keepMessageLineBreaks = false } = {}) {
21213
21240
  if (shouldRenderJson(options, io)) {
21214
21241
  io.stdout.write(`${JSON.stringify(failure2.document)}
21215
21242
  `);
21216
21243
  } else {
21217
- io.stderr.write(`Error: ${failure2.document.message}
21218
- `);
21244
+ io.stderr.write(
21245
+ `Error: ${escapeTerminalText(failure2.document.message, FAILURE_MESSAGE_MAX_CODE_POINTS, { keepLineBreaks: keepMessageLineBreaks })}
21246
+ `
21247
+ );
21219
21248
  const upgradeCommand = getUpgradeCommand(failure2.document.details);
21220
21249
  if (upgradeCommand) {
21221
- io.stderr.write(`Upgrade: ${upgradeCommand}
21222
- `);
21250
+ io.stderr.write(
21251
+ `Upgrade: ${escapeTerminalText(upgradeCommand, FAILURE_UPGRADE_COMMAND_MAX_CODE_POINTS)}
21252
+ `
21253
+ );
21223
21254
  }
21224
21255
  if (failure2.document.retryHint) {
21225
- io.stderr.write(`${failure2.document.retryHint}
21226
- `);
21256
+ io.stderr.write(
21257
+ `${escapeTerminalText(failure2.document.retryHint, FAILURE_RETRY_HINT_MAX_CODE_POINTS)}
21258
+ `
21259
+ );
21227
21260
  }
21228
21261
  }
21229
21262
  printDebugRequestId(options, io, failure2.document.requestId);
@@ -21476,8 +21509,7 @@ function runCheckpointResolveCommand(options) {
21476
21509
  return runCommand(
21477
21510
  {
21478
21511
  outputSchema: checkpointResolveOutputSchema,
21479
- renderHuman: (response) => `Run ${response.runId} resumed: ${response.resumedStatus}
21480
- `,
21512
+ renderHuman: renderCheckpointResolve,
21481
21513
  toolArguments: { runId: runId.value, optionId: optionId.value },
21482
21514
  toolName: "checkpoint-resolve"
21483
21515
  },
@@ -21488,6 +21520,15 @@ function runCheckpointResolveCommand(options) {
21488
21520
  }
21489
21521
  );
21490
21522
  }
21523
+ function renderCheckpointResolve(response) {
21524
+ const poll = `plob run-status ${response.runId}`;
21525
+ if (response.resumePending === true) {
21526
+ return `Run ${response.runId}: selection recorded; the run is resuming in the background. Keep polling \`${poll}\` and don't resolve again.
21527
+ `;
21528
+ }
21529
+ return `Run ${response.runId}: selection accepted. Keep polling \`${poll}\`. If later polls keep listing the options without a recorded selection, resolving again with the same option id is safe.
21530
+ `;
21531
+ }
21491
21532
 
21492
21533
  // src/commands/context-pack.ts
21493
21534
  function registerContextPackCommand(program, options) {
@@ -22143,11 +22184,11 @@ async function runLogoutCommand(options) {
22143
22184
  options.io
22144
22185
  );
22145
22186
  let existingSecret;
22146
- let removed = false;
22187
+ let removedEntries = [];
22147
22188
  try {
22148
22189
  existingSecret = await deps.credentialStore.loadBearerToken();
22149
22190
  if (existingSecret) {
22150
- removed = await deps.credentialStore.deleteBearerToken();
22191
+ removedEntries = await deps.credentialStore.deleteBearerToken();
22151
22192
  }
22152
22193
  } catch {
22153
22194
  return renderCliError(
@@ -22164,7 +22205,7 @@ async function runLogoutCommand(options) {
22164
22205
  if (!existingSecret) {
22165
22206
  return renderMissingCredential(options);
22166
22207
  }
22167
- if (!removed) {
22208
+ if (removedEntries.length === 0) {
22168
22209
  return renderCliError(
22169
22210
  createCliErrorDocument({
22170
22211
  code: "internal",
@@ -22178,7 +22219,8 @@ async function runLogoutCommand(options) {
22178
22219
  {
22179
22220
  data: {
22180
22221
  keychain: keychainCredentialLocation,
22181
- removed: true
22222
+ removed: true,
22223
+ removedEntries
22182
22224
  },
22183
22225
  schemaVersion: AGENTIC_CONTRACT_SCHEMA_VERSION
22184
22226
  },
@@ -22211,7 +22253,11 @@ function renderLogoutSuccess(document, options, io) {
22211
22253
  `);
22212
22254
  return;
22213
22255
  }
22214
- io.stdout.write("ProductLobster API key removed from the OS keychain.\n");
22256
+ const services = document.data.removedEntries.map((entry) => entry.service).join(", ");
22257
+ io.stdout.write(
22258
+ `ProductLobster API key removed from the OS keychain: ${services}.
22259
+ `
22260
+ );
22215
22261
  }
22216
22262
 
22217
22263
  // src/commands/observe.ts
@@ -22604,6 +22650,19 @@ function resolveSafeTarget(root, entryName) {
22604
22650
  }
22605
22651
 
22606
22652
  // src/commands/prototype-pack.ts
22653
+ var SIGNED_URL_MAX_REDIRECT_HOPS = 5;
22654
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([
22655
+ 301,
22656
+ 302,
22657
+ 303,
22658
+ 307,
22659
+ 308
22660
+ ]);
22661
+ var PACK_NETWORK_FAILURE = {
22662
+ code: "network",
22663
+ message: "Unable to fetch the prototype ZIP. Check the network connection and retry.",
22664
+ retryHint: "Retry after the connection is available."
22665
+ };
22607
22666
  function registerPrototypePackCommand(program, options) {
22608
22667
  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).");
22609
22668
  command.action(async (commandOptions) => {
@@ -22683,22 +22742,7 @@ async function fetchAndExtractPack(input2) {
22683
22742
  message: "The prototype ZIP URL must use https. Retry once; if it continues, report the failure."
22684
22743
  });
22685
22744
  }
22686
- let response;
22687
- try {
22688
- response = await fetch(input2.pack.url, {
22689
- headers,
22690
- // With the bearer attached, never replay it to wherever a redirect
22691
- // points; the non-2xx check below reports the redirect status.
22692
- ...headers.authorization ? { redirect: "manual" } : {}
22693
- });
22694
- } catch (error62) {
22695
- throw createCliOperationalError({
22696
- code: "network",
22697
- diagnostic: describeThrownError(error62),
22698
- message: "Unable to fetch the prototype ZIP. Check the network connection and retry.",
22699
- retryHint: "Retry after the connection is available."
22700
- });
22701
- }
22745
+ const response = input2.pack.delivery === "authenticated_stream" ? await fetchPack(input2.pack.url, headers) : await fetchSignedPack(input2.pack.url);
22702
22746
  if (!response.ok) {
22703
22747
  const failure2 = await normalizeHttpResponseFailure(
22704
22748
  response,
@@ -22710,7 +22754,15 @@ async function fetchAndExtractPack(input2) {
22710
22754
  failure2.diagnostic
22711
22755
  );
22712
22756
  }
22713
- const archive = new Uint8Array(await response.arrayBuffer());
22757
+ let archive;
22758
+ try {
22759
+ archive = new Uint8Array(await response.arrayBuffer());
22760
+ } catch (error62) {
22761
+ throw createCliOperationalError({
22762
+ ...PACK_NETWORK_FAILURE,
22763
+ diagnostic: `body unreadable: ${describeThrownError(error62)}`
22764
+ });
22765
+ }
22714
22766
  try {
22715
22767
  return await extractZipToDirectory(archive, input2.outDir);
22716
22768
  } catch (error62) {
@@ -22726,6 +22778,53 @@ async function fetchAndExtractPack(input2) {
22726
22778
  });
22727
22779
  }
22728
22780
  }
22781
+ async function fetchPack(url2, headers) {
22782
+ try {
22783
+ return await fetch(url2, { headers, redirect: "manual" });
22784
+ } catch (error62) {
22785
+ throw createCliOperationalError({
22786
+ ...PACK_NETWORK_FAILURE,
22787
+ diagnostic: describeThrownError(error62)
22788
+ });
22789
+ }
22790
+ }
22791
+ async function fetchSignedPack(initialUrl) {
22792
+ let url2 = initialUrl;
22793
+ for (let hop = 0; ; hop += 1) {
22794
+ const response = await fetchPack(url2, {});
22795
+ if (!REDIRECT_STATUSES.has(response.status)) {
22796
+ return response;
22797
+ }
22798
+ response.body?.cancel().catch(() => void 0);
22799
+ if (hop === SIGNED_URL_MAX_REDIRECT_HOPS) {
22800
+ throw signedRedirectFailure("redirected too many times");
22801
+ }
22802
+ const location = response.headers.get("location");
22803
+ if (!location) {
22804
+ throw signedRedirectFailure("redirected without a target URL");
22805
+ }
22806
+ const next = resolveRedirectUrl(location, url2);
22807
+ if (!(next && isHttpsOrLoopbackUrl(next))) {
22808
+ throw signedRedirectFailure(
22809
+ "redirected to a URL that does not use https"
22810
+ );
22811
+ }
22812
+ url2 = next;
22813
+ }
22814
+ }
22815
+ function resolveRedirectUrl(location, currentUrl) {
22816
+ try {
22817
+ return new URL(location, currentUrl).href;
22818
+ } catch {
22819
+ return;
22820
+ }
22821
+ }
22822
+ function signedRedirectFailure(reason) {
22823
+ return createCliOperationalError({
22824
+ code: "internal",
22825
+ message: `The prototype ZIP download ${reason}. Retry once; if it continues, report the failure.`
22826
+ });
22827
+ }
22729
22828
  function bundleRouteFailure(status) {
22730
22829
  if (status === 401) {
22731
22830
  return {
@@ -22868,8 +22967,7 @@ function runRunCancelCommand(options) {
22868
22967
  return runCommand(
22869
22968
  {
22870
22969
  outputSchema: runCancelOutputSchema,
22871
- renderHuman: (response) => `Run ${response.runId} cancel accepted.
22872
- `,
22970
+ renderHuman: renderRunCancel,
22873
22971
  toolArguments: { runId: runId.value },
22874
22972
  toolName: "run-cancel"
22875
22973
  },
@@ -22880,6 +22978,15 @@ function runRunCancelCommand(options) {
22880
22978
  }
22881
22979
  );
22882
22980
  }
22981
+ function renderRunCancel(response) {
22982
+ if (response.alreadyTerminal === true) {
22983
+ const status = response.status ? ` (status: ${response.status})` : "";
22984
+ return `Run ${response.runId} had already stopped${status}; this call changed nothing.
22985
+ `;
22986
+ }
22987
+ return `Run ${response.runId} cancel accepted.
22988
+ `;
22989
+ }
22883
22990
 
22884
22991
  // src/commands/run-output.ts
22885
22992
  function registerRunOutputCommand(program, options) {
@@ -23069,6 +23176,9 @@ function runRunStatusCommand(options) {
23069
23176
  }
23070
23177
  );
23071
23178
  }
23179
+ var CHECKPOINT_QUESTION_DISPLAY_MAX = 500;
23180
+ var CHECKPOINT_OPTION_ID_DISPLAY_MAX = 200;
23181
+ var CHECKPOINT_OPTION_TITLE_DISPLAY_MAX = 200;
23072
23182
  function renderRunStatus(response) {
23073
23183
  const lines = [`Run ${response.runId}: ${response.status}`];
23074
23184
  if (response.stages.length > 0) {
@@ -23081,11 +23191,7 @@ function renderRunStatus(response) {
23081
23191
  lines.push(`Transitions: ${response.transitions.length}`);
23082
23192
  }
23083
23193
  if (response.checkpoint) {
23084
- lines.push(`Pending decision: ${response.checkpoint.question}`);
23085
- for (const option of response.checkpoint.options) {
23086
- const marker = option.recommended ? " recommended" : "";
23087
- lines.push(`- ${option.id}: ${option.title}${marker}`);
23088
- }
23194
+ lines.push(...renderCheckpoint(response.checkpoint));
23089
23195
  }
23090
23196
  if (response.nextCursor) {
23091
23197
  lines.push(`Next cursor: ${response.nextCursor}`);
@@ -23093,6 +23199,26 @@ function renderRunStatus(response) {
23093
23199
  return `${lines.join("\n")}
23094
23200
  `;
23095
23201
  }
23202
+ function renderCheckpoint(checkpoint) {
23203
+ const question = escapeTerminalText(
23204
+ checkpoint.question,
23205
+ CHECKPOINT_QUESTION_DISPLAY_MAX
23206
+ );
23207
+ if (checkpoint.resumePending === true) {
23208
+ return [`Decision recorded; the run is resuming. Checkpoint: ${question}`];
23209
+ }
23210
+ const lines = [`Pending decision: ${question}`];
23211
+ for (const option of checkpoint.options) {
23212
+ const id = escapeTerminalText(option.id, CHECKPOINT_OPTION_ID_DISPLAY_MAX);
23213
+ const title = escapeTerminalText(
23214
+ option.title,
23215
+ CHECKPOINT_OPTION_TITLE_DISPLAY_MAX
23216
+ );
23217
+ const marker = option.recommended ? " recommended" : "";
23218
+ lines.push(`- ${id}: ${title}${marker}`);
23219
+ }
23220
+ return lines;
23221
+ }
23096
23222
 
23097
23223
  // src/commands/upload.ts
23098
23224
  import { readFile as readFile2, stat } from "fs/promises";
@@ -23244,11 +23370,7 @@ async function runUploadCommand(options) {
23244
23370
  });
23245
23371
  } catch (error62) {
23246
23372
  return renderCliError(
23247
- createCliErrorDocument({
23248
- code: "network",
23249
- message: "Unable to reach ProductLobster. Check the network connection and retry.",
23250
- retryHint: "Retry after the connection is available."
23251
- }),
23373
+ createNetworkErrorDocument(),
23252
23374
  options.globalOptions,
23253
23375
  options.io,
23254
23376
  describeThrownError(error62)
@@ -23257,7 +23379,7 @@ async function runUploadCommand(options) {
23257
23379
  if (!response.ok) {
23258
23380
  const failure2 = await normalizeHttpResponseFailure(
23259
23381
  response,
23260
- uploadRouteFailure
23382
+ createUploadRouteFailure(prepared.extension)
23261
23383
  );
23262
23384
  return renderCliError(
23263
23385
  failure2.document,
@@ -23266,12 +23388,34 @@ async function runUploadCommand(options) {
23266
23388
  failure2.diagnostic
23267
23389
  );
23268
23390
  }
23391
+ let bodyText;
23392
+ try {
23393
+ bodyText = await response.text();
23394
+ } catch (error62) {
23395
+ return renderCliError(
23396
+ createCliErrorDocument({
23397
+ code: "network",
23398
+ message: "The connection to ProductLobster closed before the upload was confirmed. The upload may already have been stored.",
23399
+ retryHint: "Check the workspace's documents in the ProductLobster app before uploading again."
23400
+ }),
23401
+ options.globalOptions,
23402
+ options.io,
23403
+ `body unreadable: ${describeThrownError(error62)}`
23404
+ );
23405
+ }
23269
23406
  const uploadResponse = normalizeUploadSuccessPayload(
23270
- parseResponsePayload(await response.text())
23407
+ parseResponsePayload(bodyText)
23271
23408
  );
23272
23409
  renderUploadSuccess(uploadResponse, options.globalOptions, options.io);
23273
23410
  return EXIT_CODES.success;
23274
23411
  }
23412
+ function createNetworkErrorDocument() {
23413
+ return createCliErrorDocument({
23414
+ code: "network",
23415
+ message: "Unable to reach ProductLobster. Check the network connection and retry.",
23416
+ retryHint: "Retry after the connection is available."
23417
+ });
23418
+ }
23275
23419
  function resolveDocumentUploadUrl(mcpEndpoint, workspaceId) {
23276
23420
  const endpoint = new URL(mcpEndpoint);
23277
23421
  return new URL(
@@ -23301,9 +23445,8 @@ async function prepareUploadFile(filePath) {
23301
23445
  message: `File size ${fileStat.size} exceeds maximum of ${MAX_UPLOAD_FILE_SIZE_BYTES} bytes.`
23302
23446
  };
23303
23447
  }
23304
- const mimeType = MIME_TYPES_BY_EXTENSION.get(
23305
- path3.extname(normalizedPath).toLowerCase()
23306
- );
23448
+ const extension = path3.extname(normalizedPath).toLowerCase();
23449
+ const mimeType = MIME_TYPES_BY_EXTENSION.get(extension);
23307
23450
  if (!mimeType) {
23308
23451
  return {
23309
23452
  message: "Unsupported file extension. Supported extensions: .pdf, .docx, .txt, .md, .markdown, .png, .jpg, .jpeg, .webp."
@@ -23311,6 +23454,7 @@ async function prepareUploadFile(filePath) {
23311
23454
  }
23312
23455
  const bytes = await readFile2(normalizedPath);
23313
23456
  return {
23457
+ extension,
23314
23458
  file: new File([bytes], path3.basename(normalizedPath), { type: mimeType })
23315
23459
  };
23316
23460
  }
@@ -23324,18 +23468,25 @@ function parseResponsePayload(text) {
23324
23468
  return text;
23325
23469
  }
23326
23470
  }
23327
- function uploadRouteFailure(status, jsonBody) {
23328
- if (status >= 300 && status < 400) {
23471
+ function createUploadRouteFailure(extension) {
23472
+ return (status, jsonBody) => {
23473
+ if (status >= 300 && status < 400) {
23474
+ return {
23475
+ code: "internal",
23476
+ message: `ProductLobster redirected the upload (HTTP ${status}) instead of accepting it. The API key was not sent on. Retry once; if it continues, report the failure.`
23477
+ };
23478
+ }
23479
+ if (status === 415) {
23480
+ return {
23481
+ code: "invalid_arguments",
23482
+ message: `ProductLobster did not accept the ${extension} file type. Upload the document in another supported format and retry.`
23483
+ };
23484
+ }
23485
+ const byStatus = FAILURE_BY_STATUS.get(status);
23329
23486
  return {
23330
- code: "internal",
23331
- message: `ProductLobster redirected the upload (HTTP ${status}) instead of accepting it. The API key was not sent on. Retry once; if it continues, report the failure.`
23487
+ code: byStatus?.code,
23488
+ message: extractJsonErrorMessage(jsonBody) ?? byStatus?.message ?? "The upload failed. Retry once; if it continues, report the failure."
23332
23489
  };
23333
- }
23334
- const routeMessage = extractJsonErrorMessage(jsonBody);
23335
- const byStatus = FAILURE_BY_STATUS.get(status);
23336
- return {
23337
- code: byStatus?.code,
23338
- message: routeMessage ?? byStatus?.message ?? "The upload failed. Retry once; if it continues, report the failure."
23339
23490
  };
23340
23491
  }
23341
23492
  function extractJsonErrorMessage(jsonBody) {
@@ -23664,7 +23815,9 @@ function renderParseError(error62, argv, io) {
23664
23815
  message
23665
23816
  }),
23666
23817
  options,
23667
- io
23818
+ io,
23819
+ void 0,
23820
+ { keepMessageLineBreaks: true }
23668
23821
  );
23669
23822
  }
23670
23823
  function stripCommanderPrefix(message) {
package/dist/index.d.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  import { Command } from 'commander';
2
2
 
3
+ type KeychainCredentialLocation = typeof keychainCredentialLocation | typeof legacyKeychainCredentialLocation;
3
4
  interface CredentialStore {
4
- deleteBearerToken(): Promise<boolean>;
5
+ /**
6
+ * Deletes the current and the legacy entry. Resolves to the entries that
7
+ * existed and were removed, current first; empty when neither was removed.
8
+ */
9
+ deleteBearerToken(): Promise<KeychainCredentialLocation[]>;
5
10
  loadBearerToken(): Promise<string | null>;
6
11
  saveBearerToken(secret: string): Promise<void>;
7
12
  }
13
+ declare const keychainCredentialLocation: {
14
+ readonly account: "default";
15
+ readonly service: "productlobster.plob-cli";
16
+ };
17
+ declare const legacyKeychainCredentialLocation: {
18
+ readonly account: "default";
19
+ readonly service: "productlobster.pl-cli";
20
+ };
8
21
 
9
22
  declare const EXIT_CODES: {
10
23
  readonly success: 0;
@@ -108,4 +121,4 @@ declare function createProgram(config?: CliProgramConfig): {
108
121
  };
109
122
  declare function runCli(argv?: readonly string[], config?: CliProgramConfig): Promise<number>;
110
123
 
111
- export { type CliExitCode, type CliIO, type CliProgramConfig, type CommandRunnerDeps, EXIT_CODES, type McpClientConfig, type McpToolClient, type RunnerGlobalOptions, createProgram, createSdkMcpClient, runCli, runCommand };
124
+ export { type CliExitCode, type CliIO, type CliProgramConfig, type CommandRunnerDeps, type CredentialStore, EXIT_CODES, type KeychainCredentialLocation, type McpClientConfig, type McpToolClient, type RunnerGlobalOptions, createProgram, createSdkMcpClient, runCli, runCommand };
package/dist/index.js CHANGED
@@ -23,10 +23,10 @@ A full analysis is asynchronous and durable. The loop is the same for every work
23
23
 
24
24
  1. **Start.** Call \`run-start\` with a \`workflow\` of \`launch\`, \`audit\`, \`focused-analysis\`, \`prototype\`, or \`refinement\`, plus its inputs. It returns a run id. For \`prototype\`, the returned id is a prototype id, not a run id: track it with \`prototype-list\` and fetch it with \`prototype-pack\`, not \`run-status\`. 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.
25
25
  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.
26
- 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.
26
+ 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. The run resumes in the background, so the answer and the next few \`run-status\` polls (often a minute or more) can still say \`checkpoint_pending\`. \`resumePending: true\` (on the answer, or on the \`checkpoint\` block of \`run-status\`) means your selection is recorded and the run is resuming: keep polling, and do not resolve again. A successful answer with \`resumePending: false\` usually means another accept of the same decision is still being recorded, so keep polling; if \`run-status\` keeps listing the options without \`resumePending: true\`, resolving again with the same \`optionId\` is safe.
27
27
  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.
28
28
 
29
- Other run controls: \`run-cancel\` stops a run that is still in progress and acknowledges every call, a finished run included, so confirm the outcome with \`run-status\`; \`run-replay\` re-runs one, optionally from a stage.
29
+ Other run controls: \`run-cancel\` stops a run that is still in progress. It answers \`cancelled: true\` for every accepted call, a finished run included, so check \`alreadyTerminal\`: \`true\` means the run had already stopped and this call changed nothing. Its \`status\` is the run's status after the call. \`run-replay\` re-runs a run, optionally from a stage.
30
30
 
31
31
  ## Reading and writing workspace context
32
32
 
@@ -52,9 +52,9 @@ Every failure is a structured object: \`{ code, message, requestId, retryHint }\
52
52
  | --- | --- | --- |
53
53
  | \`unauthorized\` | Key missing, revoked, or wrong | Ask the person to check or rotate the key |
54
54
  | \`unauthorized_scope\` | Key lacks the scope this tool needs | Ask the person to create a key with that permission |
55
- | \`workspace_mismatch\` | Argument points outside the bound workspace (or a fabricated id) | Use ids returned by this surface; do not invent them |
55
+ | \`workspace_mismatch\` | Argument points outside the bound workspace | Use ids returned by this surface; do not invent them |
56
56
  | \`needs_sharpening\` | A run request was too thin | Add the detail the result lists, then retry |
57
- | \`invalid_arguments\` | A field was the wrong shape | Fix the named fields and retry |
57
+ | \`invalid_arguments\` | A field was the wrong shape, or a checkpoint \`optionId\` was not one of the options offered | Fix the named fields and retry; for \`optionId\`, copy an id from the options \`run-status\` lists |
58
58
  | \`idempotency_key_reuse\` | The idempotency key was already used for a different write | Use a new key for a new write; reuse a key only to retry the same write |
59
59
  | \`conflict\` | State changed under you (e.g. a stale document token) | Re-read the current state, then retry |
60
60
  | \`rate_limited\` | Daily call budget or a short spike | Wait for the reset the result reports |
@@ -19867,7 +19867,12 @@ var checkpointResolveInputSchema = external_exports.object({
19867
19867
  var checkpointResolveOutputSchema = external_exports.object({
19868
19868
  runId: external_exports.string().describe("Identifier of the resolved run."),
19869
19869
  resolved: external_exports.literal(true).describe("The checkpoint was resolved; resume polling run-status."),
19870
- resumedStatus: runStatusEnum.describe("Coarse status the run resumed into."),
19870
+ resumedStatus: runStatusEnum.describe(
19871
+ "Coarse status at the time of the answer; usually checkpoint_pending while the resume is in flight."
19872
+ ),
19873
+ resumePending: external_exports.boolean().optional().describe(
19874
+ "True when the selection is recorded and the run is resuming in the background: keep polling run-status and do not resolve again. If false, keep polling; if later polls keep listing the options without resumePending true, resolving again with the same optionId is safe."
19875
+ ),
19871
19876
  schemaVersion: schemaVersionSchema
19872
19877
  });
19873
19878
 
@@ -20303,7 +20308,13 @@ var runCancelInputSchema = external_exports.object({
20303
20308
  });
20304
20309
  var runCancelOutputSchema = external_exports.object({
20305
20310
  runId: external_exports.string().describe("Identifier of the cancelled run."),
20306
- cancelled: external_exports.literal(true).describe("The cancel was accepted; confirm via run-status."),
20311
+ cancelled: external_exports.literal(true).describe(
20312
+ "The cancel was accepted, or the run is already stopped; check alreadyTerminal."
20313
+ ),
20314
+ alreadyTerminal: external_exports.boolean().optional().describe(
20315
+ "True when the run had already stopped and this call changed nothing."
20316
+ ),
20317
+ status: runStatusEnum.optional().describe("Coarse status of the run after this call."),
20307
20318
  schemaVersion: schemaVersionSchema
20308
20319
  });
20309
20320
  var runReplayInputSchema = external_exports.object({
@@ -20420,7 +20431,10 @@ var checkpointPayloadSchema = external_exports.object({
20420
20431
  options: external_exports.array(checkpointOptionSchema).describe("The strategic options to choose between."),
20421
20432
  keyDifferences: external_exports.string().describe("How the options differ from one another."),
20422
20433
  recommendationJustification: external_exports.string().describe("Why the recommended option is recommended."),
20423
- refineUrl: external_exports.string().describe("Hosted-app URL to refine this checkpoint set.")
20434
+ refineUrl: external_exports.string().describe("Hosted-app URL to refine this checkpoint set."),
20435
+ resumePending: external_exports.boolean().optional().describe(
20436
+ "True when a selection is recorded and the run is resuming in the background: keep polling and do not resolve again. False after a successful checkpoint-resolve: keep polling; if later polls keep listing the options without resumePending true, resolving again with the same optionId is safe."
20437
+ )
20424
20438
  });
20425
20439
  var runStatusInputSchema = external_exports.object({
20426
20440
  runId: external_exports.string().describe("Identifier of the run to poll."),
@@ -20508,12 +20522,14 @@ var API_KEY_SECRET_PATTERN = /^ak_[\x21-\x7E]+$/;
20508
20522
  var KeyringCredentialStore = class {
20509
20523
  async deleteBearerToken() {
20510
20524
  const keyring = await import("@napi-rs/keyring");
20511
- const removed = await deleteKeyringPassword(keyring, KEYCHAIN_SERVICE);
20512
- const legacyRemoved = await deleteKeyringPassword(
20513
- keyring,
20514
- LEGACY_KEYCHAIN_SERVICE
20515
- );
20516
- return removed || legacyRemoved;
20525
+ const removed = [];
20526
+ if (await deleteKeyringPassword(keyring, KEYCHAIN_SERVICE)) {
20527
+ removed.push(keychainCredentialLocation);
20528
+ }
20529
+ if (await deleteKeyringPassword(keyring, LEGACY_KEYCHAIN_SERVICE)) {
20530
+ removed.push(legacyKeychainCredentialLocation);
20531
+ }
20532
+ return removed;
20517
20533
  }
20518
20534
  async loadBearerToken() {
20519
20535
  const keyring = await import("@napi-rs/keyring");
@@ -20598,6 +20614,10 @@ var keychainCredentialLocation = {
20598
20614
  account: KEYCHAIN_ACCOUNT,
20599
20615
  service: KEYCHAIN_SERVICE
20600
20616
  };
20617
+ var legacyKeychainCredentialLocation = {
20618
+ account: KEYCHAIN_ACCOUNT,
20619
+ service: LEGACY_KEYCHAIN_SERVICE
20620
+ };
20601
20621
 
20602
20622
  // src/runtime/exit-codes.ts
20603
20623
  var EXIT_CODES = {
@@ -20705,11 +20725,13 @@ async function createSdkMcpClient(config2) {
20705
20725
  function isUnsafeTerminalCodePoint(codePoint) {
20706
20726
  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;
20707
20727
  }
20708
- function escapeTerminalText(value, maxCodePoints) {
20728
+ var LINE_FEED_CODE_POINT = 10;
20729
+ function escapeTerminalText(value, maxCodePoints, options = {}) {
20709
20730
  const codePoints = Array.from(value);
20710
20731
  const escaped = codePoints.slice(0, maxCodePoints).map((character) => {
20711
20732
  const codePoint = character.codePointAt(0) ?? 0;
20712
- return isUnsafeTerminalCodePoint(codePoint) ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
20733
+ const keep = options.keepLineBreaks && codePoint === LINE_FEED_CODE_POINT;
20734
+ return isUnsafeTerminalCodePoint(codePoint) && !keep ? `\\u${codePoint.toString(16).padStart(4, "0")}` : character;
20713
20735
  }).join("");
20714
20736
  return codePoints.length > maxCodePoints ? `${escaped}\u2026` : escaped;
20715
20737
  }
@@ -20733,6 +20755,9 @@ var API_KEY_IN_TEXT_PATTERN = /ak_[^\s"'\\]+/g;
20733
20755
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
20734
20756
  var API_KEY_MARKER_PATTERN = /ak_/i;
20735
20757
  var DEBUG_DIAGNOSTIC_MAX_CODE_POINTS = 300;
20758
+ var FAILURE_MESSAGE_MAX_CODE_POINTS = 1e3;
20759
+ var FAILURE_RETRY_HINT_MAX_CODE_POINTS = 500;
20760
+ var FAILURE_UPGRADE_COMMAND_MAX_CODE_POINTS = 200;
20736
20761
  var defaultIo = {
20737
20762
  stdin: process.stdin,
20738
20763
  stderr: process.stderr,
@@ -20864,9 +20889,11 @@ function createCliOperationalError(input2) {
20864
20889
  input2.diagnostic
20865
20890
  );
20866
20891
  }
20867
- function renderCliError(document, options, io, diagnostic) {
20892
+ function renderCliError(document, options, io, diagnostic, { keepMessageLineBreaks = false } = {}) {
20868
20893
  const exitCode = exitCodeForCliError(document.code);
20869
- renderFailure({ diagnostic, document, exitCode }, options, io);
20894
+ renderFailure({ diagnostic, document, exitCode }, options, io, {
20895
+ keepMessageLineBreaks
20896
+ });
20870
20897
  return exitCode;
20871
20898
  }
20872
20899
  function resolveRunnerDeps(overrides, io) {
@@ -21208,21 +21235,27 @@ function renderSuccess(response, spec, options, io) {
21208
21235
  io.stdout.write(`${JSON.stringify(response)}
21209
21236
  `);
21210
21237
  }
21211
- function renderFailure(failure2, options, io) {
21238
+ function renderFailure(failure2, options, io, { keepMessageLineBreaks = false } = {}) {
21212
21239
  if (shouldRenderJson(options, io)) {
21213
21240
  io.stdout.write(`${JSON.stringify(failure2.document)}
21214
21241
  `);
21215
21242
  } else {
21216
- io.stderr.write(`Error: ${failure2.document.message}
21217
- `);
21243
+ io.stderr.write(
21244
+ `Error: ${escapeTerminalText(failure2.document.message, FAILURE_MESSAGE_MAX_CODE_POINTS, { keepLineBreaks: keepMessageLineBreaks })}
21245
+ `
21246
+ );
21218
21247
  const upgradeCommand = getUpgradeCommand(failure2.document.details);
21219
21248
  if (upgradeCommand) {
21220
- io.stderr.write(`Upgrade: ${upgradeCommand}
21221
- `);
21249
+ io.stderr.write(
21250
+ `Upgrade: ${escapeTerminalText(upgradeCommand, FAILURE_UPGRADE_COMMAND_MAX_CODE_POINTS)}
21251
+ `
21252
+ );
21222
21253
  }
21223
21254
  if (failure2.document.retryHint) {
21224
- io.stderr.write(`${failure2.document.retryHint}
21225
- `);
21255
+ io.stderr.write(
21256
+ `${escapeTerminalText(failure2.document.retryHint, FAILURE_RETRY_HINT_MAX_CODE_POINTS)}
21257
+ `
21258
+ );
21226
21259
  }
21227
21260
  }
21228
21261
  printDebugRequestId(options, io, failure2.document.requestId);
@@ -21475,8 +21508,7 @@ function runCheckpointResolveCommand(options) {
21475
21508
  return runCommand(
21476
21509
  {
21477
21510
  outputSchema: checkpointResolveOutputSchema,
21478
- renderHuman: (response) => `Run ${response.runId} resumed: ${response.resumedStatus}
21479
- `,
21511
+ renderHuman: renderCheckpointResolve,
21480
21512
  toolArguments: { runId: runId.value, optionId: optionId.value },
21481
21513
  toolName: "checkpoint-resolve"
21482
21514
  },
@@ -21487,6 +21519,15 @@ function runCheckpointResolveCommand(options) {
21487
21519
  }
21488
21520
  );
21489
21521
  }
21522
+ function renderCheckpointResolve(response) {
21523
+ const poll = `plob run-status ${response.runId}`;
21524
+ if (response.resumePending === true) {
21525
+ return `Run ${response.runId}: selection recorded; the run is resuming in the background. Keep polling \`${poll}\` and don't resolve again.
21526
+ `;
21527
+ }
21528
+ return `Run ${response.runId}: selection accepted. Keep polling \`${poll}\`. If later polls keep listing the options without a recorded selection, resolving again with the same option id is safe.
21529
+ `;
21530
+ }
21490
21531
 
21491
21532
  // src/commands/context-pack.ts
21492
21533
  function registerContextPackCommand(program, options) {
@@ -22142,11 +22183,11 @@ async function runLogoutCommand(options) {
22142
22183
  options.io
22143
22184
  );
22144
22185
  let existingSecret;
22145
- let removed = false;
22186
+ let removedEntries = [];
22146
22187
  try {
22147
22188
  existingSecret = await deps.credentialStore.loadBearerToken();
22148
22189
  if (existingSecret) {
22149
- removed = await deps.credentialStore.deleteBearerToken();
22190
+ removedEntries = await deps.credentialStore.deleteBearerToken();
22150
22191
  }
22151
22192
  } catch {
22152
22193
  return renderCliError(
@@ -22163,7 +22204,7 @@ async function runLogoutCommand(options) {
22163
22204
  if (!existingSecret) {
22164
22205
  return renderMissingCredential(options);
22165
22206
  }
22166
- if (!removed) {
22207
+ if (removedEntries.length === 0) {
22167
22208
  return renderCliError(
22168
22209
  createCliErrorDocument({
22169
22210
  code: "internal",
@@ -22177,7 +22218,8 @@ async function runLogoutCommand(options) {
22177
22218
  {
22178
22219
  data: {
22179
22220
  keychain: keychainCredentialLocation,
22180
- removed: true
22221
+ removed: true,
22222
+ removedEntries
22181
22223
  },
22182
22224
  schemaVersion: AGENTIC_CONTRACT_SCHEMA_VERSION
22183
22225
  },
@@ -22210,7 +22252,11 @@ function renderLogoutSuccess(document, options, io) {
22210
22252
  `);
22211
22253
  return;
22212
22254
  }
22213
- io.stdout.write("ProductLobster API key removed from the OS keychain.\n");
22255
+ const services = document.data.removedEntries.map((entry) => entry.service).join(", ");
22256
+ io.stdout.write(
22257
+ `ProductLobster API key removed from the OS keychain: ${services}.
22258
+ `
22259
+ );
22214
22260
  }
22215
22261
 
22216
22262
  // src/commands/observe.ts
@@ -22603,6 +22649,19 @@ function resolveSafeTarget(root, entryName) {
22603
22649
  }
22604
22650
 
22605
22651
  // src/commands/prototype-pack.ts
22652
+ var SIGNED_URL_MAX_REDIRECT_HOPS = 5;
22653
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([
22654
+ 301,
22655
+ 302,
22656
+ 303,
22657
+ 307,
22658
+ 308
22659
+ ]);
22660
+ var PACK_NETWORK_FAILURE = {
22661
+ code: "network",
22662
+ message: "Unable to fetch the prototype ZIP. Check the network connection and retry.",
22663
+ retryHint: "Retry after the connection is available."
22664
+ };
22606
22665
  function registerPrototypePackCommand(program, options) {
22607
22666
  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).");
22608
22667
  command.action(async (commandOptions) => {
@@ -22682,22 +22741,7 @@ async function fetchAndExtractPack(input2) {
22682
22741
  message: "The prototype ZIP URL must use https. Retry once; if it continues, report the failure."
22683
22742
  });
22684
22743
  }
22685
- let response;
22686
- try {
22687
- response = await fetch(input2.pack.url, {
22688
- headers,
22689
- // With the bearer attached, never replay it to wherever a redirect
22690
- // points; the non-2xx check below reports the redirect status.
22691
- ...headers.authorization ? { redirect: "manual" } : {}
22692
- });
22693
- } catch (error62) {
22694
- throw createCliOperationalError({
22695
- code: "network",
22696
- diagnostic: describeThrownError(error62),
22697
- message: "Unable to fetch the prototype ZIP. Check the network connection and retry.",
22698
- retryHint: "Retry after the connection is available."
22699
- });
22700
- }
22744
+ const response = input2.pack.delivery === "authenticated_stream" ? await fetchPack(input2.pack.url, headers) : await fetchSignedPack(input2.pack.url);
22701
22745
  if (!response.ok) {
22702
22746
  const failure2 = await normalizeHttpResponseFailure(
22703
22747
  response,
@@ -22709,7 +22753,15 @@ async function fetchAndExtractPack(input2) {
22709
22753
  failure2.diagnostic
22710
22754
  );
22711
22755
  }
22712
- const archive = new Uint8Array(await response.arrayBuffer());
22756
+ let archive;
22757
+ try {
22758
+ archive = new Uint8Array(await response.arrayBuffer());
22759
+ } catch (error62) {
22760
+ throw createCliOperationalError({
22761
+ ...PACK_NETWORK_FAILURE,
22762
+ diagnostic: `body unreadable: ${describeThrownError(error62)}`
22763
+ });
22764
+ }
22713
22765
  try {
22714
22766
  return await extractZipToDirectory(archive, input2.outDir);
22715
22767
  } catch (error62) {
@@ -22725,6 +22777,53 @@ async function fetchAndExtractPack(input2) {
22725
22777
  });
22726
22778
  }
22727
22779
  }
22780
+ async function fetchPack(url2, headers) {
22781
+ try {
22782
+ return await fetch(url2, { headers, redirect: "manual" });
22783
+ } catch (error62) {
22784
+ throw createCliOperationalError({
22785
+ ...PACK_NETWORK_FAILURE,
22786
+ diagnostic: describeThrownError(error62)
22787
+ });
22788
+ }
22789
+ }
22790
+ async function fetchSignedPack(initialUrl) {
22791
+ let url2 = initialUrl;
22792
+ for (let hop = 0; ; hop += 1) {
22793
+ const response = await fetchPack(url2, {});
22794
+ if (!REDIRECT_STATUSES.has(response.status)) {
22795
+ return response;
22796
+ }
22797
+ response.body?.cancel().catch(() => void 0);
22798
+ if (hop === SIGNED_URL_MAX_REDIRECT_HOPS) {
22799
+ throw signedRedirectFailure("redirected too many times");
22800
+ }
22801
+ const location = response.headers.get("location");
22802
+ if (!location) {
22803
+ throw signedRedirectFailure("redirected without a target URL");
22804
+ }
22805
+ const next = resolveRedirectUrl(location, url2);
22806
+ if (!(next && isHttpsOrLoopbackUrl(next))) {
22807
+ throw signedRedirectFailure(
22808
+ "redirected to a URL that does not use https"
22809
+ );
22810
+ }
22811
+ url2 = next;
22812
+ }
22813
+ }
22814
+ function resolveRedirectUrl(location, currentUrl) {
22815
+ try {
22816
+ return new URL(location, currentUrl).href;
22817
+ } catch {
22818
+ return;
22819
+ }
22820
+ }
22821
+ function signedRedirectFailure(reason) {
22822
+ return createCliOperationalError({
22823
+ code: "internal",
22824
+ message: `The prototype ZIP download ${reason}. Retry once; if it continues, report the failure.`
22825
+ });
22826
+ }
22728
22827
  function bundleRouteFailure(status) {
22729
22828
  if (status === 401) {
22730
22829
  return {
@@ -22867,8 +22966,7 @@ function runRunCancelCommand(options) {
22867
22966
  return runCommand(
22868
22967
  {
22869
22968
  outputSchema: runCancelOutputSchema,
22870
- renderHuman: (response) => `Run ${response.runId} cancel accepted.
22871
- `,
22969
+ renderHuman: renderRunCancel,
22872
22970
  toolArguments: { runId: runId.value },
22873
22971
  toolName: "run-cancel"
22874
22972
  },
@@ -22879,6 +22977,15 @@ function runRunCancelCommand(options) {
22879
22977
  }
22880
22978
  );
22881
22979
  }
22980
+ function renderRunCancel(response) {
22981
+ if (response.alreadyTerminal === true) {
22982
+ const status = response.status ? ` (status: ${response.status})` : "";
22983
+ return `Run ${response.runId} had already stopped${status}; this call changed nothing.
22984
+ `;
22985
+ }
22986
+ return `Run ${response.runId} cancel accepted.
22987
+ `;
22988
+ }
22882
22989
 
22883
22990
  // src/commands/run-output.ts
22884
22991
  function registerRunOutputCommand(program, options) {
@@ -23068,6 +23175,9 @@ function runRunStatusCommand(options) {
23068
23175
  }
23069
23176
  );
23070
23177
  }
23178
+ var CHECKPOINT_QUESTION_DISPLAY_MAX = 500;
23179
+ var CHECKPOINT_OPTION_ID_DISPLAY_MAX = 200;
23180
+ var CHECKPOINT_OPTION_TITLE_DISPLAY_MAX = 200;
23071
23181
  function renderRunStatus(response) {
23072
23182
  const lines = [`Run ${response.runId}: ${response.status}`];
23073
23183
  if (response.stages.length > 0) {
@@ -23080,11 +23190,7 @@ function renderRunStatus(response) {
23080
23190
  lines.push(`Transitions: ${response.transitions.length}`);
23081
23191
  }
23082
23192
  if (response.checkpoint) {
23083
- lines.push(`Pending decision: ${response.checkpoint.question}`);
23084
- for (const option of response.checkpoint.options) {
23085
- const marker = option.recommended ? " recommended" : "";
23086
- lines.push(`- ${option.id}: ${option.title}${marker}`);
23087
- }
23193
+ lines.push(...renderCheckpoint(response.checkpoint));
23088
23194
  }
23089
23195
  if (response.nextCursor) {
23090
23196
  lines.push(`Next cursor: ${response.nextCursor}`);
@@ -23092,6 +23198,26 @@ function renderRunStatus(response) {
23092
23198
  return `${lines.join("\n")}
23093
23199
  `;
23094
23200
  }
23201
+ function renderCheckpoint(checkpoint) {
23202
+ const question = escapeTerminalText(
23203
+ checkpoint.question,
23204
+ CHECKPOINT_QUESTION_DISPLAY_MAX
23205
+ );
23206
+ if (checkpoint.resumePending === true) {
23207
+ return [`Decision recorded; the run is resuming. Checkpoint: ${question}`];
23208
+ }
23209
+ const lines = [`Pending decision: ${question}`];
23210
+ for (const option of checkpoint.options) {
23211
+ const id = escapeTerminalText(option.id, CHECKPOINT_OPTION_ID_DISPLAY_MAX);
23212
+ const title = escapeTerminalText(
23213
+ option.title,
23214
+ CHECKPOINT_OPTION_TITLE_DISPLAY_MAX
23215
+ );
23216
+ const marker = option.recommended ? " recommended" : "";
23217
+ lines.push(`- ${id}: ${title}${marker}`);
23218
+ }
23219
+ return lines;
23220
+ }
23095
23221
 
23096
23222
  // src/commands/upload.ts
23097
23223
  import { readFile as readFile2, stat } from "fs/promises";
@@ -23243,11 +23369,7 @@ async function runUploadCommand(options) {
23243
23369
  });
23244
23370
  } catch (error62) {
23245
23371
  return renderCliError(
23246
- createCliErrorDocument({
23247
- code: "network",
23248
- message: "Unable to reach ProductLobster. Check the network connection and retry.",
23249
- retryHint: "Retry after the connection is available."
23250
- }),
23372
+ createNetworkErrorDocument(),
23251
23373
  options.globalOptions,
23252
23374
  options.io,
23253
23375
  describeThrownError(error62)
@@ -23256,7 +23378,7 @@ async function runUploadCommand(options) {
23256
23378
  if (!response.ok) {
23257
23379
  const failure2 = await normalizeHttpResponseFailure(
23258
23380
  response,
23259
- uploadRouteFailure
23381
+ createUploadRouteFailure(prepared.extension)
23260
23382
  );
23261
23383
  return renderCliError(
23262
23384
  failure2.document,
@@ -23265,12 +23387,34 @@ async function runUploadCommand(options) {
23265
23387
  failure2.diagnostic
23266
23388
  );
23267
23389
  }
23390
+ let bodyText;
23391
+ try {
23392
+ bodyText = await response.text();
23393
+ } catch (error62) {
23394
+ return renderCliError(
23395
+ createCliErrorDocument({
23396
+ code: "network",
23397
+ message: "The connection to ProductLobster closed before the upload was confirmed. The upload may already have been stored.",
23398
+ retryHint: "Check the workspace's documents in the ProductLobster app before uploading again."
23399
+ }),
23400
+ options.globalOptions,
23401
+ options.io,
23402
+ `body unreadable: ${describeThrownError(error62)}`
23403
+ );
23404
+ }
23268
23405
  const uploadResponse = normalizeUploadSuccessPayload(
23269
- parseResponsePayload(await response.text())
23406
+ parseResponsePayload(bodyText)
23270
23407
  );
23271
23408
  renderUploadSuccess(uploadResponse, options.globalOptions, options.io);
23272
23409
  return EXIT_CODES.success;
23273
23410
  }
23411
+ function createNetworkErrorDocument() {
23412
+ return createCliErrorDocument({
23413
+ code: "network",
23414
+ message: "Unable to reach ProductLobster. Check the network connection and retry.",
23415
+ retryHint: "Retry after the connection is available."
23416
+ });
23417
+ }
23274
23418
  function resolveDocumentUploadUrl(mcpEndpoint, workspaceId) {
23275
23419
  const endpoint = new URL(mcpEndpoint);
23276
23420
  return new URL(
@@ -23300,9 +23444,8 @@ async function prepareUploadFile(filePath) {
23300
23444
  message: `File size ${fileStat.size} exceeds maximum of ${MAX_UPLOAD_FILE_SIZE_BYTES} bytes.`
23301
23445
  };
23302
23446
  }
23303
- const mimeType = MIME_TYPES_BY_EXTENSION.get(
23304
- path3.extname(normalizedPath).toLowerCase()
23305
- );
23447
+ const extension = path3.extname(normalizedPath).toLowerCase();
23448
+ const mimeType = MIME_TYPES_BY_EXTENSION.get(extension);
23306
23449
  if (!mimeType) {
23307
23450
  return {
23308
23451
  message: "Unsupported file extension. Supported extensions: .pdf, .docx, .txt, .md, .markdown, .png, .jpg, .jpeg, .webp."
@@ -23310,6 +23453,7 @@ async function prepareUploadFile(filePath) {
23310
23453
  }
23311
23454
  const bytes = await readFile2(normalizedPath);
23312
23455
  return {
23456
+ extension,
23313
23457
  file: new File([bytes], path3.basename(normalizedPath), { type: mimeType })
23314
23458
  };
23315
23459
  }
@@ -23323,18 +23467,25 @@ function parseResponsePayload(text) {
23323
23467
  return text;
23324
23468
  }
23325
23469
  }
23326
- function uploadRouteFailure(status, jsonBody) {
23327
- if (status >= 300 && status < 400) {
23470
+ function createUploadRouteFailure(extension) {
23471
+ return (status, jsonBody) => {
23472
+ if (status >= 300 && status < 400) {
23473
+ return {
23474
+ code: "internal",
23475
+ message: `ProductLobster redirected the upload (HTTP ${status}) instead of accepting it. The API key was not sent on. Retry once; if it continues, report the failure.`
23476
+ };
23477
+ }
23478
+ if (status === 415) {
23479
+ return {
23480
+ code: "invalid_arguments",
23481
+ message: `ProductLobster did not accept the ${extension} file type. Upload the document in another supported format and retry.`
23482
+ };
23483
+ }
23484
+ const byStatus = FAILURE_BY_STATUS.get(status);
23328
23485
  return {
23329
- code: "internal",
23330
- message: `ProductLobster redirected the upload (HTTP ${status}) instead of accepting it. The API key was not sent on. Retry once; if it continues, report the failure.`
23486
+ code: byStatus?.code,
23487
+ message: extractJsonErrorMessage(jsonBody) ?? byStatus?.message ?? "The upload failed. Retry once; if it continues, report the failure."
23331
23488
  };
23332
- }
23333
- const routeMessage = extractJsonErrorMessage(jsonBody);
23334
- const byStatus = FAILURE_BY_STATUS.get(status);
23335
- return {
23336
- code: byStatus?.code,
23337
- message: routeMessage ?? byStatus?.message ?? "The upload failed. Retry once; if it continues, report the failure."
23338
23489
  };
23339
23490
  }
23340
23491
  function extractJsonErrorMessage(jsonBody) {
@@ -23663,7 +23814,9 @@ function renderParseError(error62, argv, io) {
23663
23814
  message
23664
23815
  }),
23665
23816
  options,
23666
- io
23817
+ io,
23818
+ void 0,
23819
+ { keepMessageLineBreaks: true }
23667
23820
  );
23668
23821
  }
23669
23822
  function stripCommanderPrefix(message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productlobster/plob",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",