@scalequality/cli 0.4.20 → 0.4.21

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": "64b584e7f307f8bed9bee151ca7dd57c5fe8e54c8adf6e9fd76ba11dbfba93c1",
4
+ "sourceCommit": "a4093e270f574ff0830763c574a17585cf269fe6"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -4893,14 +4893,16 @@ function sandboxTestEnv() {
4893
4893
  var ApprovalBroker = class {
4894
4894
  waiting = /* @__PURE__ */ new Map();
4895
4895
  early = /* @__PURE__ */ new Map();
4896
+ /** True when a call was waiting for this decision (it returns now); false when it is kept for a later wait. */
4896
4897
  resolve(approvalId, decision) {
4897
4898
  const w = this.waiting.get(approvalId);
4898
4899
  if (w) {
4899
4900
  this.waiting.delete(approvalId);
4900
4901
  w(decision);
4901
- } else {
4902
- this.early.set(approvalId, decision);
4902
+ return true;
4903
4903
  }
4904
+ this.early.set(approvalId, decision);
4905
+ return false;
4904
4906
  }
4905
4907
  wait(approvalId, signal) {
4906
4908
  const ready = this.early.get(approvalId);
@@ -10257,7 +10259,8 @@ function buildSystemAppend(c) {
10257
10259
  "- Lead with the outcome in one or two sentences: what changed, what was found, or what you need from the user.",
10258
10260
  "- 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
10261
  "- 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."
10262
+ "- 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.",
10263
+ "- 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
10264
  ].join("\n");
10262
10265
  }
10263
10266
 
@@ -10421,7 +10424,7 @@ async function restoreTranscript(configDir, cwd, t) {
10421
10424
  // src/application/services/workspaceSandbox/workspaceFiles.ts
10422
10425
  var import_promises12 = require("fs/promises");
10423
10426
  var import_path13 = require("path");
10424
- var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3 };
10427
+ var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3, maxImageBytes: 6 * 1024 * 1024 };
10425
10428
  var FileRequestError = class extends Error {
10426
10429
  constructor(code) {
10427
10430
  super(code);
@@ -10502,6 +10505,48 @@ async function readTextFile(root, rel, range = {}, denied = []) {
10502
10505
  }
10503
10506
  return { path: clean, from, to: Math.max(from, Math.min(last, total)), totalLines: total, content, truncated, size: st.size };
10504
10507
  }
10508
+ function imageMime(buf) {
10509
+ const at = (offset, hex) => buf.length >= offset + hex.length / 2 && buf.subarray(offset, offset + hex.length / 2).toString("hex") === hex;
10510
+ if (at(0, "89504e470d0a1a0a")) return "image/png";
10511
+ if (at(0, "ffd8ff")) return "image/jpeg";
10512
+ if (at(0, "474946383761") || at(0, "474946383961")) return "image/gif";
10513
+ if (at(0, "52494646") && at(8, "57454250")) return "image/webp";
10514
+ if (at(4, "66747970") && /^(avif|avis)$/.test(buf.subarray(8, 12).toString("latin1"))) return "image/avif";
10515
+ if (at(0, "424d")) return "image/bmp";
10516
+ const head = buf.subarray(0, 2048).toString("utf8").replace(/^\uFEFF/, "").trimStart();
10517
+ if (/^(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*(<!DOCTYPE svg[^>]*>\s*)?<svg[\s>]/i.test(head)) return "image/svg+xml";
10518
+ return null;
10519
+ }
10520
+ async function insideRelative(path, bases) {
10521
+ const clean = (path ?? "").trim().replace(/^file:\/\//i, "");
10522
+ if (!(0, import_path13.isAbsolute)(clean)) return clean.replace(/^\.\//, "");
10523
+ for (const base of bases) {
10524
+ for (const root of [(0, import_path13.resolve)(base), await (0, import_promises12.realpath)(base).catch(() => (0, import_path13.resolve)(base))]) {
10525
+ const rel = (0, import_path13.relative)(root, (0, import_path13.resolve)(clean));
10526
+ if (!rel) return "";
10527
+ if (rel !== ".." && !rel.startsWith(`..${import_path13.sep}`) && !(0, import_path13.isAbsolute)(rel)) return rel.split(import_path13.sep).join("/");
10528
+ }
10529
+ }
10530
+ throw new FileRequestError("PATH_OUTSIDE_WORKSPACE");
10531
+ }
10532
+ async function readImageFile(root, rel, denied = []) {
10533
+ const { abs, rel: clean } = await fenced(root, rel, denied);
10534
+ const st = await (0, import_promises12.stat)(abs).catch(() => null);
10535
+ if (!st) throw new FileRequestError("NOT_FOUND");
10536
+ if (!st.isFile()) throw new FileRequestError("NOT_A_FILE");
10537
+ if (st.size > FILE_CAPS.maxImageBytes) throw new FileRequestError("FILE_TOO_LARGE");
10538
+ const fh = await (0, import_promises12.open)(abs, "r");
10539
+ let buf;
10540
+ try {
10541
+ buf = Buffer.alloc(st.size);
10542
+ await fh.read(buf, 0, st.size, 0);
10543
+ } finally {
10544
+ await fh.close();
10545
+ }
10546
+ const mime = imageMime(buf);
10547
+ if (!mime) throw new FileRequestError("NOT_AN_IMAGE");
10548
+ return { path: clean, mime, size: st.size, data: buf.toString("base64") };
10549
+ }
10505
10550
 
10506
10551
  // src/application/services/workspaceSandbox/orgMcp.ts
10507
10552
  var import_module = require("module");
@@ -11392,7 +11437,7 @@ var WorkspaceEngine = class {
11392
11437
  }
11393
11438
  case "approval": {
11394
11439
  const parsed = ApprovalBroker.fromPayload(p);
11395
- if (parsed) this.broker.resolve(parsed.approvalId, parsed.decision);
11440
+ if (parsed && this.broker.resolve(parsed.approvalId, parsed.decision) && parsed.decision.decision === "REJECT" && this.live) this.live.refused = true;
11396
11441
  return;
11397
11442
  }
11398
11443
  case "stop":
@@ -11536,6 +11581,13 @@ var WorkspaceEngine = class {
11536
11581
  const denied = this.deps.privateDirs ?? [];
11537
11582
  const path = typeof p.path === "string" ? p.path : "";
11538
11583
  const range = { from: typeof p.from === "number" ? p.from : void 0, to: typeof p.to === "number" ? p.to : void 0 };
11584
+ if (kind === "read_file" && p.as === "image") {
11585
+ const rel = await insideRelative(path, [repo2?.root ?? this.deps.root]);
11586
+ const image = await this.fencedFiles(repo2, rel, (root, r) => readImageFile(root, r, denied));
11587
+ const { data, ...meta } = image;
11588
+ 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 }));
11589
+ return;
11590
+ }
11539
11591
  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
11592
  reply = { ok: true, result: { ...repoFullName ? { repoFullName } : {}, ...result } };
11541
11593
  } catch (e) {
@@ -11694,6 +11746,7 @@ ${String(item.content)}` : String(item.content);
11694
11746
  }
11695
11747
  if (moved && !live.foreground.size) return;
11696
11748
  }
11749
+ if (live.refused) return;
11697
11750
  await this.interruptTurn();
11698
11751
  }
11699
11752
  /**
@@ -12385,6 +12438,7 @@ ${text3}`;
12385
12438
  pushChain: Promise.resolve(),
12386
12439
  foreground: /* @__PURE__ */ new Map(),
12387
12440
  interrupted: false,
12441
+ refused: false,
12388
12442
  stopGuard: null,
12389
12443
  between: false,
12390
12444
  snapshot: null,
@@ -12506,8 +12560,9 @@ ${text3}`;
12506
12560
  }
12507
12561
  } catch (e) {
12508
12562
  if (!ac.signal.aborted) {
12509
- this.deps.log.warn("turn failed", { error: this.redactor.text(e.message) });
12510
- this.error("TURN_FAILED");
12563
+ const ended = live.turnEnded || live.interrupted;
12564
+ this.deps.log.warn(ended ? "engine process ended after the turn" : "turn failed", { error: this.redactor.text(e.message) });
12565
+ if (!ended) this.error("TURN_FAILED");
12511
12566
  }
12512
12567
  } finally {
12513
12568
  const stopped = ac.signal.aborted || live.interrupted;
@@ -12539,6 +12594,7 @@ ${text3}`;
12539
12594
  if (ev.type === "message_start") live.timing.modelCall(typeof ev.message?.id === "string" ? ev.message.id : null);
12540
12595
  } else if (f.type === "assistant") {
12541
12596
  live.timing.modelEvent();
12597
+ live.refused = false;
12542
12598
  const msg = f.message ?? {};
12543
12599
  if (!f.error && typeof msg.model === "string") this.checkModel(live, msg.model);
12544
12600
  live.timing.modelCall(typeof msg.id === "string" ? msg.id : null);
@@ -12614,6 +12670,7 @@ ${text3}`;
12614
12670
  if (snap) this.recordCheckpoint(turnId, snap);
12615
12671
  items.forEach((item, i) => this.messageStatus(String(item.turnId), i === 0 ? "started" : "absorbed"));
12616
12672
  live.interrupted = false;
12673
+ live.refused = false;
12617
12674
  live.turnId = turnId;
12618
12675
  live.fellBack = false;
12619
12676
  live.headUuid = members[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
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": {