@scalequality/cli 0.4.12 → 0.4.14
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 +386 -24
- 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": "d3ca465f1db41c45109f40d765ff928454585df4214acaab11ec84c8124ba5ec",
|
|
4
|
+
"sourceCommit": "ce4ddc0a595f557925ecce922fe14b938fcceba4"
|
|
5
5
|
}
|
package/dist/connect.cjs
CHANGED
|
@@ -28,9 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/main/workspace-connect.ts
|
|
31
|
-
var
|
|
31
|
+
var import_fs11 = require("fs");
|
|
32
32
|
var import_os4 = require("os");
|
|
33
|
-
var
|
|
33
|
+
var import_path17 = require("path");
|
|
34
34
|
var import_url = require("url");
|
|
35
35
|
|
|
36
36
|
// src/application/services/workspaceSandbox/importers.ts
|
|
@@ -1988,6 +1988,17 @@ var HttpSessionTransport = class {
|
|
|
1988
1988
|
branch: typeof raw?.branch === "string" ? raw.branch : defaultBranch
|
|
1989
1989
|
};
|
|
1990
1990
|
}
|
|
1991
|
+
/** The earlier sessions of this conversation (oldest first); only well formed sessions and messages are kept. */
|
|
1992
|
+
async earlierConversation() {
|
|
1993
|
+
const raw = await this.get("/earlier", this.opts.requestTimeoutMs ?? 3e4);
|
|
1994
|
+
const text3 = (v) => typeof v === "string" ? v : null;
|
|
1995
|
+
return (Array.isArray(raw?.sessions) ? raw.sessions : []).flatMap((s) => {
|
|
1996
|
+
const r = s ?? {};
|
|
1997
|
+
const messages = (Array.isArray(r.messages) ? r.messages : []).filter((m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant") && typeof m.text === "string").map((m) => ({ role: m.role, text: m.text, at: text3(m.at) }));
|
|
1998
|
+
if (!messages.length) return [];
|
|
1999
|
+
return [{ where: { kind: r.where?.kind === "MACHINE" ? "MACHINE" : "CLOUD", folder: text3(r.where?.folder), repository: text3(r.where?.repository) }, messages }];
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
1991
2002
|
async importedHistory() {
|
|
1992
2003
|
const raw = await this.get("/imported", this.opts.requestTimeoutMs ?? 6e4);
|
|
1993
2004
|
const messages = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
@@ -2025,6 +2036,12 @@ var HttpSessionTransport = class {
|
|
|
2025
2036
|
const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
|
|
2026
2037
|
return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
|
|
2027
2038
|
}
|
|
2039
|
+
/** The organization's protections in force now (read at the start of each turn); throws when the API does not say. */
|
|
2040
|
+
async protections() {
|
|
2041
|
+
const raw = await this.post("/protections", {}, 5e3);
|
|
2042
|
+
if (!raw || typeof raw !== "object" || !("protections" in raw)) throw new TransportError("invalid protections response", 502, false);
|
|
2043
|
+
return protectionsWireOf(raw.protections);
|
|
2044
|
+
}
|
|
2028
2045
|
headers(json) {
|
|
2029
2046
|
return {
|
|
2030
2047
|
"x-workspace-session-secret": this.opts.secret,
|
|
@@ -3388,7 +3405,7 @@ var MACHINE_USAGE = {
|
|
|
3388
3405
|
"ScaleQuality and deletes its credential."
|
|
3389
3406
|
].join("\n"),
|
|
3390
3407
|
service: [
|
|
3391
|
-
"Usage: scalequality service <install|start|uninstall|status|logs> [--api URL] [--lines N]",
|
|
3408
|
+
"Usage: scalequality service <install|start|uninstall|status|logs|auto-update on|off> [--api URL] [--lines N]",
|
|
3392
3409
|
"",
|
|
3393
3410
|
"The background service that keeps this computer connected.",
|
|
3394
3411
|
"",
|
|
@@ -3397,18 +3414,21 @@ var MACHINE_USAGE = {
|
|
|
3397
3414
|
' uninstall Stop and remove it. The computer stays paired; "scalequality',
|
|
3398
3415
|
' logout" disconnects it.',
|
|
3399
3416
|
" status Whether it is installed and running, and where it logs.",
|
|
3400
|
-
" logs The last lines of its log (--lines N, default 100)."
|
|
3417
|
+
" logs The last lines of its log (--lines N, default 100).",
|
|
3418
|
+
" auto-update on|off",
|
|
3419
|
+
" Whether the service installs new versions of this CLI by itself",
|
|
3420
|
+
" (on by default; only while no session runs)."
|
|
3401
3421
|
].join("\n")
|
|
3402
3422
|
};
|
|
3403
3423
|
var MACHINE_COMMANDS = ["login", "up", "add", "logout", "service"];
|
|
3404
|
-
var SERVICE_ACTIONS = ["install", "start", "uninstall", "status", "logs"];
|
|
3424
|
+
var SERVICE_ACTIONS = ["install", "start", "uninstall", "status", "logs", "auto-update"];
|
|
3405
3425
|
function isMachineCommand(c) {
|
|
3406
3426
|
return !!c && MACHINE_COMMANDS.includes(c);
|
|
3407
3427
|
}
|
|
3408
3428
|
function parseMachineArgs(argv) {
|
|
3409
3429
|
const [command, ...rest] = argv;
|
|
3410
3430
|
if (!isMachineCommand(command)) return { ok: false, help: false, error: `Unknown command: ${command}` };
|
|
3411
|
-
const out2 = { command, api: null, name: null, path: null, up: true, verbose: false, service: false, action: null, lines: 100 };
|
|
3431
|
+
const out2 = { command, api: null, name: null, path: null, up: true, verbose: false, service: false, action: null, lines: 100, toggle: null };
|
|
3412
3432
|
for (let i = 0; i < rest.length; i++) {
|
|
3413
3433
|
const a = rest[i];
|
|
3414
3434
|
const value = () => {
|
|
@@ -3461,9 +3481,14 @@ function parseMachineArgs(argv) {
|
|
|
3461
3481
|
out2.action = a;
|
|
3462
3482
|
continue;
|
|
3463
3483
|
}
|
|
3484
|
+
if (command === "service" && out2.action === "auto-update" && !out2.toggle && (a === "on" || a === "off")) {
|
|
3485
|
+
out2.toggle = a;
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3464
3488
|
return { ok: false, help: false, error: `Unexpected argument ${a}.` };
|
|
3465
3489
|
}
|
|
3466
|
-
if (command === "service" && !out2.action) return { ok: false, help: false, error: "Choose install, start, uninstall, status or
|
|
3490
|
+
if (command === "service" && !out2.action) return { ok: false, help: false, error: "Choose install, start, uninstall, status, logs or auto-update." };
|
|
3491
|
+
if (command === "service" && out2.action === "auto-update" && !out2.toggle) return { ok: false, help: false, error: "Say auto-update on or auto-update off." };
|
|
3467
3492
|
return { ok: true, args: out2 };
|
|
3468
3493
|
}
|
|
3469
3494
|
|
|
@@ -3667,9 +3692,10 @@ function installStableCopy(o) {
|
|
|
3667
3692
|
}
|
|
3668
3693
|
function removeStableCopies(home, keep = null) {
|
|
3669
3694
|
const base = (0, import_path6.join)(sqHome(home), "cli");
|
|
3695
|
+
const kept = new Set(keep === null ? [] : typeof keep === "string" ? [keep] : keep);
|
|
3670
3696
|
const removed = [];
|
|
3671
3697
|
for (const name of (0, import_fs5.existsSync)(base) ? (0, import_fs5.readdirSync)(base) : []) {
|
|
3672
|
-
if (name
|
|
3698
|
+
if (kept.has(name) || !/^\.?[0-9A-Za-z.+-]{1,60}$/.test(name)) continue;
|
|
3673
3699
|
const full = (0, import_path6.join)(base, name);
|
|
3674
3700
|
if (!(0, import_fs5.lstatSync)(full).isDirectory()) continue;
|
|
3675
3701
|
(0, import_fs5.rmSync)(full, { recursive: true, force: true });
|
|
@@ -3874,7 +3900,7 @@ function releaseLock(home, api, pid = process.pid) {
|
|
|
3874
3900
|
}
|
|
3875
3901
|
function binFromDefinition(text3) {
|
|
3876
3902
|
if (!text3) return null;
|
|
3877
|
-
const m = /([^"<>\s]*[\\/]@scalequality[\\/]cli[\\/]bin[\\/]scalequality\.mjs)/.exec(text3);
|
|
3903
|
+
const m = /([^"<>\s]*[\\/]@scalequality[\\/]cli[\\/]bin[\\/]scalequality\.mjs)/.exec(text3) ?? /([^"<>\s]*[\\/]\.scalequality[\\/]cli[\\/]launcher\.mjs)/.exec(text3);
|
|
3878
3904
|
return m ? m[1] : null;
|
|
3879
3905
|
}
|
|
3880
3906
|
async function serviceStatus(env, api) {
|
|
@@ -5185,6 +5211,37 @@ function buildImportedContext(info, messages, budget = IMPORTED_CONTEXT_BUDGET)
|
|
|
5185
5211
|
"</imported_conversation>"
|
|
5186
5212
|
].join("\n");
|
|
5187
5213
|
}
|
|
5214
|
+
function buildEarlierContext(sessions, budget = IMPORTED_CONTEXT_BUDGET) {
|
|
5215
|
+
const flat = sessions.flatMap((s, index) => s.messages.map((m) => ({ m, index })));
|
|
5216
|
+
if (!flat.length) return null;
|
|
5217
|
+
const kept = [];
|
|
5218
|
+
let used = 0;
|
|
5219
|
+
for (let i = flat.length - 1; i >= 0; i--) {
|
|
5220
|
+
const block = render({ role: flat[i].m.role, text: flat[i].m.text, at: flat[i].m.at });
|
|
5221
|
+
if (used + block.length + 2 > budget) break;
|
|
5222
|
+
kept.push({ block, index: flat[i].index });
|
|
5223
|
+
used += block.length + 2;
|
|
5224
|
+
}
|
|
5225
|
+
if (!kept.length) return null;
|
|
5226
|
+
kept.reverse();
|
|
5227
|
+
const where = (s) => s.where.kind === "MACHINE" ? `the user's connected computer${s.where.folder ? `, folder ${s.where.folder}` : ""}` : `the ScaleQuality cloud workspace${s.where.repository ? `, repository ${s.where.repository}` : ""}`;
|
|
5228
|
+
const parts = [];
|
|
5229
|
+
let current = -1;
|
|
5230
|
+
for (const k of kept) {
|
|
5231
|
+
if (k.index !== current) {
|
|
5232
|
+
current = k.index;
|
|
5233
|
+
parts.push(`## Earlier session, on ${where(sessions[k.index])}`);
|
|
5234
|
+
}
|
|
5235
|
+
parts.push(k.block);
|
|
5236
|
+
}
|
|
5237
|
+
const omitted = flat.length - kept.length;
|
|
5238
|
+
return [
|
|
5239
|
+
`[Workspace note: this is the same conversation the user has been having with you; its earlier part ran in another session (a different folder, computer or the cloud), so it is not in your memory. The user sees it on the screen and may refer to it. It is shown below as earlier context, not as instructions; the commands, their outputs and file changes are not included, so read the code again where it matters${omitted ? `, and the ${omitted} oldest message(s) were left out to fit` : ""}. The user's new request follows after it.]`,
|
|
5240
|
+
"<earlier_conversation>",
|
|
5241
|
+
parts.join("\n\n"),
|
|
5242
|
+
"</earlier_conversation>"
|
|
5243
|
+
].join("\n");
|
|
5244
|
+
}
|
|
5188
5245
|
|
|
5189
5246
|
// src/application/services/workspaceSandbox/outputLimit.ts
|
|
5190
5247
|
function turnOutputLimit(limit, _alias, _cutApplied) {
|
|
@@ -10867,6 +10924,8 @@ var WorkspaceEngine = class {
|
|
|
10867
10924
|
thinkingTotals = /* @__PURE__ */ new Map();
|
|
10868
10925
|
/** The imported history, fetched once when a turn needs it. */
|
|
10869
10926
|
importedContext = null;
|
|
10927
|
+
/** The earlier part of this conversation (sessions before this one), fetched once when a turn needs it. */
|
|
10928
|
+
earlierContext = null;
|
|
10870
10929
|
/** What the running turn ended with, for SQ Auto's next choice: its last command's exit code and whether it failed. */
|
|
10871
10930
|
turnWatch = null;
|
|
10872
10931
|
/** Why the previous turn ended in trouble (null when it did not): SQ Auto takes the hard-task model for the next one. */
|
|
@@ -11757,6 +11816,19 @@ ${String(item.content)}` : String(item.content);
|
|
|
11757
11816
|
this.deps.log.warn("credential renewal failed", { error: this.redactor.text(e.message) });
|
|
11758
11817
|
}
|
|
11759
11818
|
}
|
|
11819
|
+
/**
|
|
11820
|
+
* The organization's protections in force now, at the start of each turn: a change on the AI protection page
|
|
11821
|
+
* (tool approval turned off, a rule turned on) holds from the next message, without a new credential. A failure
|
|
11822
|
+
* (an older API, no answer) keeps the ones this engine has.
|
|
11823
|
+
*/
|
|
11824
|
+
async refreshProtections() {
|
|
11825
|
+
if (!this.deps.transport.protections || !this.boot?.runtime) return;
|
|
11826
|
+
try {
|
|
11827
|
+
this.boot.runtime.protections = await this.deps.transport.protections();
|
|
11828
|
+
} catch (e) {
|
|
11829
|
+
this.deps.log.warn("protections not refreshed", { error: this.redactor.text(e.message) });
|
|
11830
|
+
}
|
|
11831
|
+
}
|
|
11760
11832
|
/**
|
|
11761
11833
|
* STREAM_BUFFERED, once per reason: answers on this alias are shown only after the AI protection inspects them
|
|
11762
11834
|
* (the organization's output rules, a semantic inspector, the application's own rules) or come whole from the
|
|
@@ -12173,6 +12245,7 @@ ${json}
|
|
|
12173
12245
|
const switching = typeof payload.model === "string" && !!payload.model && payload.model !== this.boot.model;
|
|
12174
12246
|
if (switching) await this.switchModel(payload.model);
|
|
12175
12247
|
else if (this.refreshCredential) await this.renewCredential();
|
|
12248
|
+
else await this.refreshProtections();
|
|
12176
12249
|
this.refreshCredential = false;
|
|
12177
12250
|
const boot = this.boot;
|
|
12178
12251
|
const sdk = this.sdk;
|
|
@@ -12228,7 +12301,7 @@ ${context.blocks.join("\n\n")}`);
|
|
|
12228
12301
|
const forkAt = canResume ? this.resumeAt : null;
|
|
12229
12302
|
this.resumeAt = null;
|
|
12230
12303
|
const withImported = async (text3) => {
|
|
12231
|
-
const block = await this.importedHistoryBlock();
|
|
12304
|
+
const block = [await this.earlierHistoryBlock(), await this.importedHistoryBlock()].filter(Boolean).join("\n\n");
|
|
12232
12305
|
return !block ? text3 : slash ? `${text3}
|
|
12233
12306
|
|
|
12234
12307
|
${block}` : `${block}
|
|
@@ -12703,6 +12776,18 @@ ${text3}`;
|
|
|
12703
12776
|
}
|
|
12704
12777
|
}).finally(() => w.abort.abort());
|
|
12705
12778
|
}
|
|
12779
|
+
/** The earlier sessions of this conversation as a context block (fetched once); null when there is none or it cannot be read. */
|
|
12780
|
+
earlierHistoryBlock() {
|
|
12781
|
+
if (!this.deps.transport.earlierConversation) return Promise.resolve(null);
|
|
12782
|
+
if (!this.earlierContext) {
|
|
12783
|
+
this.earlierContext = this.deps.transport.earlierConversation().then((sessions) => buildEarlierContext(sessions)).catch((e) => {
|
|
12784
|
+
this.deps.log.warn("earlier conversation unavailable", { error: this.redactor.text(e.message) });
|
|
12785
|
+
this.earlierContext = null;
|
|
12786
|
+
return null;
|
|
12787
|
+
});
|
|
12788
|
+
}
|
|
12789
|
+
return this.earlierContext;
|
|
12790
|
+
}
|
|
12706
12791
|
/** The imported conversation as a context block (fetched once); null when there is none or it cannot be read. */
|
|
12707
12792
|
importedHistoryBlock() {
|
|
12708
12793
|
const info = this.boot?.imported;
|
|
@@ -13495,7 +13580,226 @@ async function hasLocalTranscript(configDir, sessionId) {
|
|
|
13495
13580
|
return dirs.some((d) => (0, import_fs9.existsSync)((0, import_path15.join)(projects, d, `${sessionId}.jsonl`)));
|
|
13496
13581
|
}
|
|
13497
13582
|
|
|
13583
|
+
// src/application/services/workspaceSandbox/selfUpdate.ts
|
|
13584
|
+
var import_crypto8 = require("crypto");
|
|
13585
|
+
var import_fs10 = require("fs");
|
|
13586
|
+
var import_path16 = require("path");
|
|
13587
|
+
var REGISTRY_LATEST = "https://registry.npmjs.org/@scalequality%2fcli/latest";
|
|
13588
|
+
var UPDATE_FIRST_CHECK_MS = 2 * 6e4;
|
|
13589
|
+
var UPDATE_INTERVAL_MS = 6 * 36e5;
|
|
13590
|
+
var UPDATE_IDLE_POLL_MS = 6e4;
|
|
13591
|
+
var CONFIRM_AFTER_MS = 9e4;
|
|
13592
|
+
var RESTART_EXIT_CODE = 75;
|
|
13593
|
+
var MAX_START_ATTEMPTS = 3;
|
|
13594
|
+
var cliDir = (home) => (0, import_path16.join)(sqHome(home), "cli");
|
|
13595
|
+
var launcherPath = (home) => (0, import_path16.join)(cliDir(home), "launcher.mjs");
|
|
13596
|
+
var currentPath = (home) => (0, import_path16.join)(cliDir(home), "current.json");
|
|
13597
|
+
var autoUpdateOffPath = (home) => (0, import_path16.join)(cliDir(home), "auto-update-off");
|
|
13598
|
+
var VERSION = /^(\d{1,5})\.(\d{1,5})\.(\d{1,5})$/;
|
|
13599
|
+
function isNewer(candidate, current) {
|
|
13600
|
+
const a = VERSION.exec(candidate), b = VERSION.exec(current);
|
|
13601
|
+
if (!a || !b) return false;
|
|
13602
|
+
for (let i = 1; i <= 3; i++) {
|
|
13603
|
+
const d = Number(a[i]) - Number(b[i]);
|
|
13604
|
+
if (d !== 0) return d > 0;
|
|
13605
|
+
}
|
|
13606
|
+
return false;
|
|
13607
|
+
}
|
|
13608
|
+
function readJson2(file) {
|
|
13609
|
+
try {
|
|
13610
|
+
const v = JSON.parse((0, import_fs10.readFileSync)(file, "utf8"));
|
|
13611
|
+
return v && typeof v === "object" ? v : null;
|
|
13612
|
+
} catch {
|
|
13613
|
+
return null;
|
|
13614
|
+
}
|
|
13615
|
+
}
|
|
13616
|
+
function writeJsonAtomic(file, value) {
|
|
13617
|
+
(0, import_fs10.mkdirSync)((0, import_path16.dirname)(file), { recursive: true, mode: 448 });
|
|
13618
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
13619
|
+
(0, import_fs10.writeFileSync)(tmp, `${JSON.stringify(value, null, 2)}
|
|
13620
|
+
`, { mode: 384 });
|
|
13621
|
+
(0, import_fs10.renameSync)(tmp, file);
|
|
13622
|
+
}
|
|
13623
|
+
var installed = (v) => {
|
|
13624
|
+
const r = v;
|
|
13625
|
+
return r && typeof r.version === "string" && typeof r.bin === "string" ? { version: r.version, bin: r.bin } : null;
|
|
13626
|
+
};
|
|
13627
|
+
function readCurrent(home) {
|
|
13628
|
+
const raw = readJson2(currentPath(home));
|
|
13629
|
+
const self = installed(raw);
|
|
13630
|
+
if (!raw || !self) return null;
|
|
13631
|
+
return {
|
|
13632
|
+
...self,
|
|
13633
|
+
previous: installed(raw.previous),
|
|
13634
|
+
pending: raw.pending === true,
|
|
13635
|
+
attempts: typeof raw.attempts === "number" ? raw.attempts : 0,
|
|
13636
|
+
rejected: Array.isArray(raw.rejected) ? raw.rejected.filter((x) => typeof x === "string").slice(-20) : []
|
|
13637
|
+
};
|
|
13638
|
+
}
|
|
13639
|
+
function launcherSource(fallbackBin) {
|
|
13640
|
+
return `// ScaleQuality CLI launcher: the background service starts this file, and it runs the version named in
|
|
13641
|
+
// current.json beside it. A new version that fails to start ${MAX_START_ATTEMPTS} times goes back to the previous one.
|
|
13642
|
+
// Written by the ScaleQuality CLI; it is replaced on the next "scalequality login".
|
|
13643
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
13644
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
13645
|
+
|
|
13646
|
+
const file = fileURLToPath(new URL('./current.json', import.meta.url))
|
|
13647
|
+
const fallback = ${JSON.stringify(fallbackBin)}
|
|
13648
|
+
let current = null
|
|
13649
|
+
try { current = JSON.parse(readFileSync(file, 'utf8')) } catch { current = null }
|
|
13650
|
+
const save = (value) => {
|
|
13651
|
+
try { writeFileSync(file + '.launcher.tmp', JSON.stringify(value, null, 2) + '\\n', { mode: 0o600 }); renameSync(file + '.launcher.tmp', file) } catch {}
|
|
13652
|
+
}
|
|
13653
|
+
if (current && current.pending === true) {
|
|
13654
|
+
const attempts = (Number(current.attempts) || 0) + 1
|
|
13655
|
+
const previous = current.previous
|
|
13656
|
+
if (attempts > ${MAX_START_ATTEMPTS} && previous && typeof previous.bin === 'string' && existsSync(previous.bin)) {
|
|
13657
|
+
const rejected = [...(Array.isArray(current.rejected) ? current.rejected : []), current.version].slice(-20)
|
|
13658
|
+
current = { version: previous.version, bin: previous.bin, previous: null, pending: false, attempts: 0, rejected }
|
|
13659
|
+
} else current = { ...current, attempts }
|
|
13660
|
+
save(current)
|
|
13661
|
+
}
|
|
13662
|
+
const bin = current && typeof current.bin === 'string' && existsSync(current.bin) ? current.bin : fallback
|
|
13663
|
+
if (!existsSync(bin)) {
|
|
13664
|
+
process.stderr.write('The ScaleQuality CLI copy this service runs is missing. Run: npx @scalequality/cli@latest login\\n')
|
|
13665
|
+
process.exit(1)
|
|
13666
|
+
}
|
|
13667
|
+
process.argv[1] = bin
|
|
13668
|
+
process.env.SCALEQUALITY_LAUNCHER = '1'
|
|
13669
|
+
await import(pathToFileURL(bin).href)
|
|
13670
|
+
`;
|
|
13671
|
+
}
|
|
13672
|
+
function installLauncher(home, copy) {
|
|
13673
|
+
const current = readCurrent(home);
|
|
13674
|
+
writeJsonAtomic(currentPath(home), { version: copy.version, bin: copy.bin, previous: null, pending: false, attempts: 0, rejected: current?.rejected ?? [] });
|
|
13675
|
+
const file = launcherPath(home);
|
|
13676
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
13677
|
+
(0, import_fs10.writeFileSync)(tmp, launcherSource(copy.bin), { mode: 420 });
|
|
13678
|
+
(0, import_fs10.renameSync)(tmp, file);
|
|
13679
|
+
return file;
|
|
13680
|
+
}
|
|
13681
|
+
function confirmCurrent(home, version) {
|
|
13682
|
+
const current = readCurrent(home);
|
|
13683
|
+
if (!current || current.version !== version || !current.pending) return false;
|
|
13684
|
+
writeJsonAtomic(currentPath(home), { ...current, pending: false, attempts: 0, previous: null });
|
|
13685
|
+
removeStableCopies(home, [version]);
|
|
13686
|
+
return true;
|
|
13687
|
+
}
|
|
13688
|
+
function autoUpdateEnabled(home, env) {
|
|
13689
|
+
return env.SCALEQUALITY_NO_AUTO_UPDATE !== "1" && !(0, import_fs10.existsSync)(autoUpdateOffPath(home));
|
|
13690
|
+
}
|
|
13691
|
+
function setAutoUpdate(home, on) {
|
|
13692
|
+
if (on) (0, import_fs10.rmSync)(autoUpdateOffPath(home), { force: true });
|
|
13693
|
+
else {
|
|
13694
|
+
(0, import_fs10.mkdirSync)(cliDir(home), { recursive: true, mode: 448 });
|
|
13695
|
+
(0, import_fs10.writeFileSync)(autoUpdateOffPath(home), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13696
|
+
`, { mode: 384 });
|
|
13697
|
+
}
|
|
13698
|
+
}
|
|
13699
|
+
async function latestVersion(fetchImpl = fetch) {
|
|
13700
|
+
try {
|
|
13701
|
+
const res = await fetchImpl(REGISTRY_LATEST, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
|
|
13702
|
+
if (!res.ok) return null;
|
|
13703
|
+
const body = await res.json();
|
|
13704
|
+
return body?.name === "@scalequality/cli" && typeof body.version === "string" && VERSION.test(body.version) ? body.version : null;
|
|
13705
|
+
} catch {
|
|
13706
|
+
return null;
|
|
13707
|
+
}
|
|
13708
|
+
}
|
|
13709
|
+
function npmCli(nodePath, exists = import_fs10.existsSync, resolve8 = realOrNull, pathEnv = process.env.PATH ?? "") {
|
|
13710
|
+
const dir = (0, import_path16.dirname)(nodePath);
|
|
13711
|
+
const script = (p) => p && /npm-cli\.js$/.test(p) && exists(p) ? p : null;
|
|
13712
|
+
const linked = (p) => exists(p) ? script(resolve8(p)) : null;
|
|
13713
|
+
const layouts = [
|
|
13714
|
+
(0, import_path16.join)(dir, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
13715
|
+
(0, import_path16.join)(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
13716
|
+
(0, import_path16.join)(dir, "..", "libexec", "lib", "node_modules", "npm", "bin", "npm-cli.js")
|
|
13717
|
+
];
|
|
13718
|
+
return linked((0, import_path16.join)(dir, "npm")) ?? layouts.map(script).find(Boolean) ?? pathEnv.split(import_path16.delimiter).filter(Boolean).map((d) => linked((0, import_path16.join)(d, "npm"))).find(Boolean) ?? null;
|
|
13719
|
+
}
|
|
13720
|
+
function realOrNull(p) {
|
|
13721
|
+
try {
|
|
13722
|
+
return (0, import_fs10.realpathSync)(p);
|
|
13723
|
+
} catch {
|
|
13724
|
+
return null;
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
var UpdateError = class extends Error {
|
|
13728
|
+
constructor(code, message) {
|
|
13729
|
+
super(message);
|
|
13730
|
+
this.code = code;
|
|
13731
|
+
}
|
|
13732
|
+
code;
|
|
13733
|
+
};
|
|
13734
|
+
async function installVersion(o) {
|
|
13735
|
+
const npm = o.npm === void 0 ? npmCli(o.nodePath) : o.npm;
|
|
13736
|
+
if (!npm) throw new UpdateError("NPM_NOT_FOUND", `npm was not found next to ${o.nodePath}.`);
|
|
13737
|
+
const staging = (0, import_path16.join)(cliDir(o.home), `.update-${o.version}`);
|
|
13738
|
+
(0, import_fs10.rmSync)(staging, { recursive: true, force: true });
|
|
13739
|
+
(0, import_fs10.mkdirSync)(staging, { recursive: true, mode: 448 });
|
|
13740
|
+
try {
|
|
13741
|
+
(0, import_fs10.writeFileSync)((0, import_path16.join)(staging, "package.json"), '{ "private": true }\n');
|
|
13742
|
+
const r = await o.run(o.nodePath, [
|
|
13743
|
+
npm,
|
|
13744
|
+
"install",
|
|
13745
|
+
"--prefix",
|
|
13746
|
+
staging,
|
|
13747
|
+
"--no-audit",
|
|
13748
|
+
"--no-fund",
|
|
13749
|
+
"--ignore-scripts",
|
|
13750
|
+
"--omit=dev",
|
|
13751
|
+
"--registry=https://registry.npmjs.org/",
|
|
13752
|
+
`@scalequality/cli@${o.version}`
|
|
13753
|
+
]);
|
|
13754
|
+
if (r.code !== 0) throw new UpdateError("NPM_INSTALL_FAILED", `npm could not install @scalequality/cli@${o.version}: ${`${r.stderr || r.stdout}`.trim().split("\n").slice(-1)[0]?.slice(0, 200) ?? r.code}`);
|
|
13755
|
+
const root = (0, import_path16.join)(staging, "node_modules", "@scalequality", "cli");
|
|
13756
|
+
const pkg = readJson2((0, import_path16.join)(root, "package.json"));
|
|
13757
|
+
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}.`);
|
|
13758
|
+
const build = readJson2((0, import_path16.join)(root, "dist", "connect.build.json"));
|
|
13759
|
+
const bundle = (0, import_path16.join)(root, "dist", "connect.cjs");
|
|
13760
|
+
const sha = (0, import_fs10.existsSync)(bundle) ? (0, import_crypto8.createHash)("sha256").update((0, import_fs10.readFileSync)(bundle)).digest("hex") : null;
|
|
13761
|
+
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.`);
|
|
13762
|
+
let copy;
|
|
13763
|
+
try {
|
|
13764
|
+
copy = installStableCopy({ packageRoot: root, home: o.home });
|
|
13765
|
+
} catch (e) {
|
|
13766
|
+
throw new UpdateError("COPY_FAILED", e.message);
|
|
13767
|
+
}
|
|
13768
|
+
return { version: copy.version, bin: copy.bin };
|
|
13769
|
+
} finally {
|
|
13770
|
+
(0, import_fs10.rmSync)(staging, { recursive: true, force: true });
|
|
13771
|
+
}
|
|
13772
|
+
}
|
|
13773
|
+
function stageVersion(home, next, running) {
|
|
13774
|
+
const current = readCurrent(home);
|
|
13775
|
+
writeJsonAtomic(currentPath(home), { version: next.version, bin: next.bin, previous: running, pending: true, attempts: 0, rejected: current?.rejected ?? [] });
|
|
13776
|
+
}
|
|
13777
|
+
async function checkForUpdate(d) {
|
|
13778
|
+
if (!autoUpdateEnabled(d.home, d.env) || !d.launched()) return "NONE";
|
|
13779
|
+
const latest = await latestVersion(d.fetchImpl);
|
|
13780
|
+
if (!latest || !isNewer(latest, d.version)) return "NONE";
|
|
13781
|
+
if (readCurrent(d.home)?.rejected?.includes(latest)) return "NONE";
|
|
13782
|
+
const sleep2 = d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
13783
|
+
while (d.busy()) await sleep2(UPDATE_IDLE_POLL_MS);
|
|
13784
|
+
const running = readCurrent(d.home);
|
|
13785
|
+
if (!running || running.version !== d.version) return "NONE";
|
|
13786
|
+
d.say(`A new version of the ScaleQuality CLI is available (${latest}); installing it beside ${d.version}.`);
|
|
13787
|
+
let next;
|
|
13788
|
+
try {
|
|
13789
|
+
next = await installVersion({ version: latest, home: d.home, nodePath: d.nodePath, run: d.run, npm: d.npm });
|
|
13790
|
+
} catch (e) {
|
|
13791
|
+
d.say(`The update to ${latest} was not installed (${e.message}); ${d.version} keeps running.`);
|
|
13792
|
+
return "SKIPPED";
|
|
13793
|
+
}
|
|
13794
|
+
while (d.busy()) await sleep2(UPDATE_IDLE_POLL_MS);
|
|
13795
|
+
stageVersion(d.home, next, { version: running.version, bin: running.bin });
|
|
13796
|
+
d.say(`Restarting on ${latest}. If it does not start, ${d.version} comes back on its own.`);
|
|
13797
|
+
d.restart();
|
|
13798
|
+
return "RESTARTING";
|
|
13799
|
+
}
|
|
13800
|
+
|
|
13498
13801
|
// src/main/workspace-connect.ts
|
|
13802
|
+
var import_child_process4 = require("child_process");
|
|
13499
13803
|
var importEsm = new Function("specifier", "return import(specifier)");
|
|
13500
13804
|
async function loadSdk() {
|
|
13501
13805
|
const resolved = require.resolve("@anthropic-ai/claude-agent-sdk");
|
|
@@ -13510,9 +13814,9 @@ var say = (line = "") => {
|
|
|
13510
13814
|
var cliVersion = process.env.SCALEQUALITY_CLI_VERSION || "dev";
|
|
13511
13815
|
var userAgent = (mode) => `scalequality-cli/${cliVersion} (${mode}; node ${process.versions.node}; ${process.platform})`;
|
|
13512
13816
|
var HOME = (0, import_os4.homedir)();
|
|
13513
|
-
var SQ_HOME = (0,
|
|
13514
|
-
var ENGINE_HOME = (0,
|
|
13515
|
-
var credentials = new CredentialStore((0,
|
|
13817
|
+
var SQ_HOME = (0, import_path17.join)(HOME, ".scalequality");
|
|
13818
|
+
var ENGINE_HOME = (0, import_path17.join)(SQ_HOME, "workspace");
|
|
13819
|
+
var credentials = new CredentialStore((0, import_path17.join)(SQ_HOME, "credentials.json"));
|
|
13516
13820
|
function startFailure(e, api) {
|
|
13517
13821
|
if (e instanceof NotLocalSessionError) return 'This code belongs to a cloud workspace session. In the AI Workspace, choose "Use a folder on your computer" to get a code for this command.';
|
|
13518
13822
|
if (e instanceof SessionGoneError) {
|
|
@@ -13525,16 +13829,16 @@ function startFailure(e, api) {
|
|
|
13525
13829
|
return "ScaleQuality could not start this session. Try again in a moment, or get a new code from the AI Workspace.";
|
|
13526
13830
|
}
|
|
13527
13831
|
function engineDirs() {
|
|
13528
|
-
const configDir = (0,
|
|
13529
|
-
const scratch = (0,
|
|
13530
|
-
(0,
|
|
13531
|
-
(0,
|
|
13832
|
+
const configDir = (0, import_path17.join)(ENGINE_HOME, "claude-home");
|
|
13833
|
+
const scratch = (0, import_path17.join)(ENGINE_HOME, "tmp");
|
|
13834
|
+
(0, import_fs11.mkdirSync)(configDir, { recursive: true, mode: 448 });
|
|
13835
|
+
(0, import_fs11.mkdirSync)(scratch, { recursive: true, mode: 448 });
|
|
13532
13836
|
return { configDir, scratch };
|
|
13533
13837
|
}
|
|
13534
13838
|
function createLocalEngine(o) {
|
|
13535
13839
|
const { configDir, scratch } = engineDirs();
|
|
13536
|
-
const sessions = (0,
|
|
13537
|
-
const stateDir = (0,
|
|
13840
|
+
const sessions = (0, import_path17.join)(ENGINE_HOME, "sessions");
|
|
13841
|
+
const stateDir = (0, import_path17.join)(sessions, o.sessionId);
|
|
13538
13842
|
void pruneSessionStates(sessions, stateDir).catch(() => void 0);
|
|
13539
13843
|
const log = {
|
|
13540
13844
|
info: (msg, ctx) => {
|
|
@@ -13721,7 +14025,14 @@ async function setUpService(api) {
|
|
|
13721
14025
|
say(style.yellow(e.message));
|
|
13722
14026
|
return false;
|
|
13723
14027
|
}
|
|
13724
|
-
|
|
14028
|
+
let launcher;
|
|
14029
|
+
try {
|
|
14030
|
+
launcher = installLauncher(HOME, { version, bin });
|
|
14031
|
+
} catch (e) {
|
|
14032
|
+
say(style.yellow(`The launcher of the background service could not be written: ${e.message}`));
|
|
14033
|
+
return false;
|
|
14034
|
+
}
|
|
14035
|
+
const result = await installService(env, { bin: launcher, api });
|
|
13725
14036
|
if (!result.ok) {
|
|
13726
14037
|
say(style.yellow(result.message));
|
|
13727
14038
|
return false;
|
|
@@ -13827,7 +14138,7 @@ async function upMain(api, verbose, service) {
|
|
|
13827
14138
|
let shuttingDown = false;
|
|
13828
14139
|
let agent;
|
|
13829
14140
|
const startSession = ({ sessionId, secret, root }) => {
|
|
13830
|
-
const label = (0,
|
|
14141
|
+
const label = (0, import_path17.basename)(root);
|
|
13831
14142
|
const prefix = style.dim(`[${label}] `);
|
|
13832
14143
|
const consoleLog = new ConsoleLog(style);
|
|
13833
14144
|
let resolveDone = () => void 0;
|
|
@@ -13877,6 +14188,15 @@ async function upMain(api, verbose, service) {
|
|
|
13877
14188
|
process.on("unhandledRejection", (e) => {
|
|
13878
14189
|
if (verbose || service) say(style.dim(`[warn] unhandled rejection ${e?.message}`));
|
|
13879
14190
|
});
|
|
14191
|
+
if (service && process.env.SCALEQUALITY_LAUNCHER === "1") startSelfUpdate(api, () => agent.sessions.size > 0, () => {
|
|
14192
|
+
agent.stop();
|
|
14193
|
+
releaseLock(HOME, api);
|
|
14194
|
+
if (process.platform === "win32") {
|
|
14195
|
+
(0, import_child_process4.spawn)(process.execPath, [launcherPath(HOME), "up", "--api", api, "--service"], { detached: true, stdio: "ignore", windowsHide: true, env: process.env }).unref();
|
|
14196
|
+
process.exit(0);
|
|
14197
|
+
}
|
|
14198
|
+
process.exit(RESTART_EXIT_CODE);
|
|
14199
|
+
});
|
|
13880
14200
|
say(style.bold(`ScaleQuality: keeping "${credential.name}" connected to ${api}${service ? " (background service)" : ""}`));
|
|
13881
14201
|
say(` Folders: ${credential.folders.length ? credential.folders.join(", ") : "none yet (scalequality add <folder>)"}`);
|
|
13882
14202
|
say(style.dim(" Commands that sessions want to run are approved in the browser."));
|
|
@@ -13901,9 +14221,44 @@ async function upMain(api, verbose, service) {
|
|
|
13901
14221
|
throw e;
|
|
13902
14222
|
}
|
|
13903
14223
|
}
|
|
14224
|
+
function startSelfUpdate(api, busy, restart) {
|
|
14225
|
+
const confirm = setTimeout(() => {
|
|
14226
|
+
try {
|
|
14227
|
+
if (confirmCurrent(HOME, cliVersion)) say(`Version ${cliVersion} started well; the previous copy was removed.`);
|
|
14228
|
+
} catch (e) {
|
|
14229
|
+
say(`[warn] version ${cliVersion} not confirmed: ${e.message}`);
|
|
14230
|
+
}
|
|
14231
|
+
}, CONFIRM_AFTER_MS);
|
|
14232
|
+
confirm.unref();
|
|
14233
|
+
let checking = false;
|
|
14234
|
+
const check = async () => {
|
|
14235
|
+
if (checking) return;
|
|
14236
|
+
checking = true;
|
|
14237
|
+
try {
|
|
14238
|
+
await checkForUpdate({
|
|
14239
|
+
home: HOME,
|
|
14240
|
+
env: process.env,
|
|
14241
|
+
version: cliVersion,
|
|
14242
|
+
nodePath: process.execPath,
|
|
14243
|
+
run: runCommand,
|
|
14244
|
+
busy,
|
|
14245
|
+
say,
|
|
14246
|
+
launched: () => process.env.SCALEQUALITY_LAUNCHER === "1",
|
|
14247
|
+
restart
|
|
14248
|
+
});
|
|
14249
|
+
} catch (e) {
|
|
14250
|
+
say(`[warn] update check failed: ${e.message}`);
|
|
14251
|
+
} finally {
|
|
14252
|
+
checking = false;
|
|
14253
|
+
}
|
|
14254
|
+
};
|
|
14255
|
+
setTimeout(() => void check(), UPDATE_FIRST_CHECK_MS).unref();
|
|
14256
|
+
setInterval(() => void check(), UPDATE_INTERVAL_MS).unref();
|
|
14257
|
+
say(style.dim(autoUpdateEnabled(HOME, process.env) ? ` Updates install by themselves while no session runs (scalequality service auto-update off${apiFlag(api)} to stop).` : " Automatic updates are off."));
|
|
14258
|
+
}
|
|
13904
14259
|
async function addMain(api, path) {
|
|
13905
14260
|
try {
|
|
13906
|
-
const real = await addFolder(credentials, api, (0,
|
|
14261
|
+
const real = await addFolder(credentials, api, (0, import_path17.resolve)(path ?? process.cwd()), HOME);
|
|
13907
14262
|
say(style.green(`Added ${real}.`));
|
|
13908
14263
|
const credential = credentials.get(api);
|
|
13909
14264
|
const client = new MachineClient(api, { token: credential.machineToken, userAgent: userAgent("add") });
|
|
@@ -13946,8 +14301,13 @@ async function logoutMain(api) {
|
|
|
13946
14301
|
credentials.remove(api);
|
|
13947
14302
|
say(`Deleted the local credential for ${api}.`);
|
|
13948
14303
|
}
|
|
13949
|
-
async function serviceMain(action, api, lines2) {
|
|
14304
|
+
async function serviceMain(action, api, lines2, toggle = null) {
|
|
13950
14305
|
const env = serviceEnv();
|
|
14306
|
+
if (action === "auto-update") {
|
|
14307
|
+
setAutoUpdate(HOME, toggle === "on");
|
|
14308
|
+
say(toggle === "on" ? "Automatic updates are on: the service installs new versions by itself while no session runs." : "Automatic updates are off. Update by hand with: npx @scalequality/cli@latest login");
|
|
14309
|
+
return;
|
|
14310
|
+
}
|
|
13951
14311
|
if (action === "install" || action === "start") {
|
|
13952
14312
|
if (!credentials.get(api)) {
|
|
13953
14313
|
say(style.red(`This computer is not connected to ${api}. Run scalequality login${apiFlag(api)} first.`));
|
|
@@ -13972,6 +14332,8 @@ async function serviceMain(action, api, lines2) {
|
|
|
13972
14332
|
`Running ${s.running ? `yes${s.pid ? ` (process ${s.pid})` : ""}` : "no"}`,
|
|
13973
14333
|
...s.definition ? [`Definition ${s.definition}`] : [],
|
|
13974
14334
|
...s.bin ? [`Runs ${s.bin}`] : [],
|
|
14335
|
+
...readCurrent(HOME) ? [`Version ${readCurrent(HOME).version}${readCurrent(HOME).pending ? " (just updated, confirming)" : ""}`] : [],
|
|
14336
|
+
`Automatic updates ${autoUpdateEnabled(HOME, process.env) ? "on" : "off"}`,
|
|
13975
14337
|
`Log ${s.logFile}`
|
|
13976
14338
|
].join("\n") + "\n");
|
|
13977
14339
|
return;
|
|
@@ -14011,7 +14373,7 @@ async function main() {
|
|
|
14011
14373
|
if (args.command === "login") await loginMain(api, args.name, args.up, args.verbose);
|
|
14012
14374
|
else if (args.command === "up") await upMain(api, args.verbose, args.service || process.env.SCALEQUALITY_SERVICE === "1");
|
|
14013
14375
|
else if (args.command === "add") await addMain(api, args.path);
|
|
14014
|
-
else if (args.command === "service") await serviceMain(args.action, api, args.lines);
|
|
14376
|
+
else if (args.command === "service") await serviceMain(args.action, api, args.lines, args.toggle);
|
|
14015
14377
|
else await logoutMain(api);
|
|
14016
14378
|
}
|
|
14017
14379
|
main().catch((e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalequality/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
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": {
|