@scalequality/cli 0.4.20 → 0.4.22

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "d57c1ffcfba90f5dd699bb0315312c41c06888f6964098da3dd0a76625f2db88",
4
- "sourceCommit": "4984400255216ed0585b1c0f034d3289d4851f93"
3
+ "sha256": "46fe6b09a8c69a92a0f54cc420a3596c6f13b82177d3aec1d46456ae508a4517",
4
+ "sourceCommit": "2e2e401c42e98d17e1c0dd8286bdc6d15e44f77b"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -563,10 +563,15 @@ async function writeScrubbedTranscript(source, target) {
563
563
 
564
564
  // src/application/services/workspaceSandbox/backgroundService.ts
565
565
  var import_child_process2 = require("child_process");
566
- var import_crypto3 = require("crypto");
566
+ var import_crypto4 = require("crypto");
567
567
  var import_fs5 = require("fs");
568
568
  var import_path6 = require("path");
569
569
 
570
+ // src/application/services/workspaceSandbox/HttpSessionTransport.ts
571
+ var import_crypto2 = require("crypto");
572
+ var import_util2 = require("util");
573
+ var import_zlib = require("zlib");
574
+
570
575
  // src/application/services/workspaceSandbox/reasoning.ts
571
576
  var REASONING_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"];
572
577
  function isReasoningLevel(value) {
@@ -729,6 +734,15 @@ function serializeCheckpoints(map) {
729
734
  }
730
735
  return Object.keys(repos).length ? JSON.stringify({ version: CHECKPOINT_MAP_VERSION, repos }) : "";
731
736
  }
737
+ var CHECKPOINT_INLINE_MAX_BYTES = 6 * 1024 * 1024;
738
+ var CHECKPOINT_PART_BYTES = 4 * 1024 * 1024;
739
+ var CHECKPOINT_MAX_PARTS = 32;
740
+ function checkpointPartsOf(raw) {
741
+ const p = raw;
742
+ if (!p || typeof p !== "object" || typeof p.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(p.sha256)) return null;
743
+ if (!Number.isInteger(p.count) || p.count < 1 || p.count > CHECKPOINT_MAX_PARTS || !Number.isInteger(p.bytes) || p.bytes < 1) return null;
744
+ return { sha256: p.sha256, count: p.count, bytes: p.bytes };
745
+ }
732
746
 
733
747
  // src/application/services/workspaceSandbox/workspaceGit.ts
734
748
  var run = (0, import_util.promisify)(import_child_process.execFile);
@@ -856,11 +870,36 @@ async function computeDiff(root, base, caps = DIFF_CAPS, scope) {
856
870
  async function worktreeTreeId(root, scope) {
857
871
  return withWorktreeIndex(root, async (env) => (await git(["write-tree"], { cwd: root, env })).trim(), scope);
858
872
  }
859
- async function checkpointPatch(root, base, scope) {
873
+ var CHECKPOINT_KEPT_HEADER = "ScaleQuality-Kept:";
874
+ var CHECKPOINT_KEPT_TEXT = { bytes: 1024 * 1024, lines: 2e4 };
875
+ async function checkpointPatch(root, base, scope, opts = {}) {
860
876
  return withWorktreeIndex(root, async (env) => {
861
- const patch = await git(["diff", "--cached", "--no-renames", "--binary", base, ...scopeSpec(scope)], { cwd: root, env, maxBuffer: 512 * 1024 * 1024 });
862
- return patch ? `${CHECKPOINT_BASE_HEADER} ${base}
863
- ${patch}` : "";
877
+ const head = `${CHECKPOINT_BASE_HEADER} ${base}
878
+ `;
879
+ const whole = async () => {
880
+ const patch2 = await git(["diff", "--cached", "--no-renames", "--binary", base, ...scopeSpec(scope)], { cwd: root, env, maxBuffer: 512 * 1024 * 1024 });
881
+ return patch2 ? head + patch2 : "";
882
+ };
883
+ if (!opts.keepInFolder) return whole();
884
+ const changes = await listChangesWithEnv(root, base, env, scope);
885
+ const kept = [];
886
+ const carried = [];
887
+ for (const c of changes) {
888
+ const size = c.binary || c.status === "deleted" ? 0 : (await (0, import_promises2.stat)((0, import_path2.join)(root, c.path)).catch(() => null))?.size ?? 0;
889
+ const large = size > CHECKPOINT_KEPT_TEXT.bytes || c.additions + c.deletions > CHECKPOINT_KEPT_TEXT.lines;
890
+ if (c.binary || large) kept.push(c);
891
+ else carried.push(c.path);
892
+ }
893
+ if (!kept.length) return whole();
894
+ let patch = "";
895
+ for (let i = 0; i < carried.length; i += 200) {
896
+ patch += await git(
897
+ ["diff", "--cached", "--no-renames", "--binary", base, "--", ...carried.slice(i, i + 200)],
898
+ { cwd: root, env: { ...env, GIT_LITERAL_PATHSPECS: "1" }, maxBuffer: 512 * 1024 * 1024 }
899
+ );
900
+ }
901
+ return head + kept.map((c) => `${CHECKPOINT_KEPT_HEADER} ${c.status} ${JSON.stringify(c.path)}
902
+ `).join("") + patch;
864
903
  }, scope);
865
904
  }
866
905
  var PR_FILE_MAX_BYTES = 2 * 1024 * 1024;
@@ -1931,6 +1970,8 @@ var TransportError = class extends Error {
1931
1970
  };
1932
1971
 
1933
1972
  // src/application/services/workspaceSandbox/HttpSessionTransport.ts
1973
+ var gzipAsync = (0, import_util2.promisify)(import_zlib.gzip);
1974
+ var gunzipAsync = (0, import_util2.promisify)(import_zlib.gunzip);
1934
1975
  var GONE = [401, 403, 404, 410];
1935
1976
  var HttpSessionTransport = class {
1936
1977
  constructor(opts) {
@@ -1946,6 +1987,8 @@ var HttpSessionTransport = class {
1946
1987
  gone;
1947
1988
  async bootstrap() {
1948
1989
  const raw = await this.get(`/bootstrap?features=${ENGINE_FEATURES.join(",")}`, this.opts.requestTimeoutMs ?? 3e4);
1990
+ const parts = checkpointPartsOf(raw?.checkpointParts);
1991
+ if (parts && typeof raw?.checkpointPatch !== "string") raw.checkpointPatch = await this.checkpointFromParts(parts);
1949
1992
  return toSessionBootstrap(raw);
1950
1993
  }
1951
1994
  async pollCommands(waitSeconds, signal) {
@@ -1972,7 +2015,31 @@ var HttpSessionTransport = class {
1972
2015
  return this.post("/pull-request", req, this.opts.toolTimeoutMs ?? 18e4);
1973
2016
  }
1974
2017
  async checkpoint(req) {
1975
- await this.post("/checkpoint", req, this.opts.requestTimeoutMs ?? 6e4);
2018
+ const timeout = this.opts.requestTimeoutMs ?? 6e4;
2019
+ if (Buffer.byteLength(JSON.stringify(req), "utf8") <= CHECKPOINT_INLINE_MAX_BYTES) {
2020
+ await this.post("/checkpoint", req, timeout);
2021
+ return;
2022
+ }
2023
+ const packed = await gzipAsync(Buffer.from(req.patch, "utf8"));
2024
+ const sha256 = (0, import_crypto2.createHash)("sha256").update(req.patch, "utf8").digest("hex");
2025
+ const count = Math.ceil(packed.length / CHECKPOINT_PART_BYTES);
2026
+ if (count > CHECKPOINT_MAX_PARTS) throw new TransportError(`checkpoint of ${packed.length} compressed bytes is over the limit`, 413, false);
2027
+ for (let index = 0; index < count; index++) {
2028
+ const data = packed.subarray(index * CHECKPOINT_PART_BYTES, (index + 1) * CHECKPOINT_PART_BYTES).toString("base64");
2029
+ await this.post("/checkpoint/parts", { sha256, index, count, data }, timeout);
2030
+ }
2031
+ await this.post("/checkpoint", { parts: { sha256, count, bytes: packed.length }, sdkSessionId: req.sdkSessionId }, timeout);
2032
+ }
2033
+ async checkpointFromParts(parts) {
2034
+ const chunks = [];
2035
+ for (let index = 0; index < parts.count; index++) {
2036
+ const r = await this.get(`/checkpoint/parts/${index}`, this.opts.requestTimeoutMs ?? 6e4);
2037
+ if (r?.sha256 !== parts.sha256 || typeof r.data !== "string") throw new TransportError("checkpoint part does not match", 502, false);
2038
+ chunks.push(Buffer.from(r.data, "base64"));
2039
+ }
2040
+ const patch = (await gunzipAsync(Buffer.concat(chunks)).catch(() => Buffer.alloc(0))).toString("utf8");
2041
+ if ((0, import_crypto2.createHash)("sha256").update(patch, "utf8").digest("hex") !== parts.sha256) throw new TransportError("checkpoint parts do not match", 502, false);
2042
+ return patch;
1976
2043
  }
1977
2044
  async openRepository(repoFullName) {
1978
2045
  const raw = await this.post("/repositories/open", { repoFullName }, this.opts.requestTimeoutMs ?? 3e4);
@@ -2277,7 +2344,7 @@ var import_os3 = require("os");
2277
2344
  var import_path5 = require("path");
2278
2345
 
2279
2346
  // src/application/services/workspaceSandbox/localTracking.ts
2280
- var import_crypto2 = require("crypto");
2347
+ var import_crypto3 = require("crypto");
2281
2348
  var import_fs4 = require("fs");
2282
2349
  var import_promises5 = require("fs/promises");
2283
2350
  var import_os2 = require("os");
@@ -2305,7 +2372,7 @@ function gitTracker(root, base, prefix) {
2305
2372
  },
2306
2373
  snapshot: () => worktreeTreeId(root, scope),
2307
2374
  restore: (tree) => restoreTree(root, tree, scope),
2308
- checkpoint: () => checkpointPatch(root, base, scope),
2375
+ checkpoint: (opts) => checkpointPatch(root, base, scope, opts),
2309
2376
  prFiles: (published) => filesForPullRequest(root, base, published, scope),
2310
2377
  treeId: () => worktreeTreeId(root, scope)
2311
2378
  };
@@ -2389,7 +2456,7 @@ async function mapLimit(items, n, fn) {
2389
2456
  async function repoFingerprint(root) {
2390
2457
  const head = (await git(["rev-parse", "-q", "--verify", "HEAD"], { cwd: root }).catch(() => "")).trim();
2391
2458
  const status = await git(["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all"], { cwd: root, timeoutMs: 6e4 }).catch(() => "?");
2392
- const h = (0, import_crypto2.createHash)("sha256").update(head).update("\0").update(status);
2459
+ const h = (0, import_crypto3.createHash)("sha256").update(head).update("\0").update(status);
2393
2460
  const recs = status.split("\0");
2394
2461
  let n = 0;
2395
2462
  for (let i = 0; i < recs.length && n < 2e3; i++) {
@@ -2561,10 +2628,10 @@ var LooseFileTracker = class {
2561
2628
  }
2562
2629
  await restoreTree(this.o.root, target, this.scope());
2563
2630
  }
2564
- async checkpoint() {
2631
+ async checkpoint(opts) {
2565
2632
  await this.chain;
2566
2633
  if (!this.entries.length) return "";
2567
- return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
2634
+ return checkpointPatch(this.o.root, await this.baseTree(), this.scope(), opts);
2568
2635
  }
2569
2636
  async prFiles(published) {
2570
2637
  await this.chain;
@@ -3530,7 +3597,7 @@ function serviceSlug(api) {
3530
3597
  } catch {
3531
3598
  }
3532
3599
  const slug = host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "custom";
3533
- return `${slug}-${(0, import_crypto3.createHash)("sha256").update(api).digest("hex").slice(0, 6)}`;
3600
+ return `${slug}-${(0, import_crypto4.createHash)("sha256").update(api).digest("hex").slice(0, 6)}`;
3534
3601
  }
3535
3602
  function serviceNames(api) {
3536
3603
  const slug = serviceSlug(api);
@@ -3669,7 +3736,7 @@ function installStableCopy(o) {
3669
3736
  const bundle = (0, import_path6.join)(o.packageRoot, "dist", "connect.cjs");
3670
3737
  const bin = (dir2) => (0, import_path6.join)(dir2, "node_modules", "@scalequality", "cli", "bin", "scalequality.mjs");
3671
3738
  if (!(0, import_fs5.existsSync)(bundle)) throw new ServiceError("NOT_AN_INSTALLED_CLI", "This installation of @scalequality/cli is incomplete (dist/connect.cjs is missing).");
3672
- const sha = (0, import_crypto3.createHash)("sha256").update((0, import_fs5.readFileSync)(bundle)).digest("hex");
3739
+ const sha = (0, import_crypto4.createHash)("sha256").update((0, import_fs5.readFileSync)(bundle)).digest("hex");
3673
3740
  const base = (0, import_path6.join)(sqHome(o.home), "cli");
3674
3741
  const dir = (0, import_path6.join)(base, pkg.version);
3675
3742
  if (inside2(dir, o.packageRoot)) return { dir, bin: bin(dir), version: pkg.version, reused: true };
@@ -4817,7 +4884,7 @@ async function addFolder(credentials2, api, path, home) {
4817
4884
  var import_child_process3 = require("child_process");
4818
4885
  var import_promises14 = require("fs/promises");
4819
4886
  var import_fs9 = require("fs");
4820
- var import_crypto7 = require("crypto");
4887
+ var import_crypto8 = require("crypto");
4821
4888
  var import_path15 = require("path");
4822
4889
 
4823
4890
  // src/application/services/execution/LanguageAdapter.ts
@@ -4893,14 +4960,16 @@ function sandboxTestEnv() {
4893
4960
  var ApprovalBroker = class {
4894
4961
  waiting = /* @__PURE__ */ new Map();
4895
4962
  early = /* @__PURE__ */ new Map();
4963
+ /** True when a call was waiting for this decision (it returns now); false when it is kept for a later wait. */
4896
4964
  resolve(approvalId, decision) {
4897
4965
  const w = this.waiting.get(approvalId);
4898
4966
  if (w) {
4899
4967
  this.waiting.delete(approvalId);
4900
4968
  w(decision);
4901
- } else {
4902
- this.early.set(approvalId, decision);
4969
+ return true;
4903
4970
  }
4971
+ this.early.set(approvalId, decision);
4972
+ return false;
4904
4973
  }
4905
4974
  wait(approvalId, signal) {
4906
4975
  const ready = this.early.get(approvalId);
@@ -5259,7 +5328,7 @@ function limitInEngineText(text3) {
5259
5328
  }
5260
5329
 
5261
5330
  // src/application/services/workspaceSandbox/scalequalityTools.ts
5262
- var import_crypto4 = require("crypto");
5331
+ var import_crypto5 = require("crypto");
5263
5332
 
5264
5333
  // node_modules/zod/v3/external.js
5265
5334
  var external_exports = {};
@@ -9379,7 +9448,7 @@ function buildScaleQualityServer(sdk, host) {
9379
9448
  const tools = REMOTE_TOOLS.map(
9380
9449
  (spec) => sdk.tool(spec.name, spec.description, spec.shape, async (args) => {
9381
9450
  const a = { ...args ?? {} };
9382
- if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0, import_crypto4.randomUUID)();
9451
+ if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0, import_crypto5.randomUUID)();
9383
9452
  return callScaleQualityTool(host, spec.name, a);
9384
9453
  })
9385
9454
  );
@@ -9954,7 +10023,7 @@ function blockText(content) {
9954
10023
  }
9955
10024
 
9956
10025
  // src/application/services/workspaceSandbox/inputChannel.ts
9957
- var import_crypto5 = require("crypto");
10026
+ var import_crypto6 = require("crypto");
9958
10027
  var InputChannel = class {
9959
10028
  buffer = [];
9960
10029
  waiting = null;
@@ -10012,7 +10081,7 @@ var InputChannel = class {
10012
10081
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10013
10082
  function messageUuid(id) {
10014
10083
  if (UUID.test(id)) return id.toLowerCase();
10015
- const h = (0, import_crypto5.createHash)("sha256").update(`sq-message:${id}`).digest("hex");
10084
+ const h = (0, import_crypto6.createHash)("sha256").update(`sq-message:${id}`).digest("hex");
10016
10085
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-${(parseInt(h[16], 16) & 3 | 8).toString(16)}${h.slice(17, 20)}-${h.slice(20, 32)}`;
10017
10086
  }
10018
10087
  function userMessage(text3, images, uuid2, priority) {
@@ -10257,7 +10326,8 @@ function buildSystemAppend(c) {
10257
10326
  "- Lead with the outcome in one or two sentences: what changed, what was found, or what you need from the user.",
10258
10327
  "- The screen already shows every command you ran and every file you edited, with their output and diff. Do not list them again, and do not restate test counts or tool results the user can open; mention one only when it matters for what comes next (a test that still fails, a risk that remains).",
10259
10328
  "- Keep it short, like a colleague reporting in a chat: a few lines for most turns, no headings, a short list only when there are several separate items. Never repeat the same fact in two places.",
10260
- "- End with at most one question or one suggested next step, never a menu of options. When there is an obvious next step, say it in one line."
10329
+ "- End with at most one question or one suggested next step, never a menu of options. When there is an obvious next step, say it in one line.",
10330
+ "- To show the user an image (a screenshot, a design, a reference board, a chart), write it as a Markdown image with its path in the workspace, for example `![Login screen](docs/login.png)`: the screen displays it in the conversation, and the user can open it larger. An https image address works the same way. Never say you cannot show an image; read it yourself first when you need to know what it shows."
10261
10331
  ].join("\n");
10262
10332
  }
10263
10333
 
@@ -10343,11 +10413,11 @@ async function detectTestCommandFromManifests(dir) {
10343
10413
  }
10344
10414
 
10345
10415
  // src/application/services/workspaceSandbox/transcript.ts
10346
- var import_crypto6 = require("crypto");
10416
+ var import_crypto7 = require("crypto");
10347
10417
  var import_fs8 = require("fs");
10348
10418
  var import_promises11 = require("fs/promises");
10349
10419
  var import_path12 = require("path");
10350
- var import_zlib = require("zlib");
10420
+ var import_zlib2 = require("zlib");
10351
10421
  var TRANSCRIPT_CAPS = { maxRawBytes: 64 * 1024 * 1024, maxCompressedBytes: 4 * 1024 * 1024 };
10352
10422
  function engineProjectDir(cwd) {
10353
10423
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -10386,10 +10456,10 @@ async function packTranscript(configDir, sessionId, redactor, caps = TRANSCRIPT_
10386
10456
  }
10387
10457
  const text3 = lines2.length ? `${lines2.join("\n")}
10388
10458
  ` : "";
10389
- const zipped = (0, import_zlib.gzipSync)(Buffer.from(text3, "utf8"), { level: 9 });
10459
+ const zipped = (0, import_zlib2.gzipSync)(Buffer.from(text3, "utf8"), { level: 9 });
10390
10460
  if (zipped.length > caps.maxCompressedBytes) return { tooLarge: true, bytes: zipped.length };
10391
10461
  return {
10392
- payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0, import_crypto6.createHash)("sha256").update(text3).digest("hex") },
10462
+ payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") },
10393
10463
  secretsRemoved: removed
10394
10464
  };
10395
10465
  }
@@ -10398,11 +10468,11 @@ async function restoreTranscript(configDir, cwd, t) {
10398
10468
  if (await findTranscript(configDir, t.sdkSessionId)) return true;
10399
10469
  let text3;
10400
10470
  try {
10401
- text3 = (0, import_zlib.gunzipSync)(Buffer.from(t.data, "base64"), { maxOutputLength: TRANSCRIPT_CAPS.maxRawBytes });
10471
+ text3 = (0, import_zlib2.gunzipSync)(Buffer.from(t.data, "base64"), { maxOutputLength: TRANSCRIPT_CAPS.maxRawBytes });
10402
10472
  } catch {
10403
10473
  return false;
10404
10474
  }
10405
- if (t.sha256 && (0, import_crypto6.createHash)("sha256").update(text3).digest("hex") !== t.sha256) return false;
10475
+ if (t.sha256 && (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") !== t.sha256) return false;
10406
10476
  const dir = (0, import_path12.join)(configDir, "projects", engineProjectDir(cwd));
10407
10477
  await (0, import_promises11.mkdir)(dir, { recursive: true, mode: 448 });
10408
10478
  const target = (0, import_path12.join)(dir, `${t.sdkSessionId}.jsonl`);
@@ -10421,7 +10491,7 @@ async function restoreTranscript(configDir, cwd, t) {
10421
10491
  // src/application/services/workspaceSandbox/workspaceFiles.ts
10422
10492
  var import_promises12 = require("fs/promises");
10423
10493
  var import_path13 = require("path");
10424
- var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3 };
10494
+ var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3, maxImageBytes: 6 * 1024 * 1024 };
10425
10495
  var FileRequestError = class extends Error {
10426
10496
  constructor(code) {
10427
10497
  super(code);
@@ -10502,6 +10572,48 @@ async function readTextFile(root, rel, range = {}, denied = []) {
10502
10572
  }
10503
10573
  return { path: clean, from, to: Math.max(from, Math.min(last, total)), totalLines: total, content, truncated, size: st.size };
10504
10574
  }
10575
+ function imageMime(buf) {
10576
+ const at = (offset, hex) => buf.length >= offset + hex.length / 2 && buf.subarray(offset, offset + hex.length / 2).toString("hex") === hex;
10577
+ if (at(0, "89504e470d0a1a0a")) return "image/png";
10578
+ if (at(0, "ffd8ff")) return "image/jpeg";
10579
+ if (at(0, "474946383761") || at(0, "474946383961")) return "image/gif";
10580
+ if (at(0, "52494646") && at(8, "57454250")) return "image/webp";
10581
+ if (at(4, "66747970") && /^(avif|avis)$/.test(buf.subarray(8, 12).toString("latin1"))) return "image/avif";
10582
+ if (at(0, "424d")) return "image/bmp";
10583
+ const head = buf.subarray(0, 2048).toString("utf8").replace(/^\uFEFF/, "").trimStart();
10584
+ if (/^(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*(<!DOCTYPE svg[^>]*>\s*)?<svg[\s>]/i.test(head)) return "image/svg+xml";
10585
+ return null;
10586
+ }
10587
+ async function insideRelative(path, bases) {
10588
+ const clean = (path ?? "").trim().replace(/^file:\/\//i, "");
10589
+ if (!(0, import_path13.isAbsolute)(clean)) return clean.replace(/^\.\//, "");
10590
+ for (const base of bases) {
10591
+ for (const root of [(0, import_path13.resolve)(base), await (0, import_promises12.realpath)(base).catch(() => (0, import_path13.resolve)(base))]) {
10592
+ const rel = (0, import_path13.relative)(root, (0, import_path13.resolve)(clean));
10593
+ if (!rel) return "";
10594
+ if (rel !== ".." && !rel.startsWith(`..${import_path13.sep}`) && !(0, import_path13.isAbsolute)(rel)) return rel.split(import_path13.sep).join("/");
10595
+ }
10596
+ }
10597
+ throw new FileRequestError("PATH_OUTSIDE_WORKSPACE");
10598
+ }
10599
+ async function readImageFile(root, rel, denied = []) {
10600
+ const { abs, rel: clean } = await fenced(root, rel, denied);
10601
+ const st = await (0, import_promises12.stat)(abs).catch(() => null);
10602
+ if (!st) throw new FileRequestError("NOT_FOUND");
10603
+ if (!st.isFile()) throw new FileRequestError("NOT_A_FILE");
10604
+ if (st.size > FILE_CAPS.maxImageBytes) throw new FileRequestError("FILE_TOO_LARGE");
10605
+ const fh = await (0, import_promises12.open)(abs, "r");
10606
+ let buf;
10607
+ try {
10608
+ buf = Buffer.alloc(st.size);
10609
+ await fh.read(buf, 0, st.size, 0);
10610
+ } finally {
10611
+ await fh.close();
10612
+ }
10613
+ const mime = imageMime(buf);
10614
+ if (!mime) throw new FileRequestError("NOT_AN_IMAGE");
10615
+ return { path: clean, mime, size: st.size, data: buf.toString("base64") };
10616
+ }
10505
10617
 
10506
10618
  // src/application/services/workspaceSandbox/orgMcp.ts
10507
10619
  var import_module = require("module");
@@ -11392,7 +11504,7 @@ var WorkspaceEngine = class {
11392
11504
  }
11393
11505
  case "approval": {
11394
11506
  const parsed = ApprovalBroker.fromPayload(p);
11395
- if (parsed) this.broker.resolve(parsed.approvalId, parsed.decision);
11507
+ if (parsed && this.broker.resolve(parsed.approvalId, parsed.decision) && parsed.decision.decision === "REJECT" && this.live) this.live.refused = true;
11396
11508
  return;
11397
11509
  }
11398
11510
  case "stop":
@@ -11536,6 +11648,13 @@ var WorkspaceEngine = class {
11536
11648
  const denied = this.deps.privateDirs ?? [];
11537
11649
  const path = typeof p.path === "string" ? p.path : "";
11538
11650
  const range = { from: typeof p.from === "number" ? p.from : void 0, to: typeof p.to === "number" ? p.to : void 0 };
11651
+ if (kind === "read_file" && p.as === "image") {
11652
+ const rel = await insideRelative(path, [repo2?.root ?? this.deps.root]);
11653
+ const image = await this.fencedFiles(repo2, rel, (root, r) => readImageFile(root, r, denied));
11654
+ const { data, ...meta } = image;
11655
+ await this.deps.transport.reply(commandId, { ok: true, result: { ...repoFullName ? { repoFullName } : {}, ...this.redactor.deep(meta), data } }).catch((e) => this.deps.log.warn("file request not answered", { error: e.message }));
11656
+ return;
11657
+ }
11539
11658
  const result = kind === "list_files" ? await this.fencedFiles(repo2, path, (root, rel) => listFiles(root, rel, denied)) : await this.fencedFiles(repo2, path, (root, rel) => readTextFile(root, rel, range, denied));
11540
11659
  reply = { ok: true, result: { ...repoFullName ? { repoFullName } : {}, ...result } };
11541
11660
  } catch (e) {
@@ -11694,6 +11813,7 @@ ${String(item.content)}` : String(item.content);
11694
11813
  }
11695
11814
  if (moved && !live.foreground.size) return;
11696
11815
  }
11816
+ if (live.refused) return;
11697
11817
  await this.interruptTurn();
11698
11818
  }
11699
11819
  /**
@@ -11961,7 +12081,7 @@ ${String(item.content)}` : String(item.content);
11961
12081
  if (files === "unavailable") this.error("REWIND_FAILED", { turnId });
11962
12082
  this.emit({ type: "rewind", data: { turnId, files, conversation, ...untouched.length ? { untouched } : {} } });
11963
12083
  await this.diffNow();
11964
- await this.saveCheckpoint().catch(() => void 0);
12084
+ await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
11965
12085
  this.setState("READY", "REWOUND");
11966
12086
  await this.sink.flush();
11967
12087
  }
@@ -12385,6 +12505,7 @@ ${text3}`;
12385
12505
  pushChain: Promise.resolve(),
12386
12506
  foreground: /* @__PURE__ */ new Map(),
12387
12507
  interrupted: false,
12508
+ refused: false,
12388
12509
  stopGuard: null,
12389
12510
  between: false,
12390
12511
  snapshot: null,
@@ -12506,8 +12627,9 @@ ${text3}`;
12506
12627
  }
12507
12628
  } catch (e) {
12508
12629
  if (!ac.signal.aborted) {
12509
- this.deps.log.warn("turn failed", { error: this.redactor.text(e.message) });
12510
- this.error("TURN_FAILED");
12630
+ const ended = live.turnEnded || live.interrupted;
12631
+ this.deps.log.warn(ended ? "engine process ended after the turn" : "turn failed", { error: this.redactor.text(e.message) });
12632
+ if (!ended) this.error("TURN_FAILED");
12511
12633
  }
12512
12634
  } finally {
12513
12635
  const stopped = ac.signal.aborted || live.interrupted;
@@ -12520,7 +12642,7 @@ ${text3}`;
12520
12642
  this.turnWatch = null;
12521
12643
  this.previousTrouble = stopped || !watch ? null : watch.errored ? "follows a request that ended with an error" : watch.lastExit !== null && watch.lastExit !== 0 ? "follows a failed verification" : null;
12522
12644
  await this.diffNow();
12523
- await this.saveCheckpoint().catch(() => void 0);
12645
+ await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
12524
12646
  this.setState("READY", stopped ? "STOPPED" : void 0);
12525
12647
  await this.sink.flush();
12526
12648
  }
@@ -12539,6 +12661,7 @@ ${text3}`;
12539
12661
  if (ev.type === "message_start") live.timing.modelCall(typeof ev.message?.id === "string" ? ev.message.id : null);
12540
12662
  } else if (f.type === "assistant") {
12541
12663
  live.timing.modelEvent();
12664
+ live.refused = false;
12542
12665
  const msg = f.message ?? {};
12543
12666
  if (!f.error && typeof msg.model === "string") this.checkModel(live, msg.model);
12544
12667
  live.timing.modelCall(typeof msg.id === "string" ? msg.id : null);
@@ -12614,6 +12737,7 @@ ${text3}`;
12614
12737
  if (snap) this.recordCheckpoint(turnId, snap);
12615
12738
  items.forEach((item, i) => this.messageStatus(String(item.turnId), i === 0 ? "started" : "absorbed"));
12616
12739
  live.interrupted = false;
12740
+ live.refused = false;
12617
12741
  live.turnId = turnId;
12618
12742
  live.fellBack = false;
12619
12743
  live.headUuid = members[0];
@@ -12903,9 +13027,18 @@ ${text3}`;
12903
13027
  * The checkpoint: every open repository's change against its base, plus the
12904
13028
  * saved changes of repositories not open in this run, as one map. In the
12905
13029
  * cloud workspace the engine's transcript goes with it (v4), so a resumed
12906
- * session continues the same conversation.
13030
+ * session continues the same conversation. On the person's computer the
13031
+ * folder keeps its binary and very large files, which the map only names.
13032
+ * The transcript is kept even when the change could not be.
12907
13033
  */
12908
13034
  async saveCheckpoint() {
13035
+ try {
13036
+ await this.saveChange();
13037
+ } finally {
13038
+ await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
13039
+ }
13040
+ }
13041
+ async saveChange() {
12909
13042
  const map = new Map(this.pendingCheckpoints);
12910
13043
  for (const repo2 of this.repos.values()) {
12911
13044
  const key2 = repo2.repoFullName ?? repo2.localId ?? LOCAL_FOLDER_KEY;
@@ -12913,7 +13046,7 @@ ${text3}`;
12913
13046
  if (repo2.unsaved) map.set(key2, repo2.unsaved);
12914
13047
  continue;
12915
13048
  }
12916
- const patch2 = await repo2.tracker.checkpoint();
13049
+ const patch2 = await repo2.tracker.checkpoint(this.local ? { keepInFolder: true } : void 0);
12917
13050
  if (patch2) map.set(key2, patch2);
12918
13051
  else if (repo2.unsaved) map.set(key2, repo2.unsaved);
12919
13052
  else map.delete(key2);
@@ -12925,7 +13058,6 @@ ${patch}`;
12925
13058
  await this.deps.transport.checkpoint({ patch, sdkSessionId: this.sdkSessionId });
12926
13059
  this.lastCheckpoint = key;
12927
13060
  }
12928
- await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
12929
13061
  }
12930
13062
  /** The engine conversation, without secrets, kept by the API (only when it changed). */
12931
13063
  async saveTranscript() {
@@ -12970,8 +13102,7 @@ ${patch}`;
12970
13102
  if (running) await Promise.race([running, new Promise((r) => setTimeout(r, 2e4))]);
12971
13103
  if (opts.checkpoint) {
12972
13104
  await this.saveCheckpoint().catch((e) => {
12973
- this.deps.log.warn("checkpoint failed", { error: e.message });
12974
- this.error("CHECKPOINT_FAILED");
13105
+ this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) });
12975
13106
  });
12976
13107
  }
12977
13108
  if (reason === "shutdown" && this.state && this.state !== "FAILED") this.setState("PAUSED", pausedCode);
@@ -13003,7 +13134,7 @@ ${patch}`;
13003
13134
  this.pendingCheckpoints.delete(repo2.localId);
13004
13135
  repo2.dormant = !repo2.unsaved;
13005
13136
  }
13006
- const stateDir = this.deps.stateDir ?? (0, import_path15.join)(this.deps.scratch, `folder-${(0, import_crypto7.createHash)("sha256").update(local.root).digest("hex").slice(0, 16)}`);
13137
+ const stateDir = this.deps.stateDir ?? (0, import_path15.join)(this.deps.scratch, `folder-${(0, import_crypto8.createHash)("sha256").update(local.root).digest("hex").slice(0, 16)}`);
13007
13138
  const tracker = new LooseFileTracker({ root: local.root, stateDir, excluded: () => local.repos.map((r) => r.root) });
13008
13139
  const tracked = local.gitAvailable && await tracker.init().then(() => true, (e) => {
13009
13140
  this.deps.log.warn("loose files not tracked", { error: e.message });
@@ -13620,7 +13751,7 @@ function buildQueryOptions(o) {
13620
13751
  }
13621
13752
  function spawnKey(i) {
13622
13753
  const { orgMcpServers, ...data } = i;
13623
- return (0, import_crypto7.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
13754
+ return (0, import_crypto8.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
13624
13755
  }
13625
13756
  function timedGate(gate, took) {
13626
13757
  const timed = async (run2) => {
@@ -13646,7 +13777,7 @@ async function hasLocalTranscript(configDir, sessionId) {
13646
13777
  }
13647
13778
 
13648
13779
  // src/application/services/workspaceSandbox/selfUpdate.ts
13649
- var import_crypto8 = require("crypto");
13780
+ var import_crypto9 = require("crypto");
13650
13781
  var import_fs10 = require("fs");
13651
13782
  var import_path16 = require("path");
13652
13783
  var REGISTRY_LATEST = "https://registry.npmjs.org/@scalequality%2fcli/latest";
@@ -13829,7 +13960,7 @@ async function installVersion(o) {
13829
13960
  if (pkg?.name !== "@scalequality/cli" || pkg?.version !== o.version) throw new UpdateError("WRONG_PACKAGE", `npm installed ${String(pkg?.name)}@${String(pkg?.version)} instead of ${o.version}.`);
13830
13961
  const build = readJson2((0, import_path16.join)(root, "dist", "connect.build.json"));
13831
13962
  const bundle = (0, import_path16.join)(root, "dist", "connect.cjs");
13832
- const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto8.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
13963
+ const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto9.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
13833
13964
  if (!sha || typeof build?.sha256 !== "string" || build.sha256 !== sha) throw new UpdateError("BUNDLE_MISMATCH", `The engine of ${o.version} does not match the sha256 it declares.`);
13834
13965
  let copy;
13835
13966
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {