@scalequality/cli 0.4.22 → 0.4.24

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": "46fe6b09a8c69a92a0f54cc420a3596c6f13b82177d3aec1d46456ae508a4517",
4
- "sourceCommit": "2e2e401c42e98d17e1c0dd8286bdc6d15e44f77b"
3
+ "sha256": "56fec662ea9898b810790ac0362ca07038e58a4de77d5acff3ad1797a34436d0",
4
+ "sourceCommit": "774041e55e6d33ce8fd785e6a36d9b913df3f077"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -1122,7 +1122,9 @@ var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
1122
1122
  var DENIED_TOOLS = ["WebSearch", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
1123
1123
  var READ_TOOLS = { Read: "file_path", Glob: "path", Grep: "path", LS: "path" };
1124
1124
  var WRITE_TOOLS = { Write: "file_path", Edit: "file_path", MultiEdit: "file_path", NotebookEdit: "notebook_path" };
1125
- 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" };
1126
1128
  var HARMLESS = /* @__PURE__ */ new Set(["TodoWrite", "BashOutput", "KillShell", "TaskStop", "TaskOutput", "Skill"]);
1127
1129
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
1128
1130
  var SQ_READ_ONLY = /* @__PURE__ */ new Set([
@@ -1244,6 +1246,18 @@ function readOnlyCommand(command) {
1244
1246
  return READ_ONLY_PROGRAMS.has(program);
1245
1247
  });
1246
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
+ }
1247
1261
  async function decideToolUse(toolName, input, ctx) {
1248
1262
  if (toolName.startsWith("mcp__") && ctx.orgMcpTools?.all.has(toolName)) {
1249
1263
  if (ctx.plan && !ctx.orgMcpTools.readOnly.has(toolName)) return { behavior: "deny", message: PLAN_ONLY_READS };
@@ -1252,6 +1266,7 @@ async function decideToolUse(toolName, input, ctx) {
1252
1266
  if (toolName.startsWith("mcp__")) {
1253
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." };
1254
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);
1255
1270
  return { behavior: "allow", updatedInput: input };
1256
1271
  }
1257
1272
  if (HARMLESS.has(toolName)) return { behavior: "allow", updatedInput: input };
@@ -2103,6 +2118,15 @@ var HttpSessionTransport = class {
2103
2118
  const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
2104
2119
  return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
2105
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
+ }
2106
2130
  /** The organization's protections in force now (read at the start of each turn); throws when the API does not say. */
2107
2131
  async protections() {
2108
2132
  const raw = await this.post("/protections", {}, 5e3);
@@ -2254,6 +2278,7 @@ function toSessionBootstrap(raw) {
2254
2278
  workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
2255
2279
  projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
2256
2280
  folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
2281
+ imageGeneration: raw?.imageGeneration === true,
2257
2282
  transcript: transcriptOf(raw?.transcript),
2258
2283
  baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
2259
2284
  pullRequests: pullRequestsOf(raw?.pullRequests),
@@ -9479,6 +9504,19 @@ function buildScaleQualityServer(sdk, host) {
9479
9504
  async (args) => host.openPullRequest(String(args.title ?? ""), String(args.body ?? ""), typeof args.repoFullName === "string" ? args.repoFullName : void 0)
9480
9505
  )
9481
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
+ }
9482
9520
  return sdk.createSdkMcpServer({
9483
9521
  name: SQ_MCP_SERVER,
9484
9522
  version: "1.0.0",
@@ -9559,6 +9597,7 @@ var SQ_TOOL_LABELS = {
9559
9597
  list_repositories: { kind: "tool", label: "Listing the repositories of the session" },
9560
9598
  open_repository: { kind: "tool", label: "Opening a repository" },
9561
9599
  measure_change: { kind: "measure", label: "Measuring the change" },
9600
+ generate_image: { kind: "tool", label: "Creating an image" },
9562
9601
  open_pull_request: { kind: "tool", label: "Opening the pull request" }
9563
9602
  };
9564
9603
  function clip(s, n) {
@@ -10327,7 +10366,9 @@ function buildSystemAppend(c) {
10327
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).",
10328
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.",
10329
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.",
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."
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
+ "- To show the user a page you built (a prototype, a screen, a mockup, a chart, a small game, an HTML report), save it as an .html file in the workspace and write it as a Markdown image with that path, for example `![Battle HUD](prototype/hud.html)`: the screen runs the page in the conversation, with its code one click away. Stylesheets, scripts and images it links from the workspace go with it; scripts from a CDN load only from cdn.jsdelivr.net, cdnjs.cloudflare.com, unpkg.com or cdn.tailwindcss.com, and it calls no other address. Never say the conversation cannot show a page or a preview, and never offer to open it on the user's computer instead.",
10371
+ ...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."] : []
10331
10372
  ].join("\n");
10332
10373
  }
10333
10374
 
@@ -10971,6 +11012,7 @@ function editPreview(tool, input) {
10971
11012
  else if (tool === "MultiEdit" && Array.isArray(input.edits)) {
10972
11013
  for (const e of input.edits.slice(0, 5)) out2.push(...lines2(e?.old_string, "-"), ...lines2(e?.new_string, "+"), "");
10973
11014
  } else if (tool === "NotebookEdit") out2.push(...lines2(input.new_source, "+"));
11015
+ else if (tool.endsWith("__generate_image")) out2.push(...lines2(input.prompt, "+"));
10974
11016
  return out2.join("\n").slice(0, 4e3);
10975
11017
  }
10976
11018
  function turnOffering(raw, loaded) {
@@ -11888,7 +11930,8 @@ ${String(item.content)}` : String(item.content);
11888
11930
  })),
11889
11931
  onDemand: !!this.deps.clone && !this.local,
11890
11932
  layout: this.promptLayout(),
11891
- scopeBrief: this.scopeBrief
11933
+ scopeBrief: this.scopeBrief,
11934
+ imageGeneration: !!this.boot?.imageGeneration && !!this.deps.transport.generateImage
11892
11935
  });
11893
11936
  const conventions = await readConventions([...this.repos.values()].filter((r) => !r.dormant).flatMap((r) => {
11894
11937
  const label = r.repoFullName ?? r.localId ?? (0, import_path15.basename)(r.root);
@@ -13277,9 +13320,48 @@ ${patch}`;
13277
13320
  measureChange: (repo2) => this.measureChange(repo2),
13278
13321
  openPullRequest: (title, body, repo2) => this.openPullRequest(title, body, repo2),
13279
13322
  openRepository: (repo2) => this.openRepository(repo2),
13280
- listRepositories: async () => this.listRepositories()
13323
+ listRepositories: async () => this.listRepositories(),
13324
+ // Offered only when the API creates images for this session (an older API never gets the call).
13325
+ ...this.boot?.imageGeneration && this.deps.transport.generateImage ? { generateImage: (a) => this.generateImage(a) } : {}
13281
13326
  };
13282
13327
  }
13328
+ /**
13329
+ * generate_image (Erik, 20/10/2026: Code and Chat both create images): the API creates it as the "Create image" mode
13330
+ * does, for the session's owner and metered to that person; the engine saves it where the model asked, inside the
13331
+ * folder (the policy fenced the path and the person's mode already allowed the write), and the conversation shows it
13332
+ * from there. A refusal or failure goes back to the model to tell, and nothing is retried.
13333
+ */
13334
+ async generateImage(args) {
13335
+ const transport = this.deps.transport, root = this.deps.root;
13336
+ if (!transport.generateImage) return text("Creating images is not available in this session. Tell the user plainly.", true);
13337
+ const target = await resolveInside(root, args.filePath).catch(() => null);
13338
+ if (!target) return text(`generate_image saves only inside the workspace (${root}). Nothing was created.`, true);
13339
+ let reference;
13340
+ if (args.editPath) {
13341
+ try {
13342
+ const image = await readImageFile(root, await insideRelative(args.editPath, [root]), this.deps.privateDirs ?? []);
13343
+ 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);
13344
+ reference = { name: (0, import_path15.basename)(image.path), mimeType: image.mime, sizeBytes: image.size, dataBase64: image.data };
13345
+ } catch (e) {
13346
+ return text(`Could not read ${args.editPath} to change it (${e instanceof FileRequestError ? e.code : "UNREADABLE"}). Nothing was created.`, true);
13347
+ }
13348
+ }
13349
+ let created;
13350
+ try {
13351
+ created = await transport.generateImage({ prompt: args.prompt.slice(0, 4e3), ...reference ? { reference } : {}, idempotencyKey: (0, import_crypto8.randomUUID)() });
13352
+ } catch (e) {
13353
+ const why = e instanceof SessionGoneError ? `refused (${e.status})` : e instanceof TransportError && e.status ? `failed (${e.status})` : "failed";
13354
+ 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);
13355
+ }
13356
+ 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);
13357
+ const bytes = Buffer.from(created.dataBase64, "base64");
13358
+ if (!bytes.length || !imageMime(bytes)) return text("The image model returned something that is not an image. Nothing was saved.", true);
13359
+ await (0, import_promises14.mkdir)((0, import_path15.dirname)(target), { recursive: true });
13360
+ await (0, import_promises14.writeFile)(target, bytes);
13361
+ this.scheduleDiff();
13362
+ const rel = (0, import_path15.relative)(await (0, import_promises14.realpath)(root).catch(() => root), target).split(import_path15.sep).join("/");
13363
+ 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.`);
13364
+ }
13283
13365
  listRepositories() {
13284
13366
  const open2 = [...this.repos.values()];
13285
13367
  const data = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
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": {