@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.
- package/dist/connect.build.json +2 -2
- package/dist/connect.cjs +194 -39
- 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": "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
|
|
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
|
-
|
|
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
|
|
862
|
-
|
|
863
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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 ``: 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
|
+
...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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(() =>
|
|
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(() =>
|
|
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,
|
|
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  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,
|
|
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
|
|
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,
|
|
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.
|
|
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": {
|