@scalequality/cli 0.4.21 → 0.4.23

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": "64b584e7f307f8bed9bee151ca7dd57c5fe8e54c8adf6e9fd76ba11dbfba93c1",
4
- "sourceCommit": "a4093e270f574ff0830763c574a17585cf269fe6"
3
+ "sha256": "73abecf4dfd9ae9523b7b024af7979b0047893e02b842bfddd8e3d9e6d30d3ab",
4
+ "sourceCommit": "3ddf22e4f5c3cb9757011873fcedc57b914f6b29"
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;
@@ -1083,7 +1122,9 @@ var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
1083
1122
  var DENIED_TOOLS = ["WebSearch", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
1084
1123
  var READ_TOOLS = { Read: "file_path", Glob: "path", Grep: "path", LS: "path" };
1085
1124
  var WRITE_TOOLS = { Write: "file_path", Edit: "file_path", MultiEdit: "file_path", NotebookEdit: "notebook_path" };
1086
- var WRITE_TOOL_PATHS = WRITE_TOOLS;
1125
+ var IMAGE_TOOL = "generate_image";
1126
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
1127
+ var WRITE_TOOL_PATHS = { ...WRITE_TOOLS, [`mcp__${"scalequality"}__${IMAGE_TOOL}`]: "file_path" };
1087
1128
  var HARMLESS = /* @__PURE__ */ new Set(["TodoWrite", "BashOutput", "KillShell", "TaskStop", "TaskOutput", "Skill"]);
1088
1129
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
1089
1130
  var SQ_READ_ONLY = /* @__PURE__ */ new Set([
@@ -1205,6 +1246,18 @@ function readOnlyCommand(command) {
1205
1246
  return READ_ONLY_PROGRAMS.has(program);
1206
1247
  });
1207
1248
  }
1249
+ async function imageToolDecision(input, ctx) {
1250
+ const raw = input.file_path;
1251
+ if (typeof raw !== "string" || !raw.trim()) return { behavior: "deny", message: "generate_image needs file_path: where to save the image in the folder (for example images/hero.png)." };
1252
+ if (!IMAGE_EXTENSIONS.has((0, import_path3.extname)(raw).toLowerCase())) return { behavior: "deny", message: "generate_image saves .png, .jpg or .webp files: give file_path one of these extensions." };
1253
+ if (ctx.protections?.secretFiles) {
1254
+ const why = secretFileToolDenial("Write", { file_path: raw });
1255
+ if (why) return { behavior: "deny", message: why, reason: "PROTECTION_SECRET_FILE" };
1256
+ }
1257
+ const decided = await decideToolUse("Write", { file_path: raw }, { ...ctx, plan: false, planDir: void 0 });
1258
+ if (decided.behavior !== "allow") return decided;
1259
+ return { behavior: "allow", updatedInput: { ...input, file_path: (0, import_path3.resolve)(ctx.root, raw.trim()) } };
1260
+ }
1208
1261
  async function decideToolUse(toolName, input, ctx) {
1209
1262
  if (toolName.startsWith("mcp__") && ctx.orgMcpTools?.all.has(toolName)) {
1210
1263
  if (ctx.plan && !ctx.orgMcpTools.readOnly.has(toolName)) return { behavior: "deny", message: PLAN_ONLY_READS };
@@ -1213,6 +1266,7 @@ async function decideToolUse(toolName, input, ctx) {
1213
1266
  if (toolName.startsWith("mcp__")) {
1214
1267
  if (!toolName.startsWith(SQ_MCP_PREFIX)) return { behavior: "deny", message: "Only ScaleQuality tools and the MCP servers your organization approved are available in this workspace." };
1215
1268
  if (ctx.plan && !SQ_READ_ONLY.has(toolName.slice(SQ_MCP_PREFIX.length))) return { behavior: "deny", message: PLAN_ONLY_READS };
1269
+ if (toolName === SQ_MCP_PREFIX + IMAGE_TOOL) return imageToolDecision(input, ctx);
1216
1270
  return { behavior: "allow", updatedInput: input };
1217
1271
  }
1218
1272
  if (HARMLESS.has(toolName)) return { behavior: "allow", updatedInput: input };
@@ -1931,6 +1985,8 @@ var TransportError = class extends Error {
1931
1985
  };
1932
1986
 
1933
1987
  // src/application/services/workspaceSandbox/HttpSessionTransport.ts
1988
+ var gzipAsync = (0, import_util2.promisify)(import_zlib.gzip);
1989
+ var gunzipAsync = (0, import_util2.promisify)(import_zlib.gunzip);
1934
1990
  var GONE = [401, 403, 404, 410];
1935
1991
  var HttpSessionTransport = class {
1936
1992
  constructor(opts) {
@@ -1946,6 +2002,8 @@ var HttpSessionTransport = class {
1946
2002
  gone;
1947
2003
  async bootstrap() {
1948
2004
  const raw = await this.get(`/bootstrap?features=${ENGINE_FEATURES.join(",")}`, this.opts.requestTimeoutMs ?? 3e4);
2005
+ const parts = checkpointPartsOf(raw?.checkpointParts);
2006
+ if (parts && typeof raw?.checkpointPatch !== "string") raw.checkpointPatch = await this.checkpointFromParts(parts);
1949
2007
  return toSessionBootstrap(raw);
1950
2008
  }
1951
2009
  async pollCommands(waitSeconds, signal) {
@@ -1972,7 +2030,31 @@ var HttpSessionTransport = class {
1972
2030
  return this.post("/pull-request", req, this.opts.toolTimeoutMs ?? 18e4);
1973
2031
  }
1974
2032
  async checkpoint(req) {
1975
- await this.post("/checkpoint", req, this.opts.requestTimeoutMs ?? 6e4);
2033
+ const timeout = this.opts.requestTimeoutMs ?? 6e4;
2034
+ if (Buffer.byteLength(JSON.stringify(req), "utf8") <= CHECKPOINT_INLINE_MAX_BYTES) {
2035
+ await this.post("/checkpoint", req, timeout);
2036
+ return;
2037
+ }
2038
+ const packed = await gzipAsync(Buffer.from(req.patch, "utf8"));
2039
+ const sha256 = (0, import_crypto2.createHash)("sha256").update(req.patch, "utf8").digest("hex");
2040
+ const count = Math.ceil(packed.length / CHECKPOINT_PART_BYTES);
2041
+ if (count > CHECKPOINT_MAX_PARTS) throw new TransportError(`checkpoint of ${packed.length} compressed bytes is over the limit`, 413, false);
2042
+ for (let index = 0; index < count; index++) {
2043
+ const data = packed.subarray(index * CHECKPOINT_PART_BYTES, (index + 1) * CHECKPOINT_PART_BYTES).toString("base64");
2044
+ await this.post("/checkpoint/parts", { sha256, index, count, data }, timeout);
2045
+ }
2046
+ await this.post("/checkpoint", { parts: { sha256, count, bytes: packed.length }, sdkSessionId: req.sdkSessionId }, timeout);
2047
+ }
2048
+ async checkpointFromParts(parts) {
2049
+ const chunks = [];
2050
+ for (let index = 0; index < parts.count; index++) {
2051
+ const r = await this.get(`/checkpoint/parts/${index}`, this.opts.requestTimeoutMs ?? 6e4);
2052
+ if (r?.sha256 !== parts.sha256 || typeof r.data !== "string") throw new TransportError("checkpoint part does not match", 502, false);
2053
+ chunks.push(Buffer.from(r.data, "base64"));
2054
+ }
2055
+ const patch = (await gunzipAsync(Buffer.concat(chunks)).catch(() => Buffer.alloc(0))).toString("utf8");
2056
+ if ((0, import_crypto2.createHash)("sha256").update(patch, "utf8").digest("hex") !== parts.sha256) throw new TransportError("checkpoint parts do not match", 502, false);
2057
+ return patch;
1976
2058
  }
1977
2059
  async openRepository(repoFullName) {
1978
2060
  const raw = await this.post("/repositories/open", { repoFullName }, this.opts.requestTimeoutMs ?? 3e4);
@@ -2036,6 +2118,15 @@ var HttpSessionTransport = class {
2036
2118
  const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
2037
2119
  return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
2038
2120
  }
2121
+ /** One image created by the API for this session (generate_image); an image takes up to a few minutes. */
2122
+ async generateImage(req) {
2123
+ const raw = await this.post("/images", req, 24e4);
2124
+ if (typeof raw?.error?.code === "string") return { refused: raw.error.code.slice(0, 100) };
2125
+ const image = raw?.image;
2126
+ if (!image || typeof image.dataBase64 !== "string" || typeof image.mimeType !== "string" || typeof image.name !== "string")
2127
+ throw new TransportError("invalid image response", 502, false);
2128
+ return { name: image.name, mimeType: image.mimeType, sizeBytes: Number(image.sizeBytes) || 0, dataBase64: image.dataBase64 };
2129
+ }
2039
2130
  /** The organization's protections in force now (read at the start of each turn); throws when the API does not say. */
2040
2131
  async protections() {
2041
2132
  const raw = await this.post("/protections", {}, 5e3);
@@ -2187,6 +2278,7 @@ function toSessionBootstrap(raw) {
2187
2278
  workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
2188
2279
  projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
2189
2280
  folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
2281
+ imageGeneration: raw?.imageGeneration === true,
2190
2282
  transcript: transcriptOf(raw?.transcript),
2191
2283
  baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
2192
2284
  pullRequests: pullRequestsOf(raw?.pullRequests),
@@ -2277,7 +2369,7 @@ var import_os3 = require("os");
2277
2369
  var import_path5 = require("path");
2278
2370
 
2279
2371
  // src/application/services/workspaceSandbox/localTracking.ts
2280
- var import_crypto2 = require("crypto");
2372
+ var import_crypto3 = require("crypto");
2281
2373
  var import_fs4 = require("fs");
2282
2374
  var import_promises5 = require("fs/promises");
2283
2375
  var import_os2 = require("os");
@@ -2305,7 +2397,7 @@ function gitTracker(root, base, prefix) {
2305
2397
  },
2306
2398
  snapshot: () => worktreeTreeId(root, scope),
2307
2399
  restore: (tree) => restoreTree(root, tree, scope),
2308
- checkpoint: () => checkpointPatch(root, base, scope),
2400
+ checkpoint: (opts) => checkpointPatch(root, base, scope, opts),
2309
2401
  prFiles: (published) => filesForPullRequest(root, base, published, scope),
2310
2402
  treeId: () => worktreeTreeId(root, scope)
2311
2403
  };
@@ -2389,7 +2481,7 @@ async function mapLimit(items, n, fn) {
2389
2481
  async function repoFingerprint(root) {
2390
2482
  const head = (await git(["rev-parse", "-q", "--verify", "HEAD"], { cwd: root }).catch(() => "")).trim();
2391
2483
  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);
2484
+ const h = (0, import_crypto3.createHash)("sha256").update(head).update("\0").update(status);
2393
2485
  const recs = status.split("\0");
2394
2486
  let n = 0;
2395
2487
  for (let i = 0; i < recs.length && n < 2e3; i++) {
@@ -2561,10 +2653,10 @@ var LooseFileTracker = class {
2561
2653
  }
2562
2654
  await restoreTree(this.o.root, target, this.scope());
2563
2655
  }
2564
- async checkpoint() {
2656
+ async checkpoint(opts) {
2565
2657
  await this.chain;
2566
2658
  if (!this.entries.length) return "";
2567
- return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
2659
+ return checkpointPatch(this.o.root, await this.baseTree(), this.scope(), opts);
2568
2660
  }
2569
2661
  async prFiles(published) {
2570
2662
  await this.chain;
@@ -3530,7 +3622,7 @@ function serviceSlug(api) {
3530
3622
  } catch {
3531
3623
  }
3532
3624
  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)}`;
3625
+ return `${slug}-${(0, import_crypto4.createHash)("sha256").update(api).digest("hex").slice(0, 6)}`;
3534
3626
  }
3535
3627
  function serviceNames(api) {
3536
3628
  const slug = serviceSlug(api);
@@ -3669,7 +3761,7 @@ function installStableCopy(o) {
3669
3761
  const bundle = (0, import_path6.join)(o.packageRoot, "dist", "connect.cjs");
3670
3762
  const bin = (dir2) => (0, import_path6.join)(dir2, "node_modules", "@scalequality", "cli", "bin", "scalequality.mjs");
3671
3763
  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");
3764
+ const sha = (0, import_crypto4.createHash)("sha256").update((0, import_fs5.readFileSync)(bundle)).digest("hex");
3673
3765
  const base = (0, import_path6.join)(sqHome(o.home), "cli");
3674
3766
  const dir = (0, import_path6.join)(base, pkg.version);
3675
3767
  if (inside2(dir, o.packageRoot)) return { dir, bin: bin(dir), version: pkg.version, reused: true };
@@ -4817,7 +4909,7 @@ async function addFolder(credentials2, api, path, home) {
4817
4909
  var import_child_process3 = require("child_process");
4818
4910
  var import_promises14 = require("fs/promises");
4819
4911
  var import_fs9 = require("fs");
4820
- var import_crypto7 = require("crypto");
4912
+ var import_crypto8 = require("crypto");
4821
4913
  var import_path15 = require("path");
4822
4914
 
4823
4915
  // src/application/services/execution/LanguageAdapter.ts
@@ -5261,7 +5353,7 @@ function limitInEngineText(text3) {
5261
5353
  }
5262
5354
 
5263
5355
  // src/application/services/workspaceSandbox/scalequalityTools.ts
5264
- var import_crypto4 = require("crypto");
5356
+ var import_crypto5 = require("crypto");
5265
5357
 
5266
5358
  // node_modules/zod/v3/external.js
5267
5359
  var external_exports = {};
@@ -9381,7 +9473,7 @@ function buildScaleQualityServer(sdk, host) {
9381
9473
  const tools = REMOTE_TOOLS.map(
9382
9474
  (spec) => sdk.tool(spec.name, spec.description, spec.shape, async (args) => {
9383
9475
  const a = { ...args ?? {} };
9384
- if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0, import_crypto4.randomUUID)();
9476
+ if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0, import_crypto5.randomUUID)();
9385
9477
  return callScaleQualityTool(host, spec.name, a);
9386
9478
  })
9387
9479
  );
@@ -9412,6 +9504,19 @@ function buildScaleQualityServer(sdk, host) {
9412
9504
  async (args) => host.openPullRequest(String(args.title ?? ""), String(args.body ?? ""), typeof args.repoFullName === "string" ? args.repoFullName : void 0)
9413
9505
  )
9414
9506
  );
9507
+ if (host.generateImage) {
9508
+ const generate = host.generateImage.bind(host);
9509
+ tools.push(sdk.tool(
9510
+ IMAGE_TOOL,
9511
+ "Create an image with an image generation model and save it in the workspace folder; then show it to the user in your answer as a Markdown image with its path. Use it whenever the user asks for an image, picture, illustration, concept art, character sheet, sprite, mockup or any other visual. It may spend credit, like any model call.",
9512
+ {
9513
+ prompt: external_exports.string().min(1).max(4e3).describe("A detailed description of the image in English: subject, style, composition, colours, lighting and any text to render."),
9514
+ file_path: external_exports.string().min(1).max(500).describe("Where to save it in the workspace, ending in .png, .jpg or .webp (for example images/hero.png)."),
9515
+ edit_image_path: external_exports.string().min(1).max(500).optional().describe("An image already in the workspace to change, instead of starting a new one.")
9516
+ },
9517
+ async (args) => generate({ prompt: String(args.prompt ?? ""), filePath: String(args.file_path ?? ""), ...typeof args.edit_image_path === "string" ? { editPath: args.edit_image_path } : {} })
9518
+ ));
9519
+ }
9415
9520
  return sdk.createSdkMcpServer({
9416
9521
  name: SQ_MCP_SERVER,
9417
9522
  version: "1.0.0",
@@ -9492,6 +9597,7 @@ var SQ_TOOL_LABELS = {
9492
9597
  list_repositories: { kind: "tool", label: "Listing the repositories of the session" },
9493
9598
  open_repository: { kind: "tool", label: "Opening a repository" },
9494
9599
  measure_change: { kind: "measure", label: "Measuring the change" },
9600
+ generate_image: { kind: "tool", label: "Creating an image" },
9495
9601
  open_pull_request: { kind: "tool", label: "Opening the pull request" }
9496
9602
  };
9497
9603
  function clip(s, n) {
@@ -9956,7 +10062,7 @@ function blockText(content) {
9956
10062
  }
9957
10063
 
9958
10064
  // src/application/services/workspaceSandbox/inputChannel.ts
9959
- var import_crypto5 = require("crypto");
10065
+ var import_crypto6 = require("crypto");
9960
10066
  var InputChannel = class {
9961
10067
  buffer = [];
9962
10068
  waiting = null;
@@ -10014,7 +10120,7 @@ var InputChannel = class {
10014
10120
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10015
10121
  function messageUuid(id) {
10016
10122
  if (UUID.test(id)) return id.toLowerCase();
10017
- const h = (0, import_crypto5.createHash)("sha256").update(`sq-message:${id}`).digest("hex");
10123
+ const h = (0, import_crypto6.createHash)("sha256").update(`sq-message:${id}`).digest("hex");
10018
10124
  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)}`;
10019
10125
  }
10020
10126
  function userMessage(text3, images, uuid2, priority) {
@@ -10260,7 +10366,8 @@ function buildSystemAppend(c) {
10260
10366
  "- 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).",
10261
10367
  "- 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.",
10262
10368
  "- 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."
10369
+ "- 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.",
10370
+ ...c.imageGeneration ? ["- To create a new image (a character sheet, concept art, a sprite, an icon, a mockup, a reference board), call generate_image with a detailed prompt and where to save it, then show it from that path. Never say you cannot create images, and never answer an image request with only a prompt for another tool."] : []
10264
10371
  ].join("\n");
10265
10372
  }
10266
10373
 
@@ -10346,11 +10453,11 @@ async function detectTestCommandFromManifests(dir) {
10346
10453
  }
10347
10454
 
10348
10455
  // src/application/services/workspaceSandbox/transcript.ts
10349
- var import_crypto6 = require("crypto");
10456
+ var import_crypto7 = require("crypto");
10350
10457
  var import_fs8 = require("fs");
10351
10458
  var import_promises11 = require("fs/promises");
10352
10459
  var import_path12 = require("path");
10353
- var import_zlib = require("zlib");
10460
+ var import_zlib2 = require("zlib");
10354
10461
  var TRANSCRIPT_CAPS = { maxRawBytes: 64 * 1024 * 1024, maxCompressedBytes: 4 * 1024 * 1024 };
10355
10462
  function engineProjectDir(cwd) {
10356
10463
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -10389,10 +10496,10 @@ async function packTranscript(configDir, sessionId, redactor, caps = TRANSCRIPT_
10389
10496
  }
10390
10497
  const text3 = lines2.length ? `${lines2.join("\n")}
10391
10498
  ` : "";
10392
- const zipped = (0, import_zlib.gzipSync)(Buffer.from(text3, "utf8"), { level: 9 });
10499
+ const zipped = (0, import_zlib2.gzipSync)(Buffer.from(text3, "utf8"), { level: 9 });
10393
10500
  if (zipped.length > caps.maxCompressedBytes) return { tooLarge: true, bytes: zipped.length };
10394
10501
  return {
10395
- payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0, import_crypto6.createHash)("sha256").update(text3).digest("hex") },
10502
+ payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") },
10396
10503
  secretsRemoved: removed
10397
10504
  };
10398
10505
  }
@@ -10401,11 +10508,11 @@ async function restoreTranscript(configDir, cwd, t) {
10401
10508
  if (await findTranscript(configDir, t.sdkSessionId)) return true;
10402
10509
  let text3;
10403
10510
  try {
10404
- text3 = (0, import_zlib.gunzipSync)(Buffer.from(t.data, "base64"), { maxOutputLength: TRANSCRIPT_CAPS.maxRawBytes });
10511
+ text3 = (0, import_zlib2.gunzipSync)(Buffer.from(t.data, "base64"), { maxOutputLength: TRANSCRIPT_CAPS.maxRawBytes });
10405
10512
  } catch {
10406
10513
  return false;
10407
10514
  }
10408
- if (t.sha256 && (0, import_crypto6.createHash)("sha256").update(text3).digest("hex") !== t.sha256) return false;
10515
+ if (t.sha256 && (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") !== t.sha256) return false;
10409
10516
  const dir = (0, import_path12.join)(configDir, "projects", engineProjectDir(cwd));
10410
10517
  await (0, import_promises11.mkdir)(dir, { recursive: true, mode: 448 });
10411
10518
  const target = (0, import_path12.join)(dir, `${t.sdkSessionId}.jsonl`);
@@ -10904,6 +11011,7 @@ function editPreview(tool, input) {
10904
11011
  else if (tool === "MultiEdit" && Array.isArray(input.edits)) {
10905
11012
  for (const e of input.edits.slice(0, 5)) out2.push(...lines2(e?.old_string, "-"), ...lines2(e?.new_string, "+"), "");
10906
11013
  } else if (tool === "NotebookEdit") out2.push(...lines2(input.new_source, "+"));
11014
+ else if (tool.endsWith("__generate_image")) out2.push(...lines2(input.prompt, "+"));
10907
11015
  return out2.join("\n").slice(0, 4e3);
10908
11016
  }
10909
11017
  function turnOffering(raw, loaded) {
@@ -11821,7 +11929,8 @@ ${String(item.content)}` : String(item.content);
11821
11929
  })),
11822
11930
  onDemand: !!this.deps.clone && !this.local,
11823
11931
  layout: this.promptLayout(),
11824
- scopeBrief: this.scopeBrief
11932
+ scopeBrief: this.scopeBrief,
11933
+ imageGeneration: !!this.boot?.imageGeneration && !!this.deps.transport.generateImage
11825
11934
  });
11826
11935
  const conventions = await readConventions([...this.repos.values()].filter((r) => !r.dormant).flatMap((r) => {
11827
11936
  const label = r.repoFullName ?? r.localId ?? (0, import_path15.basename)(r.root);
@@ -12014,7 +12123,7 @@ ${String(item.content)}` : String(item.content);
12014
12123
  if (files === "unavailable") this.error("REWIND_FAILED", { turnId });
12015
12124
  this.emit({ type: "rewind", data: { turnId, files, conversation, ...untouched.length ? { untouched } : {} } });
12016
12125
  await this.diffNow();
12017
- await this.saveCheckpoint().catch(() => void 0);
12126
+ await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
12018
12127
  this.setState("READY", "REWOUND");
12019
12128
  await this.sink.flush();
12020
12129
  }
@@ -12575,7 +12684,7 @@ ${text3}`;
12575
12684
  this.turnWatch = null;
12576
12685
  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;
12577
12686
  await this.diffNow();
12578
- await this.saveCheckpoint().catch(() => void 0);
12687
+ await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
12579
12688
  this.setState("READY", stopped ? "STOPPED" : void 0);
12580
12689
  await this.sink.flush();
12581
12690
  }
@@ -12960,9 +13069,18 @@ ${text3}`;
12960
13069
  * The checkpoint: every open repository's change against its base, plus the
12961
13070
  * saved changes of repositories not open in this run, as one map. In the
12962
13071
  * cloud workspace the engine's transcript goes with it (v4), so a resumed
12963
- * session continues the same conversation.
13072
+ * session continues the same conversation. On the person's computer the
13073
+ * folder keeps its binary and very large files, which the map only names.
13074
+ * The transcript is kept even when the change could not be.
12964
13075
  */
12965
13076
  async saveCheckpoint() {
13077
+ try {
13078
+ await this.saveChange();
13079
+ } finally {
13080
+ await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
13081
+ }
13082
+ }
13083
+ async saveChange() {
12966
13084
  const map = new Map(this.pendingCheckpoints);
12967
13085
  for (const repo2 of this.repos.values()) {
12968
13086
  const key2 = repo2.repoFullName ?? repo2.localId ?? LOCAL_FOLDER_KEY;
@@ -12970,7 +13088,7 @@ ${text3}`;
12970
13088
  if (repo2.unsaved) map.set(key2, repo2.unsaved);
12971
13089
  continue;
12972
13090
  }
12973
- const patch2 = await repo2.tracker.checkpoint();
13091
+ const patch2 = await repo2.tracker.checkpoint(this.local ? { keepInFolder: true } : void 0);
12974
13092
  if (patch2) map.set(key2, patch2);
12975
13093
  else if (repo2.unsaved) map.set(key2, repo2.unsaved);
12976
13094
  else map.delete(key2);
@@ -12982,7 +13100,6 @@ ${patch}`;
12982
13100
  await this.deps.transport.checkpoint({ patch, sdkSessionId: this.sdkSessionId });
12983
13101
  this.lastCheckpoint = key;
12984
13102
  }
12985
- await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
12986
13103
  }
12987
13104
  /** The engine conversation, without secrets, kept by the API (only when it changed). */
12988
13105
  async saveTranscript() {
@@ -13027,8 +13144,7 @@ ${patch}`;
13027
13144
  if (running) await Promise.race([running, new Promise((r) => setTimeout(r, 2e4))]);
13028
13145
  if (opts.checkpoint) {
13029
13146
  await this.saveCheckpoint().catch((e) => {
13030
- this.deps.log.warn("checkpoint failed", { error: e.message });
13031
- this.error("CHECKPOINT_FAILED");
13147
+ this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) });
13032
13148
  });
13033
13149
  }
13034
13150
  if (reason === "shutdown" && this.state && this.state !== "FAILED") this.setState("PAUSED", pausedCode);
@@ -13060,7 +13176,7 @@ ${patch}`;
13060
13176
  this.pendingCheckpoints.delete(repo2.localId);
13061
13177
  repo2.dormant = !repo2.unsaved;
13062
13178
  }
13063
- 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)}`);
13179
+ 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)}`);
13064
13180
  const tracker = new LooseFileTracker({ root: local.root, stateDir, excluded: () => local.repos.map((r) => r.root) });
13065
13181
  const tracked = local.gitAvailable && await tracker.init().then(() => true, (e) => {
13066
13182
  this.deps.log.warn("loose files not tracked", { error: e.message });
@@ -13203,9 +13319,48 @@ ${patch}`;
13203
13319
  measureChange: (repo2) => this.measureChange(repo2),
13204
13320
  openPullRequest: (title, body, repo2) => this.openPullRequest(title, body, repo2),
13205
13321
  openRepository: (repo2) => this.openRepository(repo2),
13206
- listRepositories: async () => this.listRepositories()
13322
+ listRepositories: async () => this.listRepositories(),
13323
+ // Offered only when the API creates images for this session (an older API never gets the call).
13324
+ ...this.boot?.imageGeneration && this.deps.transport.generateImage ? { generateImage: (a) => this.generateImage(a) } : {}
13207
13325
  };
13208
13326
  }
13327
+ /**
13328
+ * generate_image (Erik, 20/10/2026: Code and Chat both create images): the API creates it as the "Create image" mode
13329
+ * does, for the session's owner and metered to that person; the engine saves it where the model asked, inside the
13330
+ * folder (the policy fenced the path and the person's mode already allowed the write), and the conversation shows it
13331
+ * from there. A refusal or failure goes back to the model to tell, and nothing is retried.
13332
+ */
13333
+ async generateImage(args) {
13334
+ const transport = this.deps.transport, root = this.deps.root;
13335
+ if (!transport.generateImage) return text("Creating images is not available in this session. Tell the user plainly.", true);
13336
+ const target = await resolveInside(root, args.filePath).catch(() => null);
13337
+ if (!target) return text(`generate_image saves only inside the workspace (${root}). Nothing was created.`, true);
13338
+ let reference;
13339
+ if (args.editPath) {
13340
+ try {
13341
+ const image = await readImageFile(root, await insideRelative(args.editPath, [root]), this.deps.privateDirs ?? []);
13342
+ if (!["image/png", "image/jpeg", "image/webp"].includes(image.mime)) return text(`${args.editPath} is not a PNG, JPEG or WebP image, so it cannot be changed. Nothing was created.`, true);
13343
+ reference = { name: (0, import_path15.basename)(image.path), mimeType: image.mime, sizeBytes: image.size, dataBase64: image.data };
13344
+ } catch (e) {
13345
+ return text(`Could not read ${args.editPath} to change it (${e instanceof FileRequestError ? e.code : "UNREADABLE"}). Nothing was created.`, true);
13346
+ }
13347
+ }
13348
+ let created;
13349
+ try {
13350
+ created = await transport.generateImage({ prompt: args.prompt.slice(0, 4e3), ...reference ? { reference } : {}, idempotencyKey: (0, import_crypto8.randomUUID)() });
13351
+ } catch (e) {
13352
+ const why = e instanceof SessionGoneError ? `refused (${e.status})` : e instanceof TransportError && e.status ? `failed (${e.status})` : "failed";
13353
+ return text(`The image could not be created: ScaleQuality ${why}. Tell the user plainly in one sentence; do not claim an image was made and do not offer a prompt for another tool instead.`, true);
13354
+ }
13355
+ if ("refused" in created) return text(`The image could not be created (${created.refused}). Tell the user plainly in one sentence; do not claim an image was made and do not offer a prompt for another tool instead.`, true);
13356
+ const bytes = Buffer.from(created.dataBase64, "base64");
13357
+ if (!bytes.length || !imageMime(bytes)) return text("The image model returned something that is not an image. Nothing was saved.", true);
13358
+ await (0, import_promises14.mkdir)((0, import_path15.dirname)(target), { recursive: true });
13359
+ await (0, import_promises14.writeFile)(target, bytes);
13360
+ this.scheduleDiff();
13361
+ const rel = (0, import_path15.relative)(await (0, import_promises14.realpath)(root).catch(() => root), target).split(import_path15.sep).join("/");
13362
+ return text(`Saved the image to ${rel}. Show it to the user in your answer as ![short description](${rel}) and say in one or two sentences what you made.`);
13363
+ }
13209
13364
  listRepositories() {
13210
13365
  const open2 = [...this.repos.values()];
13211
13366
  const data = {
@@ -13677,7 +13832,7 @@ function buildQueryOptions(o) {
13677
13832
  }
13678
13833
  function spawnKey(i) {
13679
13834
  const { orgMcpServers, ...data } = i;
13680
- return (0, import_crypto7.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
13835
+ return (0, import_crypto8.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
13681
13836
  }
13682
13837
  function timedGate(gate, took) {
13683
13838
  const timed = async (run2) => {
@@ -13703,7 +13858,7 @@ async function hasLocalTranscript(configDir, sessionId) {
13703
13858
  }
13704
13859
 
13705
13860
  // src/application/services/workspaceSandbox/selfUpdate.ts
13706
- var import_crypto8 = require("crypto");
13861
+ var import_crypto9 = require("crypto");
13707
13862
  var import_fs10 = require("fs");
13708
13863
  var import_path16 = require("path");
13709
13864
  var REGISTRY_LATEST = "https://registry.npmjs.org/@scalequality%2fcli/latest";
@@ -13886,7 +14041,7 @@ async function installVersion(o) {
13886
14041
  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}.`);
13887
14042
  const build = readJson2((0, import_path16.join)(root, "dist", "connect.build.json"));
13888
14043
  const bundle = (0, import_path16.join)(root, "dist", "connect.cjs");
13889
- const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto8.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
14044
+ const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto9.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
13890
14045
  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.`);
13891
14046
  let copy;
13892
14047
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
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": {