@scalequality/cli 0.4.20 → 0.4.22
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 +173 -42
- 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": "46fe6b09a8c69a92a0f54cc420a3596c6f13b82177d3aec1d46456ae508a4517",
|
|
4
|
+
"sourceCommit": "2e2e401c42e98d17e1c0dd8286bdc6d15e44f77b"
|
|
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;
|
|
@@ -1931,6 +1970,8 @@ var TransportError = class extends Error {
|
|
|
1931
1970
|
};
|
|
1932
1971
|
|
|
1933
1972
|
// src/application/services/workspaceSandbox/HttpSessionTransport.ts
|
|
1973
|
+
var gzipAsync = (0, import_util2.promisify)(import_zlib.gzip);
|
|
1974
|
+
var gunzipAsync = (0, import_util2.promisify)(import_zlib.gunzip);
|
|
1934
1975
|
var GONE = [401, 403, 404, 410];
|
|
1935
1976
|
var HttpSessionTransport = class {
|
|
1936
1977
|
constructor(opts) {
|
|
@@ -1946,6 +1987,8 @@ var HttpSessionTransport = class {
|
|
|
1946
1987
|
gone;
|
|
1947
1988
|
async bootstrap() {
|
|
1948
1989
|
const raw = await this.get(`/bootstrap?features=${ENGINE_FEATURES.join(",")}`, this.opts.requestTimeoutMs ?? 3e4);
|
|
1990
|
+
const parts = checkpointPartsOf(raw?.checkpointParts);
|
|
1991
|
+
if (parts && typeof raw?.checkpointPatch !== "string") raw.checkpointPatch = await this.checkpointFromParts(parts);
|
|
1949
1992
|
return toSessionBootstrap(raw);
|
|
1950
1993
|
}
|
|
1951
1994
|
async pollCommands(waitSeconds, signal) {
|
|
@@ -1972,7 +2015,31 @@ var HttpSessionTransport = class {
|
|
|
1972
2015
|
return this.post("/pull-request", req, this.opts.toolTimeoutMs ?? 18e4);
|
|
1973
2016
|
}
|
|
1974
2017
|
async checkpoint(req) {
|
|
1975
|
-
|
|
2018
|
+
const timeout = this.opts.requestTimeoutMs ?? 6e4;
|
|
2019
|
+
if (Buffer.byteLength(JSON.stringify(req), "utf8") <= CHECKPOINT_INLINE_MAX_BYTES) {
|
|
2020
|
+
await this.post("/checkpoint", req, timeout);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const packed = await gzipAsync(Buffer.from(req.patch, "utf8"));
|
|
2024
|
+
const sha256 = (0, import_crypto2.createHash)("sha256").update(req.patch, "utf8").digest("hex");
|
|
2025
|
+
const count = Math.ceil(packed.length / CHECKPOINT_PART_BYTES);
|
|
2026
|
+
if (count > CHECKPOINT_MAX_PARTS) throw new TransportError(`checkpoint of ${packed.length} compressed bytes is over the limit`, 413, false);
|
|
2027
|
+
for (let index = 0; index < count; index++) {
|
|
2028
|
+
const data = packed.subarray(index * CHECKPOINT_PART_BYTES, (index + 1) * CHECKPOINT_PART_BYTES).toString("base64");
|
|
2029
|
+
await this.post("/checkpoint/parts", { sha256, index, count, data }, timeout);
|
|
2030
|
+
}
|
|
2031
|
+
await this.post("/checkpoint", { parts: { sha256, count, bytes: packed.length }, sdkSessionId: req.sdkSessionId }, timeout);
|
|
2032
|
+
}
|
|
2033
|
+
async checkpointFromParts(parts) {
|
|
2034
|
+
const chunks = [];
|
|
2035
|
+
for (let index = 0; index < parts.count; index++) {
|
|
2036
|
+
const r = await this.get(`/checkpoint/parts/${index}`, this.opts.requestTimeoutMs ?? 6e4);
|
|
2037
|
+
if (r?.sha256 !== parts.sha256 || typeof r.data !== "string") throw new TransportError("checkpoint part does not match", 502, false);
|
|
2038
|
+
chunks.push(Buffer.from(r.data, "base64"));
|
|
2039
|
+
}
|
|
2040
|
+
const patch = (await gunzipAsync(Buffer.concat(chunks)).catch(() => Buffer.alloc(0))).toString("utf8");
|
|
2041
|
+
if ((0, import_crypto2.createHash)("sha256").update(patch, "utf8").digest("hex") !== parts.sha256) throw new TransportError("checkpoint parts do not match", 502, false);
|
|
2042
|
+
return patch;
|
|
1976
2043
|
}
|
|
1977
2044
|
async openRepository(repoFullName) {
|
|
1978
2045
|
const raw = await this.post("/repositories/open", { repoFullName }, this.opts.requestTimeoutMs ?? 3e4);
|
|
@@ -2277,7 +2344,7 @@ var import_os3 = require("os");
|
|
|
2277
2344
|
var import_path5 = require("path");
|
|
2278
2345
|
|
|
2279
2346
|
// src/application/services/workspaceSandbox/localTracking.ts
|
|
2280
|
-
var
|
|
2347
|
+
var import_crypto3 = require("crypto");
|
|
2281
2348
|
var import_fs4 = require("fs");
|
|
2282
2349
|
var import_promises5 = require("fs/promises");
|
|
2283
2350
|
var import_os2 = require("os");
|
|
@@ -2305,7 +2372,7 @@ function gitTracker(root, base, prefix) {
|
|
|
2305
2372
|
},
|
|
2306
2373
|
snapshot: () => worktreeTreeId(root, scope),
|
|
2307
2374
|
restore: (tree) => restoreTree(root, tree, scope),
|
|
2308
|
-
checkpoint: () => checkpointPatch(root, base, scope),
|
|
2375
|
+
checkpoint: (opts) => checkpointPatch(root, base, scope, opts),
|
|
2309
2376
|
prFiles: (published) => filesForPullRequest(root, base, published, scope),
|
|
2310
2377
|
treeId: () => worktreeTreeId(root, scope)
|
|
2311
2378
|
};
|
|
@@ -2389,7 +2456,7 @@ async function mapLimit(items, n, fn) {
|
|
|
2389
2456
|
async function repoFingerprint(root) {
|
|
2390
2457
|
const head = (await git(["rev-parse", "-q", "--verify", "HEAD"], { cwd: root }).catch(() => "")).trim();
|
|
2391
2458
|
const status = await git(["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all"], { cwd: root, timeoutMs: 6e4 }).catch(() => "?");
|
|
2392
|
-
const h = (0,
|
|
2459
|
+
const h = (0, import_crypto3.createHash)("sha256").update(head).update("\0").update(status);
|
|
2393
2460
|
const recs = status.split("\0");
|
|
2394
2461
|
let n = 0;
|
|
2395
2462
|
for (let i = 0; i < recs.length && n < 2e3; i++) {
|
|
@@ -2561,10 +2628,10 @@ var LooseFileTracker = class {
|
|
|
2561
2628
|
}
|
|
2562
2629
|
await restoreTree(this.o.root, target, this.scope());
|
|
2563
2630
|
}
|
|
2564
|
-
async checkpoint() {
|
|
2631
|
+
async checkpoint(opts) {
|
|
2565
2632
|
await this.chain;
|
|
2566
2633
|
if (!this.entries.length) return "";
|
|
2567
|
-
return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
|
|
2634
|
+
return checkpointPatch(this.o.root, await this.baseTree(), this.scope(), opts);
|
|
2568
2635
|
}
|
|
2569
2636
|
async prFiles(published) {
|
|
2570
2637
|
await this.chain;
|
|
@@ -3530,7 +3597,7 @@ function serviceSlug(api) {
|
|
|
3530
3597
|
} catch {
|
|
3531
3598
|
}
|
|
3532
3599
|
const slug = host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "custom";
|
|
3533
|
-
return `${slug}-${(0,
|
|
3600
|
+
return `${slug}-${(0, import_crypto4.createHash)("sha256").update(api).digest("hex").slice(0, 6)}`;
|
|
3534
3601
|
}
|
|
3535
3602
|
function serviceNames(api) {
|
|
3536
3603
|
const slug = serviceSlug(api);
|
|
@@ -3669,7 +3736,7 @@ function installStableCopy(o) {
|
|
|
3669
3736
|
const bundle = (0, import_path6.join)(o.packageRoot, "dist", "connect.cjs");
|
|
3670
3737
|
const bin = (dir2) => (0, import_path6.join)(dir2, "node_modules", "@scalequality", "cli", "bin", "scalequality.mjs");
|
|
3671
3738
|
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,
|
|
3739
|
+
const sha = (0, import_crypto4.createHash)("sha256").update((0, import_fs5.readFileSync)(bundle)).digest("hex");
|
|
3673
3740
|
const base = (0, import_path6.join)(sqHome(o.home), "cli");
|
|
3674
3741
|
const dir = (0, import_path6.join)(base, pkg.version);
|
|
3675
3742
|
if (inside2(dir, o.packageRoot)) return { dir, bin: bin(dir), version: pkg.version, reused: true };
|
|
@@ -4817,7 +4884,7 @@ async function addFolder(credentials2, api, path, home) {
|
|
|
4817
4884
|
var import_child_process3 = require("child_process");
|
|
4818
4885
|
var import_promises14 = require("fs/promises");
|
|
4819
4886
|
var import_fs9 = require("fs");
|
|
4820
|
-
var
|
|
4887
|
+
var import_crypto8 = require("crypto");
|
|
4821
4888
|
var import_path15 = require("path");
|
|
4822
4889
|
|
|
4823
4890
|
// src/application/services/execution/LanguageAdapter.ts
|
|
@@ -4893,14 +4960,16 @@ function sandboxTestEnv() {
|
|
|
4893
4960
|
var ApprovalBroker = class {
|
|
4894
4961
|
waiting = /* @__PURE__ */ new Map();
|
|
4895
4962
|
early = /* @__PURE__ */ new Map();
|
|
4963
|
+
/** True when a call was waiting for this decision (it returns now); false when it is kept for a later wait. */
|
|
4896
4964
|
resolve(approvalId, decision) {
|
|
4897
4965
|
const w = this.waiting.get(approvalId);
|
|
4898
4966
|
if (w) {
|
|
4899
4967
|
this.waiting.delete(approvalId);
|
|
4900
4968
|
w(decision);
|
|
4901
|
-
|
|
4902
|
-
this.early.set(approvalId, decision);
|
|
4969
|
+
return true;
|
|
4903
4970
|
}
|
|
4971
|
+
this.early.set(approvalId, decision);
|
|
4972
|
+
return false;
|
|
4904
4973
|
}
|
|
4905
4974
|
wait(approvalId, signal) {
|
|
4906
4975
|
const ready = this.early.get(approvalId);
|
|
@@ -5259,7 +5328,7 @@ function limitInEngineText(text3) {
|
|
|
5259
5328
|
}
|
|
5260
5329
|
|
|
5261
5330
|
// src/application/services/workspaceSandbox/scalequalityTools.ts
|
|
5262
|
-
var
|
|
5331
|
+
var import_crypto5 = require("crypto");
|
|
5263
5332
|
|
|
5264
5333
|
// node_modules/zod/v3/external.js
|
|
5265
5334
|
var external_exports = {};
|
|
@@ -9379,7 +9448,7 @@ function buildScaleQualityServer(sdk, host) {
|
|
|
9379
9448
|
const tools = REMOTE_TOOLS.map(
|
|
9380
9449
|
(spec) => sdk.tool(spec.name, spec.description, spec.shape, async (args) => {
|
|
9381
9450
|
const a = { ...args ?? {} };
|
|
9382
|
-
if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0,
|
|
9451
|
+
if (spec.idempotent && typeof a.idempotencyKey !== "string") a.idempotencyKey = (0, import_crypto5.randomUUID)();
|
|
9383
9452
|
return callScaleQualityTool(host, spec.name, a);
|
|
9384
9453
|
})
|
|
9385
9454
|
);
|
|
@@ -9954,7 +10023,7 @@ function blockText(content) {
|
|
|
9954
10023
|
}
|
|
9955
10024
|
|
|
9956
10025
|
// src/application/services/workspaceSandbox/inputChannel.ts
|
|
9957
|
-
var
|
|
10026
|
+
var import_crypto6 = require("crypto");
|
|
9958
10027
|
var InputChannel = class {
|
|
9959
10028
|
buffer = [];
|
|
9960
10029
|
waiting = null;
|
|
@@ -10012,7 +10081,7 @@ var InputChannel = class {
|
|
|
10012
10081
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10013
10082
|
function messageUuid(id) {
|
|
10014
10083
|
if (UUID.test(id)) return id.toLowerCase();
|
|
10015
|
-
const h = (0,
|
|
10084
|
+
const h = (0, import_crypto6.createHash)("sha256").update(`sq-message:${id}`).digest("hex");
|
|
10016
10085
|
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)}`;
|
|
10017
10086
|
}
|
|
10018
10087
|
function userMessage(text3, images, uuid2, priority) {
|
|
@@ -10257,7 +10326,8 @@ function buildSystemAppend(c) {
|
|
|
10257
10326
|
"- Lead with the outcome in one or two sentences: what changed, what was found, or what you need from the user.",
|
|
10258
10327
|
"- The screen already shows every command you ran and every file you edited, with their output and diff. Do not list them again, and do not restate test counts or tool results the user can open; mention one only when it matters for what comes next (a test that still fails, a risk that remains).",
|
|
10259
10328
|
"- Keep it short, like a colleague reporting in a chat: a few lines for most turns, no headings, a short list only when there are several separate items. Never repeat the same fact in two places.",
|
|
10260
|
-
"- End with at most one question or one suggested next step, never a menu of options. When there is an obvious next step, say it in one line."
|
|
10329
|
+
"- 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."
|
|
10261
10331
|
].join("\n");
|
|
10262
10332
|
}
|
|
10263
10333
|
|
|
@@ -10343,11 +10413,11 @@ async function detectTestCommandFromManifests(dir) {
|
|
|
10343
10413
|
}
|
|
10344
10414
|
|
|
10345
10415
|
// src/application/services/workspaceSandbox/transcript.ts
|
|
10346
|
-
var
|
|
10416
|
+
var import_crypto7 = require("crypto");
|
|
10347
10417
|
var import_fs8 = require("fs");
|
|
10348
10418
|
var import_promises11 = require("fs/promises");
|
|
10349
10419
|
var import_path12 = require("path");
|
|
10350
|
-
var
|
|
10420
|
+
var import_zlib2 = require("zlib");
|
|
10351
10421
|
var TRANSCRIPT_CAPS = { maxRawBytes: 64 * 1024 * 1024, maxCompressedBytes: 4 * 1024 * 1024 };
|
|
10352
10422
|
function engineProjectDir(cwd) {
|
|
10353
10423
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -10386,10 +10456,10 @@ async function packTranscript(configDir, sessionId, redactor, caps = TRANSCRIPT_
|
|
|
10386
10456
|
}
|
|
10387
10457
|
const text3 = lines2.length ? `${lines2.join("\n")}
|
|
10388
10458
|
` : "";
|
|
10389
|
-
const zipped = (0,
|
|
10459
|
+
const zipped = (0, import_zlib2.gzipSync)(Buffer.from(text3, "utf8"), { level: 9 });
|
|
10390
10460
|
if (zipped.length > caps.maxCompressedBytes) return { tooLarge: true, bytes: zipped.length };
|
|
10391
10461
|
return {
|
|
10392
|
-
payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0,
|
|
10462
|
+
payload: { sdkSessionId: sessionId, encoding: "gzip-base64", data: zipped.toString("base64"), bytes: Buffer.byteLength(text3), sha256: (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") },
|
|
10393
10463
|
secretsRemoved: removed
|
|
10394
10464
|
};
|
|
10395
10465
|
}
|
|
@@ -10398,11 +10468,11 @@ async function restoreTranscript(configDir, cwd, t) {
|
|
|
10398
10468
|
if (await findTranscript(configDir, t.sdkSessionId)) return true;
|
|
10399
10469
|
let text3;
|
|
10400
10470
|
try {
|
|
10401
|
-
text3 = (0,
|
|
10471
|
+
text3 = (0, import_zlib2.gunzipSync)(Buffer.from(t.data, "base64"), { maxOutputLength: TRANSCRIPT_CAPS.maxRawBytes });
|
|
10402
10472
|
} catch {
|
|
10403
10473
|
return false;
|
|
10404
10474
|
}
|
|
10405
|
-
if (t.sha256 && (0,
|
|
10475
|
+
if (t.sha256 && (0, import_crypto7.createHash)("sha256").update(text3).digest("hex") !== t.sha256) return false;
|
|
10406
10476
|
const dir = (0, import_path12.join)(configDir, "projects", engineProjectDir(cwd));
|
|
10407
10477
|
await (0, import_promises11.mkdir)(dir, { recursive: true, mode: 448 });
|
|
10408
10478
|
const target = (0, import_path12.join)(dir, `${t.sdkSessionId}.jsonl`);
|
|
@@ -10421,7 +10491,7 @@ async function restoreTranscript(configDir, cwd, t) {
|
|
|
10421
10491
|
// src/application/services/workspaceSandbox/workspaceFiles.ts
|
|
10422
10492
|
var import_promises12 = require("fs/promises");
|
|
10423
10493
|
var import_path13 = require("path");
|
|
10424
|
-
var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3 };
|
|
10494
|
+
var FILE_CAPS = { maxEntries: 500, maxReadBytes: 256 * 1024, maxFileBytes: 20 * 1024 * 1024, maxLines: 5e3, maxImageBytes: 6 * 1024 * 1024 };
|
|
10425
10495
|
var FileRequestError = class extends Error {
|
|
10426
10496
|
constructor(code) {
|
|
10427
10497
|
super(code);
|
|
@@ -10502,6 +10572,48 @@ async function readTextFile(root, rel, range = {}, denied = []) {
|
|
|
10502
10572
|
}
|
|
10503
10573
|
return { path: clean, from, to: Math.max(from, Math.min(last, total)), totalLines: total, content, truncated, size: st.size };
|
|
10504
10574
|
}
|
|
10575
|
+
function imageMime(buf) {
|
|
10576
|
+
const at = (offset, hex) => buf.length >= offset + hex.length / 2 && buf.subarray(offset, offset + hex.length / 2).toString("hex") === hex;
|
|
10577
|
+
if (at(0, "89504e470d0a1a0a")) return "image/png";
|
|
10578
|
+
if (at(0, "ffd8ff")) return "image/jpeg";
|
|
10579
|
+
if (at(0, "474946383761") || at(0, "474946383961")) return "image/gif";
|
|
10580
|
+
if (at(0, "52494646") && at(8, "57454250")) return "image/webp";
|
|
10581
|
+
if (at(4, "66747970") && /^(avif|avis)$/.test(buf.subarray(8, 12).toString("latin1"))) return "image/avif";
|
|
10582
|
+
if (at(0, "424d")) return "image/bmp";
|
|
10583
|
+
const head = buf.subarray(0, 2048).toString("utf8").replace(/^\uFEFF/, "").trimStart();
|
|
10584
|
+
if (/^(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*(<!DOCTYPE svg[^>]*>\s*)?<svg[\s>]/i.test(head)) return "image/svg+xml";
|
|
10585
|
+
return null;
|
|
10586
|
+
}
|
|
10587
|
+
async function insideRelative(path, bases) {
|
|
10588
|
+
const clean = (path ?? "").trim().replace(/^file:\/\//i, "");
|
|
10589
|
+
if (!(0, import_path13.isAbsolute)(clean)) return clean.replace(/^\.\//, "");
|
|
10590
|
+
for (const base of bases) {
|
|
10591
|
+
for (const root of [(0, import_path13.resolve)(base), await (0, import_promises12.realpath)(base).catch(() => (0, import_path13.resolve)(base))]) {
|
|
10592
|
+
const rel = (0, import_path13.relative)(root, (0, import_path13.resolve)(clean));
|
|
10593
|
+
if (!rel) return "";
|
|
10594
|
+
if (rel !== ".." && !rel.startsWith(`..${import_path13.sep}`) && !(0, import_path13.isAbsolute)(rel)) return rel.split(import_path13.sep).join("/");
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
throw new FileRequestError("PATH_OUTSIDE_WORKSPACE");
|
|
10598
|
+
}
|
|
10599
|
+
async function readImageFile(root, rel, denied = []) {
|
|
10600
|
+
const { abs, rel: clean } = await fenced(root, rel, denied);
|
|
10601
|
+
const st = await (0, import_promises12.stat)(abs).catch(() => null);
|
|
10602
|
+
if (!st) throw new FileRequestError("NOT_FOUND");
|
|
10603
|
+
if (!st.isFile()) throw new FileRequestError("NOT_A_FILE");
|
|
10604
|
+
if (st.size > FILE_CAPS.maxImageBytes) throw new FileRequestError("FILE_TOO_LARGE");
|
|
10605
|
+
const fh = await (0, import_promises12.open)(abs, "r");
|
|
10606
|
+
let buf;
|
|
10607
|
+
try {
|
|
10608
|
+
buf = Buffer.alloc(st.size);
|
|
10609
|
+
await fh.read(buf, 0, st.size, 0);
|
|
10610
|
+
} finally {
|
|
10611
|
+
await fh.close();
|
|
10612
|
+
}
|
|
10613
|
+
const mime = imageMime(buf);
|
|
10614
|
+
if (!mime) throw new FileRequestError("NOT_AN_IMAGE");
|
|
10615
|
+
return { path: clean, mime, size: st.size, data: buf.toString("base64") };
|
|
10616
|
+
}
|
|
10505
10617
|
|
|
10506
10618
|
// src/application/services/workspaceSandbox/orgMcp.ts
|
|
10507
10619
|
var import_module = require("module");
|
|
@@ -11392,7 +11504,7 @@ var WorkspaceEngine = class {
|
|
|
11392
11504
|
}
|
|
11393
11505
|
case "approval": {
|
|
11394
11506
|
const parsed = ApprovalBroker.fromPayload(p);
|
|
11395
|
-
if (parsed
|
|
11507
|
+
if (parsed && this.broker.resolve(parsed.approvalId, parsed.decision) && parsed.decision.decision === "REJECT" && this.live) this.live.refused = true;
|
|
11396
11508
|
return;
|
|
11397
11509
|
}
|
|
11398
11510
|
case "stop":
|
|
@@ -11536,6 +11648,13 @@ var WorkspaceEngine = class {
|
|
|
11536
11648
|
const denied = this.deps.privateDirs ?? [];
|
|
11537
11649
|
const path = typeof p.path === "string" ? p.path : "";
|
|
11538
11650
|
const range = { from: typeof p.from === "number" ? p.from : void 0, to: typeof p.to === "number" ? p.to : void 0 };
|
|
11651
|
+
if (kind === "read_file" && p.as === "image") {
|
|
11652
|
+
const rel = await insideRelative(path, [repo2?.root ?? this.deps.root]);
|
|
11653
|
+
const image = await this.fencedFiles(repo2, rel, (root, r) => readImageFile(root, r, denied));
|
|
11654
|
+
const { data, ...meta } = image;
|
|
11655
|
+
await this.deps.transport.reply(commandId, { ok: true, result: { ...repoFullName ? { repoFullName } : {}, ...this.redactor.deep(meta), data } }).catch((e) => this.deps.log.warn("file request not answered", { error: e.message }));
|
|
11656
|
+
return;
|
|
11657
|
+
}
|
|
11539
11658
|
const result = kind === "list_files" ? await this.fencedFiles(repo2, path, (root, rel) => listFiles(root, rel, denied)) : await this.fencedFiles(repo2, path, (root, rel) => readTextFile(root, rel, range, denied));
|
|
11540
11659
|
reply = { ok: true, result: { ...repoFullName ? { repoFullName } : {}, ...result } };
|
|
11541
11660
|
} catch (e) {
|
|
@@ -11694,6 +11813,7 @@ ${String(item.content)}` : String(item.content);
|
|
|
11694
11813
|
}
|
|
11695
11814
|
if (moved && !live.foreground.size) return;
|
|
11696
11815
|
}
|
|
11816
|
+
if (live.refused) return;
|
|
11697
11817
|
await this.interruptTurn();
|
|
11698
11818
|
}
|
|
11699
11819
|
/**
|
|
@@ -11961,7 +12081,7 @@ ${String(item.content)}` : String(item.content);
|
|
|
11961
12081
|
if (files === "unavailable") this.error("REWIND_FAILED", { turnId });
|
|
11962
12082
|
this.emit({ type: "rewind", data: { turnId, files, conversation, ...untouched.length ? { untouched } : {} } });
|
|
11963
12083
|
await this.diffNow();
|
|
11964
|
-
await this.saveCheckpoint().catch(() =>
|
|
12084
|
+
await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
|
|
11965
12085
|
this.setState("READY", "REWOUND");
|
|
11966
12086
|
await this.sink.flush();
|
|
11967
12087
|
}
|
|
@@ -12385,6 +12505,7 @@ ${text3}`;
|
|
|
12385
12505
|
pushChain: Promise.resolve(),
|
|
12386
12506
|
foreground: /* @__PURE__ */ new Map(),
|
|
12387
12507
|
interrupted: false,
|
|
12508
|
+
refused: false,
|
|
12388
12509
|
stopGuard: null,
|
|
12389
12510
|
between: false,
|
|
12390
12511
|
snapshot: null,
|
|
@@ -12506,8 +12627,9 @@ ${text3}`;
|
|
|
12506
12627
|
}
|
|
12507
12628
|
} catch (e) {
|
|
12508
12629
|
if (!ac.signal.aborted) {
|
|
12509
|
-
|
|
12510
|
-
this.
|
|
12630
|
+
const ended = live.turnEnded || live.interrupted;
|
|
12631
|
+
this.deps.log.warn(ended ? "engine process ended after the turn" : "turn failed", { error: this.redactor.text(e.message) });
|
|
12632
|
+
if (!ended) this.error("TURN_FAILED");
|
|
12511
12633
|
}
|
|
12512
12634
|
} finally {
|
|
12513
12635
|
const stopped = ac.signal.aborted || live.interrupted;
|
|
@@ -12520,7 +12642,7 @@ ${text3}`;
|
|
|
12520
12642
|
this.turnWatch = null;
|
|
12521
12643
|
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;
|
|
12522
12644
|
await this.diffNow();
|
|
12523
|
-
await this.saveCheckpoint().catch(() =>
|
|
12645
|
+
await this.saveCheckpoint().catch((e) => this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) }));
|
|
12524
12646
|
this.setState("READY", stopped ? "STOPPED" : void 0);
|
|
12525
12647
|
await this.sink.flush();
|
|
12526
12648
|
}
|
|
@@ -12539,6 +12661,7 @@ ${text3}`;
|
|
|
12539
12661
|
if (ev.type === "message_start") live.timing.modelCall(typeof ev.message?.id === "string" ? ev.message.id : null);
|
|
12540
12662
|
} else if (f.type === "assistant") {
|
|
12541
12663
|
live.timing.modelEvent();
|
|
12664
|
+
live.refused = false;
|
|
12542
12665
|
const msg = f.message ?? {};
|
|
12543
12666
|
if (!f.error && typeof msg.model === "string") this.checkModel(live, msg.model);
|
|
12544
12667
|
live.timing.modelCall(typeof msg.id === "string" ? msg.id : null);
|
|
@@ -12614,6 +12737,7 @@ ${text3}`;
|
|
|
12614
12737
|
if (snap) this.recordCheckpoint(turnId, snap);
|
|
12615
12738
|
items.forEach((item, i) => this.messageStatus(String(item.turnId), i === 0 ? "started" : "absorbed"));
|
|
12616
12739
|
live.interrupted = false;
|
|
12740
|
+
live.refused = false;
|
|
12617
12741
|
live.turnId = turnId;
|
|
12618
12742
|
live.fellBack = false;
|
|
12619
12743
|
live.headUuid = members[0];
|
|
@@ -12903,9 +13027,18 @@ ${text3}`;
|
|
|
12903
13027
|
* The checkpoint: every open repository's change against its base, plus the
|
|
12904
13028
|
* saved changes of repositories not open in this run, as one map. In the
|
|
12905
13029
|
* cloud workspace the engine's transcript goes with it (v4), so a resumed
|
|
12906
|
-
* session continues the same conversation.
|
|
13030
|
+
* session continues the same conversation. On the person's computer the
|
|
13031
|
+
* folder keeps its binary and very large files, which the map only names.
|
|
13032
|
+
* The transcript is kept even when the change could not be.
|
|
12907
13033
|
*/
|
|
12908
13034
|
async saveCheckpoint() {
|
|
13035
|
+
try {
|
|
13036
|
+
await this.saveChange();
|
|
13037
|
+
} finally {
|
|
13038
|
+
await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
|
|
13039
|
+
}
|
|
13040
|
+
}
|
|
13041
|
+
async saveChange() {
|
|
12909
13042
|
const map = new Map(this.pendingCheckpoints);
|
|
12910
13043
|
for (const repo2 of this.repos.values()) {
|
|
12911
13044
|
const key2 = repo2.repoFullName ?? repo2.localId ?? LOCAL_FOLDER_KEY;
|
|
@@ -12913,7 +13046,7 @@ ${text3}`;
|
|
|
12913
13046
|
if (repo2.unsaved) map.set(key2, repo2.unsaved);
|
|
12914
13047
|
continue;
|
|
12915
13048
|
}
|
|
12916
|
-
const patch2 = await repo2.tracker.checkpoint();
|
|
13049
|
+
const patch2 = await repo2.tracker.checkpoint(this.local ? { keepInFolder: true } : void 0);
|
|
12917
13050
|
if (patch2) map.set(key2, patch2);
|
|
12918
13051
|
else if (repo2.unsaved) map.set(key2, repo2.unsaved);
|
|
12919
13052
|
else map.delete(key2);
|
|
@@ -12925,7 +13058,6 @@ ${patch}`;
|
|
|
12925
13058
|
await this.deps.transport.checkpoint({ patch, sdkSessionId: this.sdkSessionId });
|
|
12926
13059
|
this.lastCheckpoint = key;
|
|
12927
13060
|
}
|
|
12928
|
-
await this.saveTranscript().catch((e) => this.deps.log.warn("transcript not kept", { error: e.message }));
|
|
12929
13061
|
}
|
|
12930
13062
|
/** The engine conversation, without secrets, kept by the API (only when it changed). */
|
|
12931
13063
|
async saveTranscript() {
|
|
@@ -12970,8 +13102,7 @@ ${patch}`;
|
|
|
12970
13102
|
if (running) await Promise.race([running, new Promise((r) => setTimeout(r, 2e4))]);
|
|
12971
13103
|
if (opts.checkpoint) {
|
|
12972
13104
|
await this.saveCheckpoint().catch((e) => {
|
|
12973
|
-
this.deps.log.warn("checkpoint failed", { error: e.message });
|
|
12974
|
-
this.error("CHECKPOINT_FAILED");
|
|
13105
|
+
this.deps.log.warn("checkpoint failed", { error: this.redactor.text(e.message) });
|
|
12975
13106
|
});
|
|
12976
13107
|
}
|
|
12977
13108
|
if (reason === "shutdown" && this.state && this.state !== "FAILED") this.setState("PAUSED", pausedCode);
|
|
@@ -13003,7 +13134,7 @@ ${patch}`;
|
|
|
13003
13134
|
this.pendingCheckpoints.delete(repo2.localId);
|
|
13004
13135
|
repo2.dormant = !repo2.unsaved;
|
|
13005
13136
|
}
|
|
13006
|
-
const stateDir = this.deps.stateDir ?? (0, import_path15.join)(this.deps.scratch, `folder-${(0,
|
|
13137
|
+
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)}`);
|
|
13007
13138
|
const tracker = new LooseFileTracker({ root: local.root, stateDir, excluded: () => local.repos.map((r) => r.root) });
|
|
13008
13139
|
const tracked = local.gitAvailable && await tracker.init().then(() => true, (e) => {
|
|
13009
13140
|
this.deps.log.warn("loose files not tracked", { error: e.message });
|
|
@@ -13620,7 +13751,7 @@ function buildQueryOptions(o) {
|
|
|
13620
13751
|
}
|
|
13621
13752
|
function spawnKey(i) {
|
|
13622
13753
|
const { orgMcpServers, ...data } = i;
|
|
13623
|
-
return (0,
|
|
13754
|
+
return (0, import_crypto8.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
|
|
13624
13755
|
}
|
|
13625
13756
|
function timedGate(gate, took) {
|
|
13626
13757
|
const timed = async (run2) => {
|
|
@@ -13646,7 +13777,7 @@ async function hasLocalTranscript(configDir, sessionId) {
|
|
|
13646
13777
|
}
|
|
13647
13778
|
|
|
13648
13779
|
// src/application/services/workspaceSandbox/selfUpdate.ts
|
|
13649
|
-
var
|
|
13780
|
+
var import_crypto9 = require("crypto");
|
|
13650
13781
|
var import_fs10 = require("fs");
|
|
13651
13782
|
var import_path16 = require("path");
|
|
13652
13783
|
var REGISTRY_LATEST = "https://registry.npmjs.org/@scalequality%2fcli/latest";
|
|
@@ -13829,7 +13960,7 @@ async function installVersion(o) {
|
|
|
13829
13960
|
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}.`);
|
|
13830
13961
|
const build = readJson2((0, import_path16.join)(root, "dist", "connect.build.json"));
|
|
13831
13962
|
const bundle = (0, import_path16.join)(root, "dist", "connect.cjs");
|
|
13832
|
-
const sha = (0, import_fs10.existsSync)(bundle) ? (0,
|
|
13963
|
+
const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto9.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
|
|
13833
13964
|
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.`);
|
|
13834
13965
|
let copy;
|
|
13835
13966
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalequality/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
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": {
|