@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "7b199d28673b767dce41406f737d3c8042ed0658ca1a233a7d8363b5b7b27dfd",
4
- "sourceCommit": "0da92af88f77faa5f860d7f37480a65800017946"
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 import_fs10 = require("fs");
31
+ var import_fs11 = require("fs");
32
32
  var import_os4 = require("os");
33
- var import_path16 = require("path");
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 logs." };
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 === keep || !/^\.?[0-9A-Za-z.+-]{1,60}$/.test(name)) continue;
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, import_path16.join)(HOME, ".scalequality");
13514
- var ENGINE_HOME = (0, import_path16.join)(SQ_HOME, "workspace");
13515
- var credentials = new CredentialStore((0, import_path16.join)(SQ_HOME, "credentials.json"));
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, import_path16.join)(ENGINE_HOME, "claude-home");
13529
- const scratch = (0, import_path16.join)(ENGINE_HOME, "tmp");
13530
- (0, import_fs10.mkdirSync)(configDir, { recursive: true, mode: 448 });
13531
- (0, import_fs10.mkdirSync)(scratch, { recursive: true, mode: 448 });
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, import_path16.join)(ENGINE_HOME, "sessions");
13537
- const stateDir = (0, import_path16.join)(sessions, o.sessionId);
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
- const result = await installService(env, { bin, api });
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, import_path16.basename)(root);
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, import_path16.resolve)(path ?? process.cwd()), HOME);
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.12",
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": {