@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.
- package/dist/connect.build.json +2 -2
- package/dist/connect.cjs +86 -4
- package/package.json +1 -1
package/dist/connect.build.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sdkVersion": "0.3.281",
|
|
3
|
-
"sha256": "
|
|
4
|
-
"sourceCommit": "
|
|
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
|
|
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 ``: 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 ``: 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 ``: 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  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.
|
|
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": {
|