@scalequality/cli 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -35,6 +35,13 @@ While `up` runs, the AI Workspace can:
35
35
  - start a session in one of your folders (at most 3 at once);
36
36
  - add a folder you pick there (it is checked here: it must exist, be inside
37
37
  your home folder and not be the home folder);
38
+ - show you your folders to pick one, only while the folder picker is open in
39
+ the browser: the subfolders of one folder of your home at a time (no files,
40
+ no hidden or system folders, links only while they stay inside the home) and
41
+ suggestions (git repositories under `~/GIT`, `~/code`, `~/projects`,
42
+ `~/Developer` and similar folders, 3 levels down, plus the folders of your
43
+ Claude Code and Codex conversations). Only folder names, their paths and the
44
+ `origin` remote of repositories are sent, without any user or password;
38
45
  - list your Claude Code (`~/.claude/projects`) and Codex (`~/.codex/sessions`)
39
46
  conversations, and import the ones you choose. Secrets (cloud keys, provider
40
47
  tokens, private keys, JWTs, connection strings, `password=`-style values) are
@@ -42,6 +49,24 @@ While `up` runs, the AI Workspace can:
42
49
  without them. Only text is imported; tool results are left out and each tool
43
50
  call becomes a one-line summary.
44
51
 
52
+ ### What `up` counts to suggest an import
53
+
54
+ When `up` starts, and then at most once every 6 hours while it runs, it counts
55
+ the Claude Code and Codex conversations on this computer, so the AI Workspace
56
+ can offer to import them. It reads the same files the import list reads
57
+ (`~/.claude/projects/*/*.jsonl`, or `CLAUDE_CONFIG_DIR`; `~/.codex/sessions/**/rollout-*.jsonl`,
58
+ or `CODEX_HOME`), at most the 500 most recent, each only up to its first
59
+ message, and prints one line such as:
60
+
61
+ ```
62
+ Found 28 Claude Code and 11 Codex conversations on this computer. You can import them from the browser.
63
+ ```
64
+
65
+ Only the two numbers and the time of the count leave the computer
66
+ (`importable: {claudeCode, codex, countedAt}` in the computer's inventory). No
67
+ title, message, file name or folder name is sent until you open the import
68
+ list in the browser and choose what to import.
69
+
45
70
  A Claude Code conversation continued on the same computer and folder resumes
46
71
  from its own transcript: a copy without secrets is written into the engine's
47
72
  folder (`~/.scalequality/workspace`); the original file is never changed.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "d634393c324bdcca4062aca8326e5ce8d4cd637e42bf49d37edf5510e1dae6d4",
4
- "sourceCommit": "d9d14ca31dd80a5a7bc14e489b362de9385261c3"
3
+ "sha256": "08314a95264b9909caa6a812f0bba154465f35fb82a3ffb541fdaeb5c3b95ee5",
4
+ "sourceCommit": "982edf3dc5d3c2e1473b4f13d0695caaed4eeaed"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -30,7 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  // src/main/workspace-connect.ts
31
31
  var import_fs5 = require("fs");
32
32
  var import_os2 = require("os");
33
- var import_path8 = require("path");
33
+ var import_path9 = require("path");
34
34
  var import_url = require("url");
35
35
 
36
36
  // src/application/services/workspaceSandbox/reasoning.ts
@@ -376,11 +376,11 @@ function routeLabel(path) {
376
376
  return path.split("?")[0];
377
377
  }
378
378
  function sleep(ms, signal) {
379
- return new Promise((resolve6) => {
380
- const t = setTimeout(resolve6, ms);
379
+ return new Promise((resolve7) => {
380
+ const t = setTimeout(resolve7, ms);
381
381
  signal?.addEventListener("abort", () => {
382
382
  clearTimeout(t);
383
- resolve6();
383
+ resolve7();
384
384
  }, { once: true });
385
385
  });
386
386
  }
@@ -478,7 +478,7 @@ var RULES = [
478
478
  { kind: "PRIVATE_KEY", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g },
479
479
  { kind: "CONNECTION_STRING", re: /\b([a-z][a-z0-9+.-]{1,30}:\/\/)([^\s:@/'"]{1,200}):([^\s@/'"]{1,300})@/gi, replace: (_m, scheme, user) => `${scheme}${user}:${R("PASSWORD")}@` },
480
480
  { kind: "AWS_ACCESS_KEY", re: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
481
- { kind: "AWS_SECRET_KEY", re: /\b(aws_?secret_?access_?key|aws_?secret|secretAccessKey)(["']?\s*[:=]\s*["']?)([A-Za-z0-9/+=]{40})\b/gi, replace: (_m, k, sep3) => `${k}${sep3}${R("AWS_SECRET_KEY")}` },
481
+ { kind: "AWS_SECRET_KEY", re: /\b(aws_?secret_?access_?key|aws_?secret|secretAccessKey)(["']?\s*[:=]\s*["']?)([A-Za-z0-9/+=]{40})\b/gi, replace: (_m, k, sep4) => `${k}${sep4}${R("AWS_SECRET_KEY")}` },
482
482
  { kind: "ANTHROPIC_KEY", re: /\bsk-ant-[a-z]{2,10}\d{0,3}-[A-Za-z0-9_-]{20,}/g },
483
483
  { kind: "OPENAI_KEY", re: /\bsk-(?:proj-|svcacct-|admin-|None-)?[A-Za-z0-9_-]{20,}/g },
484
484
  { kind: "GITHUB_TOKEN", re: /\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{22,255})\b/g },
@@ -494,7 +494,7 @@ var RULES = [
494
494
  kind: "ASSIGNED_SECRET",
495
495
  // password=..., "api_key": "...", SECRET_TOKEN: ..., --token ... (key names that say "secret").
496
496
  re: /\b([A-Za-z0-9_.-]*(?:passw(?:or)?d|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?key)[A-Za-z0-9_-]*)(["']?\s*(?:=|:|\s)\s*)(["']?)([^\s"'`,;)}\]]{8,500})\3/gi,
497
- replace: (_m, key, sep3, quote, value) => looksLikeSecretValue(value) ? `${key}${sep3}${quote}${R("SECRET")}${quote}` : null
497
+ replace: (_m, key, sep4, quote, value) => looksLikeSecretValue(value) ? `${key}${sep4}${quote}${R("SECRET")}${quote}` : null
498
498
  }
499
499
  ];
500
500
  function scrubSecrets(input) {
@@ -600,7 +600,7 @@ function claudeText(content, role) {
600
600
  }
601
601
  return { text: texts.join("\n\n").trim(), tools };
602
602
  }
603
- async function parseClaudeCodeFile(file) {
603
+ async function parseClaudeCodeFile(file, opts = {}) {
604
604
  const externalId = (0, import_path.basename)(file, ".jsonl");
605
605
  if (!isExternalId(externalId)) return null;
606
606
  const win = new MessageWindow();
@@ -611,6 +611,7 @@ async function parseClaudeCodeFile(file) {
611
611
  let firstUser = null;
612
612
  let current = null;
613
613
  for await (const r of lines(file)) {
614
+ if (opts.probe && win.total) break;
614
615
  if (!folder && typeof r.cwd === "string") folder = r.cwd;
615
616
  if (r.type === "custom-title" && typeof r.customTitle === "string") customTitle = r.customTitle;
616
617
  else if (r.type === "ai-title" && typeof r.aiTitle === "string") aiTitle = r.aiTitle;
@@ -672,13 +673,14 @@ function codexText(content, role) {
672
673
  const want = role === "user" ? "input_text" : "output_text";
673
674
  return content.filter((b) => b?.type === want && typeof b.text === "string" && !(role === "user" && WRAPPER.test(b.text))).map((b) => b.text).join("\n\n").trim();
674
675
  }
675
- async function parseCodexFile(file) {
676
+ async function parseCodexFile(file, opts = {}) {
676
677
  const fromName = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(file)?.[1] ?? null;
677
678
  let externalId = null;
678
679
  let folder = null;
679
680
  let firstUser = null;
680
681
  const win = new MessageWindow();
681
682
  for await (const r of lines(file)) {
683
+ if (opts.probe && win.total && externalId) break;
682
684
  const p = r.payload;
683
685
  if (!p || typeof p !== "object") continue;
684
686
  if (r.type === "session_meta") {
@@ -733,11 +735,14 @@ function secretsIn(c) {
733
735
  }
734
736
  return n;
735
737
  }
736
- async function scanImports(sources) {
737
- const files = [
738
+ async function importFiles(sources) {
739
+ return [
738
740
  ...(await claudeCodeFiles(sources.claudeDir)).map((f) => ({ ...f, source: "CLAUDE_CODE" })),
739
741
  ...(await codexFiles(sources.codexDir)).map((f) => ({ ...f, source: "CODEX" }))
740
742
  ].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
743
+ }
744
+ async function scanImports(sources) {
745
+ const files = await importFiles(sources);
741
746
  const items = [];
742
747
  const seen = /* @__PURE__ */ new Set();
743
748
  for (const f of files) {
@@ -756,6 +761,49 @@ async function scanImports(sources) {
756
761
  }
757
762
  return items;
758
763
  }
764
+ async function countImports(sources) {
765
+ const counts = { claudeCode: 0, codex: 0 };
766
+ const seen = /* @__PURE__ */ new Set();
767
+ for (const f of await importFiles(sources)) {
768
+ const c = await (f.source === "CLAUDE_CODE" ? parseClaudeCodeFile(f.path, { probe: true }) : parseCodexFile(f.path, { probe: true })).catch(() => null);
769
+ if (!c || seen.has(`${c.source}:${c.externalId}`)) continue;
770
+ seen.add(`${c.source}:${c.externalId}`);
771
+ if (c.source === "CLAUDE_CODE") counts.claudeCode++;
772
+ else counts.codex++;
773
+ }
774
+ return counts;
775
+ }
776
+ async function conversationFolders(sources, deadline) {
777
+ const claude = [];
778
+ for (const project of await subdirs((0, import_path.join)(sources.claudeDir, "projects"))) {
779
+ if (Date.now() > deadline) break;
780
+ const newest = (await regularFiles(project, (n) => n.endsWith(".jsonl"))).sort((a, b) => b.mtime - a.mtime)[0];
781
+ if (newest) claude.push({ ...newest, source: "CLAUDE_CODE" });
782
+ }
783
+ const codex = Date.now() > deadline ? [] : (await codexFiles(sources.codexDir)).map((f) => ({ ...f, source: "CODEX" }));
784
+ const files = [...claude, ...codex].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
785
+ const out = [];
786
+ const seen = /* @__PURE__ */ new Set();
787
+ for (const f of files) {
788
+ if (Date.now() > deadline) break;
789
+ let n = 0;
790
+ try {
791
+ for await (const r of lines(f.path)) {
792
+ if (++n > 50) break;
793
+ const cwd = f.source === "CLAUDE_CODE" ? r.cwd : r.type === "session_meta" && r.payload && typeof r.payload === "object" && !(r.payload.source && typeof r.payload.source === "object" && r.payload.source.subagent) ? r.payload.cwd : void 0;
794
+ if (typeof cwd === "string" && cwd) {
795
+ if (!seen.has(cwd)) {
796
+ seen.add(cwd);
797
+ out.push(cwd);
798
+ }
799
+ break;
800
+ }
801
+ }
802
+ } catch {
803
+ }
804
+ }
805
+ return out;
806
+ }
759
807
  async function findConversation(sources, source, externalId) {
760
808
  if (!isExternalId(externalId)) return null;
761
809
  if (source === "CLAUDE_CODE") {
@@ -833,8 +881,8 @@ function scrubRecord(record) {
833
881
  async function writeScrubbedTranscript(source, target) {
834
882
  const out = (0, import_fs.createWriteStream)(target, { mode: 384, flags: "wx" });
835
883
  let removed = 0;
836
- const done = new Promise((resolve6, reject) => {
837
- out.on("finish", resolve6);
884
+ const done = new Promise((resolve7, reject) => {
885
+ out.on("finish", resolve7);
838
886
  out.on("error", reject);
839
887
  });
840
888
  const rl = (0, import_readline.createInterface)({ input: (0, import_fs.createReadStream)(source, { encoding: "utf8" }), crlfDelay: Infinity });
@@ -1308,13 +1356,13 @@ ${indent}`);
1308
1356
  function terminalCommandPrompt(o) {
1309
1357
  const bold = (s) => o.color ? `\x1B[1m${s}\x1B[22m` : s;
1310
1358
  const dim = (s) => o.color ? `\x1B[2m${s}\x1B[22m` : s;
1311
- return (q, signal) => new Promise((resolve6) => {
1359
+ return (q, signal) => new Promise((resolve7) => {
1312
1360
  if (!o.input.isTTY) {
1313
- resolve6(null);
1361
+ resolve7(null);
1314
1362
  return;
1315
1363
  }
1316
1364
  if (signal?.aborted) {
1317
- resolve6(null);
1365
+ resolve7(null);
1318
1366
  return;
1319
1367
  }
1320
1368
  const rl = (0, import_readline2.createInterface)({ input: o.input, output: o.output, terminal: true });
@@ -1324,7 +1372,7 @@ function terminalCommandPrompt(o) {
1324
1372
  done = true;
1325
1373
  signal?.removeEventListener("abort", onAbort);
1326
1374
  rl.close();
1327
- resolve6(a);
1375
+ resolve7(a);
1328
1376
  };
1329
1377
  const onAbort = () => {
1330
1378
  o.output.write(`
@@ -1553,7 +1601,11 @@ var MACHINE_USAGE = {
1553
1601
  "Keeps this computer connected: the AI Workspace can run sessions in the",
1554
1602
  "folders you added (scalequality add), list your Claude Code and Codex",
1555
1603
  "conversations and import the ones you choose. Every command a session",
1556
- "wants to run is confirmed in this terminal. Ctrl+C disconnects."
1604
+ "wants to run is confirmed in this terminal. Ctrl+C disconnects.",
1605
+ "",
1606
+ "When it starts (and at most every 6 hours), it counts your Claude Code and",
1607
+ "Codex conversations; only the two numbers are sent, so the AI Workspace can",
1608
+ "offer to import them."
1557
1609
  ].join("\n"),
1558
1610
  add: [
1559
1611
  "Usage: scalequality add [PATH] [--api URL]",
@@ -1616,8 +1668,297 @@ function parseMachineArgs(argv) {
1616
1668
 
1617
1669
  // src/application/services/workspaceSandbox/machineCli.ts
1618
1670
  var import_fs3 = require("fs");
1671
+ var import_promises5 = require("fs/promises");
1672
+ var import_path5 = require("path");
1673
+
1674
+ // src/application/services/workspaceSandbox/machineFolders.ts
1619
1675
  var import_promises4 = require("fs/promises");
1620
1676
  var import_path4 = require("path");
1677
+ var FOLDER_LIST_LIMIT = 300;
1678
+ var FOLDER_SUGGEST_LIMIT = 200;
1679
+ var FOLDER_LIST_BUDGET_MS = 4e3;
1680
+ var FOLDER_SUGGEST_BUDGET_MS = 3e3;
1681
+ var MAX_DIRENTS = 5e3;
1682
+ var CHILD_PROBE = 256;
1683
+ var SUGGEST_DEPTH = 3;
1684
+ var MAX_REMOTE = 1024;
1685
+ var MAX_ANSWER_BYTES = 900 * 1024;
1686
+ var CONCURRENCY = 24;
1687
+ var DEV_ROOTS = ["GIT", "git", "code", "Code", "projects", "Projects", "dev", "src", "workspace", "repos", "Developer", "Documents/GitHub", "go/src"];
1688
+ var SKIP_ANYWHERE = /* @__PURE__ */ new Set(["node_modules", "bower_components", "jspm_packages", "__pycache__", "Caches", "CachedData", "DerivedData", "Pods"]);
1689
+ var SKIP_AT_HOME = /* @__PURE__ */ new Set([
1690
+ "Library",
1691
+ "Applications",
1692
+ "System",
1693
+ "Volumes",
1694
+ "AppData",
1695
+ "Application Data",
1696
+ "Local Settings",
1697
+ "Cookies",
1698
+ "NetHood",
1699
+ "PrintHood",
1700
+ "Recent",
1701
+ "SendTo",
1702
+ "Start Menu",
1703
+ "Templates",
1704
+ "snap",
1705
+ "Trash"
1706
+ ]);
1707
+ var FolderBrowseError = class extends Error {
1708
+ constructor(code) {
1709
+ super(code);
1710
+ this.code = code;
1711
+ this.name = "FolderBrowseError";
1712
+ }
1713
+ code;
1714
+ };
1715
+ var hidden = (name) => name.startsWith(".") || name.startsWith("$");
1716
+ var skipped = (name, atHome) => hidden(name) || SKIP_ANYWHERE.has(name) || atHome && SKIP_AT_HOME.has(name);
1717
+ function insideOrHome(path, home) {
1718
+ const problem = folderPathProblem(path, home);
1719
+ return problem === null || problem === "PATH_IS_HOME";
1720
+ }
1721
+ function logical(home, real) {
1722
+ if (home.logical === home.real) return real;
1723
+ const rel = (0, import_path4.relative)(home.real, real);
1724
+ return rel ? (0, import_path4.join)(home.logical, rel) : home.logical;
1725
+ }
1726
+ async function realInside(home, path) {
1727
+ const real = await (0, import_promises4.realpath)(path).catch(() => null);
1728
+ if (!real || !insideOrHome(real, home.real)) return null;
1729
+ return real;
1730
+ }
1731
+ async function resolveBrowsePath(raw, home) {
1732
+ if (raw !== void 0 && raw !== null && typeof raw !== "string") throw new FolderBrowseError("INVALID_PATH");
1733
+ const text2 = (raw ?? "").trim();
1734
+ if (text2.length > MAX_PATH_LENGTH) throw new FolderBrowseError("INVALID_PATH");
1735
+ if (/[\u0000-\u001f\u007f]/.test(text2)) throw new FolderBrowseError("INVALID_PATH");
1736
+ let target;
1737
+ if (!text2 || text2 === "~") target = home.logical;
1738
+ else {
1739
+ const rest = text2.startsWith("~/") || text2.startsWith("~\\") ? text2.slice(2) : text2;
1740
+ if (rest.split(/[\\/]+/).some((part) => part === ".." || part === ".")) throw new FolderBrowseError("INVALID_PATH");
1741
+ target = (0, import_path4.isAbsolute)(rest) && rest === text2 ? rest : (0, import_path4.resolve)(home.logical, rest);
1742
+ if (!insideOrHome(target, home.logical)) throw new FolderBrowseError("PATH_OUTSIDE_HOME");
1743
+ }
1744
+ const st = await (0, import_promises4.stat)(target).catch(() => null);
1745
+ if (!st?.isDirectory()) throw new FolderBrowseError("FOLDER_NOT_FOUND");
1746
+ const real = await realInside(home, target);
1747
+ if (!real) throw new FolderBrowseError("PATH_OUTSIDE_HOME");
1748
+ return real;
1749
+ }
1750
+ async function originRemote(repo2) {
1751
+ try {
1752
+ const dotGit = (0, import_path4.join)(repo2, ".git");
1753
+ const st = await (0, import_promises4.lstat)(dotGit);
1754
+ let gitDir = dotGit;
1755
+ if (st.isFile()) {
1756
+ const pointer = /^gitdir:\s*(.+)\s*$/m.exec(await (0, import_promises4.readFile)(dotGit, "utf8"))?.[1];
1757
+ if (!pointer) return null;
1758
+ gitDir = (0, import_path4.resolve)(repo2, pointer.trim());
1759
+ const common = (await (0, import_promises4.readFile)((0, import_path4.join)(gitDir, "commondir"), "utf8").catch(() => "")).trim();
1760
+ if (common) gitDir = (0, import_path4.resolve)(gitDir, common);
1761
+ } else if (!st.isDirectory()) return null;
1762
+ const config = await (0, import_promises4.readFile)((0, import_path4.join)(gitDir, "config"), "utf8");
1763
+ let inOrigin = false;
1764
+ for (const line of config.split(/\r?\n/)) {
1765
+ const section = /^\s*\[\s*([^\]]+?)\s*\]/.exec(line);
1766
+ if (section) {
1767
+ inOrigin = /^remote\s+"origin"$/.test(section[1]);
1768
+ continue;
1769
+ }
1770
+ if (!inOrigin) continue;
1771
+ const url = /^\s*url\s*=\s*(.+?)\s*$/.exec(line)?.[1];
1772
+ if (url) return stripRemoteCredentials(url.replace(/^"(.*)"$/, "$1")).slice(0, MAX_REMOTE) || null;
1773
+ }
1774
+ return null;
1775
+ } catch {
1776
+ return null;
1777
+ }
1778
+ }
1779
+ async function isGitRepo(dir) {
1780
+ const st = await (0, import_promises4.lstat)((0, import_path4.join)(dir, ".git")).catch(() => null);
1781
+ return !!st && (st.isDirectory() || st.isFile());
1782
+ }
1783
+ async function hasVisibleChild(dir) {
1784
+ let handle;
1785
+ try {
1786
+ handle = await (0, import_promises4.opendir)(dir);
1787
+ } catch {
1788
+ return false;
1789
+ }
1790
+ let seen = 0;
1791
+ try {
1792
+ for await (const d of handle) {
1793
+ if (++seen > CHILD_PROBE) return false;
1794
+ if ((d.isDirectory() || d.isSymbolicLink()) && !skipped(d.name, false)) return true;
1795
+ }
1796
+ return false;
1797
+ } catch {
1798
+ return false;
1799
+ } finally {
1800
+ await handle.close().catch(() => void 0);
1801
+ }
1802
+ }
1803
+ async function subfolderNames(dir, atHome) {
1804
+ const names = [];
1805
+ let handle;
1806
+ try {
1807
+ handle = await (0, import_promises4.opendir)(dir);
1808
+ } catch {
1809
+ return { names, cut: false };
1810
+ }
1811
+ let seen = 0, cut = false;
1812
+ try {
1813
+ for await (const d of handle) {
1814
+ if (++seen > MAX_DIRENTS) {
1815
+ cut = true;
1816
+ break;
1817
+ }
1818
+ if (skipped(d.name, atHome)) continue;
1819
+ if (d.isDirectory()) names.push({ name: d.name, link: false });
1820
+ else if (d.isSymbolicLink()) names.push({ name: d.name, link: true });
1821
+ }
1822
+ } catch {
1823
+ } finally {
1824
+ await handle.close().catch(() => void 0);
1825
+ }
1826
+ return { names, cut };
1827
+ }
1828
+ async function eachUntil(items, deadline, work) {
1829
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
1830
+ if (Date.now() > deadline) return false;
1831
+ await Promise.all(items.slice(i, i + CONCURRENCY).map((item) => work(item).catch(() => void 0)));
1832
+ }
1833
+ return true;
1834
+ }
1835
+ var byGitThenName = (a, b) => (a.isGitRepo === b.isGitRepo ? 0 : a.isGitRepo ? -1 : 1) || a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true });
1836
+ function fit(answer) {
1837
+ while (answer.entries.length && Buffer.byteLength(JSON.stringify(answer)) > MAX_ANSWER_BYTES) {
1838
+ answer.entries.splice(Math.max(1, Math.floor(answer.entries.length * 0.9)));
1839
+ answer.truncated = true;
1840
+ }
1841
+ return answer;
1842
+ }
1843
+ async function homeOf(home) {
1844
+ return { logical: home, real: await (0, import_promises4.realpath)(home).catch(() => home) };
1845
+ }
1846
+ async function listFolders(homePath, rawPath, opts = {}) {
1847
+ const deadline = Date.now() + (opts.budgetMs ?? FOLDER_LIST_BUDGET_MS);
1848
+ const limit = opts.limit ?? FOLDER_LIST_LIMIT;
1849
+ const home = await homeOf(homePath);
1850
+ const real = await resolveBrowsePath(rawPath, home);
1851
+ const atHome = real === home.real;
1852
+ const { names, cut } = await subfolderNames(real, atHome);
1853
+ let truncated = cut;
1854
+ const found = [];
1855
+ const seen = /* @__PURE__ */ new Set();
1856
+ truncated = !await eachUntil(names, deadline, async ({ name, link }) => {
1857
+ let target = (0, import_path4.join)(real, name);
1858
+ if (link) {
1859
+ const st = await (0, import_promises4.stat)(target).catch(() => null);
1860
+ if (!st?.isDirectory()) return;
1861
+ const inside2 = await realInside(home, target);
1862
+ if (!inside2 || inside2 === home.real) return;
1863
+ target = inside2;
1864
+ }
1865
+ if (seen.has(target)) return;
1866
+ seen.add(target);
1867
+ found.push({ name, real: target, isGitRepo: await isGitRepo(target) });
1868
+ }) || truncated;
1869
+ found.sort(byGitThenName);
1870
+ if (found.length > limit) truncated = true;
1871
+ const shown = found.slice(0, limit);
1872
+ const entries = shown.map(() => null);
1873
+ truncated = !await eachUntil(shown.map((f, i) => ({ f, i })), deadline, async ({ f, i }) => {
1874
+ const [remoteUrl, hasChildren] = await Promise.all([f.isGitRepo ? originRemote(f.real) : Promise.resolve(null), hasVisibleChild(f.real)]);
1875
+ entries[i] = { name: f.name, path: logical(home, f.real), isGitRepo: f.isGitRepo, remoteUrl, hasChildren };
1876
+ }) || truncated;
1877
+ const path = logical(home, real);
1878
+ const rel = (0, import_path4.relative)(home.logical, path);
1879
+ const parentReal = atHome ? null : (0, import_path4.resolve)(real, "..");
1880
+ const parent = parentReal && insideOrHome(parentReal, home.real) ? logical(home, parentReal) : null;
1881
+ return fit({ home: "~", homePath: home.logical, path, relative: rel.split(import_path4.sep).join("/"), parent, entries: entries.filter((e) => !!e), truncated });
1882
+ }
1883
+ async function suggestFolders(homePath, sources, opts = {}) {
1884
+ const started = Date.now();
1885
+ const deadline = started + (opts.budgetMs ?? FOLDER_SUGGEST_BUDGET_MS);
1886
+ const limit = opts.limit ?? FOLDER_SUGGEST_LIMIT;
1887
+ const home = await homeOf(homePath);
1888
+ const out = [];
1889
+ const seen = /* @__PURE__ */ new Set();
1890
+ let truncated = false;
1891
+ const add = (real, source) => {
1892
+ if (seen.has(real) || real === home.real) return;
1893
+ if (out.length >= limit) {
1894
+ truncated = true;
1895
+ return;
1896
+ }
1897
+ seen.add(real);
1898
+ out.push({ real, source });
1899
+ };
1900
+ const convoDeadline = started + Math.floor((deadline - started) / 2);
1901
+ const folders = await (opts.conversations ?? ((d) => conversationFolders(sources, d)))(convoDeadline).catch(() => []);
1902
+ for (const folder of folders) {
1903
+ if (Date.now() > deadline) {
1904
+ truncated = true;
1905
+ break;
1906
+ }
1907
+ if (typeof folder !== "string" || folderPathProblem(folder, home.logical) && folderPathProblem(folder, home.real)) continue;
1908
+ let dir = await realInside(home, folder);
1909
+ while (dir && dir !== home.real) {
1910
+ if (await isGitRepo(dir)) {
1911
+ add(dir, "CONVERSATION");
1912
+ break;
1913
+ }
1914
+ const up = (0, import_path4.resolve)(dir, "..");
1915
+ if (up === dir) break;
1916
+ dir = up;
1917
+ }
1918
+ }
1919
+ const roots = [];
1920
+ for (const root of DEV_ROOTS) {
1921
+ const real = await realInside(home, (0, import_path4.join)(home.logical, root));
1922
+ const st = real ? await (0, import_promises4.stat)(real).catch(() => null) : null;
1923
+ if (real && real !== home.real && st?.isDirectory() && !roots.includes(real)) roots.push(real);
1924
+ }
1925
+ const devFound = [];
1926
+ let level = [];
1927
+ for (const root of roots) {
1928
+ if (await isGitRepo(root)) devFound.push(root);
1929
+ else level.push(root);
1930
+ }
1931
+ for (let depth = 1; depth <= SUGGEST_DEPTH && level.length && !truncated; depth++) {
1932
+ const next = [];
1933
+ const finished = await eachUntil(level, deadline, async (dir) => {
1934
+ const { names } = await subfolderNames(dir, false);
1935
+ for (const { name, link } of names) {
1936
+ const child = (0, import_path4.join)(dir, name);
1937
+ const real = link ? await realInside(home, child) : child;
1938
+ if (!real || real === home.real) continue;
1939
+ if (await isGitRepo(real)) devFound.push(real);
1940
+ else if (depth < SUGGEST_DEPTH) next.push(real);
1941
+ }
1942
+ });
1943
+ if (!finished) truncated = true;
1944
+ if (devFound.length + out.length >= limit * 2) {
1945
+ if (next.length) truncated = true;
1946
+ break;
1947
+ }
1948
+ level = next;
1949
+ }
1950
+ devFound.sort((a, b) => (0, import_path4.relative)(home.real, a).localeCompare((0, import_path4.relative)(home.real, b), void 0, { sensitivity: "base", numeric: true }));
1951
+ for (const real of devFound) add(real, "DEV_ROOT");
1952
+ const entries = out.map(() => null);
1953
+ const done = await eachUntil(out.map((f, i) => ({ f, i })), deadline + 1e3, async ({ f, i }) => {
1954
+ const [remoteUrl, hasChildren] = await Promise.all([originRemote(f.real), hasVisibleChild(f.real)]);
1955
+ const path = logical(home, f.real);
1956
+ entries[i] = { name: path.split(/[\\/]/).pop() || path, path, isGitRepo: true, remoteUrl, hasChildren, source: f.source };
1957
+ });
1958
+ return fit({ home: "~", homePath: home.logical, entries: entries.filter((e) => !!e), truncated: truncated || !done });
1959
+ }
1960
+
1961
+ // src/application/services/workspaceSandbox/machineCli.ts
1621
1962
  var CredentialStore = class {
1622
1963
  constructor(file) {
1623
1964
  this.file = file;
@@ -1651,7 +1992,7 @@ var CredentialStore = class {
1651
1992
  }
1652
1993
  }
1653
1994
  write(data) {
1654
- (0, import_fs3.mkdirSync)((0, import_path4.dirname)(this.file), { recursive: true, mode: 448 });
1995
+ (0, import_fs3.mkdirSync)((0, import_path5.dirname)(this.file), { recursive: true, mode: 448 });
1655
1996
  const tmp = `${this.file}.${process.pid}.tmp`;
1656
1997
  (0, import_fs3.writeFileSync)(tmp, `${JSON.stringify(data, null, 2)}
1657
1998
  `, { mode: 384 });
@@ -1781,10 +2122,10 @@ var FOLDER_MESSAGES = {
1781
2122
  async function checkFolder(path, home) {
1782
2123
  const shape = folderPathProblem(path, home);
1783
2124
  if (shape) throw new FolderError(shape, FOLDER_MESSAGES[shape] ?? "That folder cannot be connected.");
1784
- const st = await (0, import_promises4.stat)(path).catch(() => null);
2125
+ const st = await (0, import_promises5.stat)(path).catch(() => null);
1785
2126
  if (!st?.isDirectory()) throw new FolderError("FOLDER_NOT_FOUND", FOLDER_MESSAGES.FOLDER_NOT_FOUND);
1786
- const real = await (0, import_promises4.realpath)(path);
1787
- const realHome = await (0, import_promises4.realpath)(home).catch(() => home);
2127
+ const real = await (0, import_promises5.realpath)(path);
2128
+ const realHome = await (0, import_promises5.realpath)(home).catch(() => home);
1788
2129
  const problem = folderPathProblem(real, realHome);
1789
2130
  if (problem) throw new FolderError(problem, FOLDER_MESSAGES[problem] ?? "That folder cannot be connected.");
1790
2131
  return real;
@@ -1792,7 +2133,7 @@ async function checkFolder(path, home) {
1792
2133
  async function folderRemote(path) {
1793
2134
  try {
1794
2135
  const top = (await git(["rev-parse", "--show-toplevel"], { cwd: path })).trim();
1795
- const realTop = await (0, import_promises4.realpath)(top).catch(() => top);
2136
+ const realTop = await (0, import_promises5.realpath)(top).catch(() => top);
1796
2137
  if (realTop !== path) return null;
1797
2138
  const url = (await git(["config", "--get", "remote.origin.url"], { cwd: path })).trim();
1798
2139
  return url ? stripRemoteCredentials(url) : null;
@@ -1813,33 +2154,42 @@ function claudeProjectDir(cwd) {
1813
2154
  }
1814
2155
  async function copyClaudeTranscript(sources, externalId, root, engineConfigDir) {
1815
2156
  if (!isExternalId(externalId)) return false;
1816
- const projects = (0, import_path4.join)(sources.claudeDir, "projects");
2157
+ const projects = (0, import_path5.join)(sources.claudeDir, "projects");
1817
2158
  let original = null;
1818
- for (const dir of await (0, import_promises4.readdir)(projects).catch(() => [])) {
1819
- const candidate = (0, import_path4.join)(projects, dir, `${externalId}.jsonl`);
1820
- const st = await (0, import_promises4.lstat)(candidate).catch(() => null);
2159
+ for (const dir of await (0, import_promises5.readdir)(projects).catch(() => [])) {
2160
+ const candidate = (0, import_path5.join)(projects, dir, `${externalId}.jsonl`);
2161
+ const st = await (0, import_promises5.lstat)(candidate).catch(() => null);
1821
2162
  if (st?.isFile()) {
1822
2163
  original = candidate;
1823
2164
  break;
1824
2165
  }
1825
2166
  }
1826
2167
  if (!original) return false;
1827
- const targetDir = (0, import_path4.join)(engineConfigDir, "projects", claudeProjectDir(root));
1828
- const target = (0, import_path4.join)(targetDir, `${externalId}.jsonl`);
2168
+ const targetDir = (0, import_path5.join)(engineConfigDir, "projects", claudeProjectDir(root));
2169
+ const target = (0, import_path5.join)(targetDir, `${externalId}.jsonl`);
1829
2170
  if ((0, import_fs3.existsSync)(target)) return true;
1830
- await (0, import_promises4.mkdir)(targetDir, { recursive: true, mode: 448 });
2171
+ await (0, import_promises5.mkdir)(targetDir, { recursive: true, mode: 448 });
1831
2172
  const partial = `${target}.${process.pid}.partial`;
1832
2173
  try {
1833
2174
  await writeScrubbedTranscript(original, partial);
1834
- await (0, import_promises4.chmod)(partial, 384).catch(() => void 0);
1835
- await (0, import_promises4.rename)(partial, target);
2175
+ await (0, import_promises5.chmod)(partial, 384).catch(() => void 0);
2176
+ await (0, import_promises5.rename)(partial, target);
1836
2177
  } catch {
1837
- await (0, import_promises4.rm)(partial, { force: true }).catch(() => void 0);
2178
+ await (0, import_promises5.rm)(partial, { force: true }).catch(() => void 0);
1838
2179
  return false;
1839
2180
  }
1840
2181
  return true;
1841
2182
  }
1842
2183
  var MAX_MACHINE_SESSIONS = 3;
2184
+ var IMPORT_COUNT_INTERVAL_MS = 6 * 60 * 6e4;
2185
+ function importableLine(c) {
2186
+ const parts = [];
2187
+ if (c.claudeCode > 0) parts.push(`${c.claudeCode} Claude Code`);
2188
+ if (c.codex > 0) parts.push(`${c.codex} Codex`);
2189
+ if (!parts.length) return null;
2190
+ const total = c.claudeCode + c.codex;
2191
+ return `Found ${parts.join(" and ")} ${total === 1 ? "conversation" : "conversations"} on this computer. You can import ${total === 1 ? "it" : "them"} from the browser.`;
2192
+ }
1843
2193
  var MachineAgent = class {
1844
2194
  constructor(deps) {
1845
2195
  this.deps = deps;
@@ -1849,6 +2199,10 @@ var MachineAgent = class {
1849
2199
  abort = new AbortController();
1850
2200
  lastState = "";
1851
2201
  stopped = false;
2202
+ importable = null;
2203
+ lastCountAt = null;
2204
+ /** The terminal line of the last count, printed once the push that carries it went through. */
2205
+ foundLine = null;
1852
2206
  credential() {
1853
2207
  return this.deps.credentials.get(this.deps.api);
1854
2208
  }
@@ -1859,11 +2213,33 @@ var MachineAgent = class {
1859
2213
  async pushState(force = false) {
1860
2214
  const credential = this.credential();
1861
2215
  if (!credential) return;
1862
- const key = JSON.stringify([credential.name, credential.folders]);
2216
+ const key = JSON.stringify([credential.name, credential.folders, this.importable]);
1863
2217
  if (!force && key === this.lastState) return;
1864
- await this.deps.client.state(await machineState(credential, this.deps.home, this.deps.info));
2218
+ const state = await machineState(credential, this.deps.home, this.deps.info);
2219
+ await this.deps.client.state(this.importable ? { ...state, importable: this.importable } : state);
1865
2220
  this.lastState = key;
1866
2221
  }
2222
+ /**
2223
+ * Counts the Claude Code and Codex conversations on this computer when `up`
2224
+ * starts and then at most once every 6 hours; the next inventory push carries
2225
+ * the two numbers. Leaves one terminal line when it starts, and again only
2226
+ * when the numbers change. A failed count is skipped until the next interval.
2227
+ */
2228
+ async countImportable() {
2229
+ const now = (this.deps.now ?? Date.now)();
2230
+ if (this.lastCountAt !== null && now - this.lastCountAt < IMPORT_COUNT_INTERVAL_MS) return;
2231
+ const first = this.lastCountAt === null;
2232
+ this.lastCountAt = now;
2233
+ let counts;
2234
+ try {
2235
+ counts = await (this.deps.countImports ?? countImports)(this.deps.sources);
2236
+ } catch {
2237
+ return;
2238
+ }
2239
+ const changed = !this.importable || this.importable.claudeCode !== counts.claudeCode || this.importable.codex !== counts.codex;
2240
+ this.importable = { claudeCode: counts.claudeCode, codex: counts.codex, countedAt: new Date(now).toISOString() };
2241
+ if (first || changed) this.foundLine = importableLine(counts);
2242
+ }
1867
2243
  stop() {
1868
2244
  this.stopped = true;
1869
2245
  this.abort.abort();
@@ -1881,9 +2257,14 @@ var MachineAgent = class {
1881
2257
  let announced = false;
1882
2258
  while (!this.stopped) {
1883
2259
  try {
2260
+ await this.countImportable();
1884
2261
  await this.pushState(!announced);
1885
2262
  if (!announced) this.deps.say("Connected. This computer is available in the ScaleQuality AI Workspace.");
1886
2263
  announced = true;
2264
+ if (this.foundLine) {
2265
+ this.deps.say(this.foundLine);
2266
+ this.foundLine = null;
2267
+ }
1887
2268
  const { commands = [] } = await this.deps.client.commands(this.deps.waitSeconds ?? 25, this.abort.signal);
1888
2269
  backoff = 1e3;
1889
2270
  for (const c of commands) await this.handle(c);
@@ -1914,6 +2295,13 @@ var MachineAgent = class {
1914
2295
  case "scan_imports":
1915
2296
  answer = { ok: true, result: { items: await scanImports(this.deps.sources) } };
1916
2297
  break;
2298
+ // The folder picker of the browser: only folder names, paths and remotes leave this computer, and only when asked.
2299
+ case "list_folders":
2300
+ answer = { ok: true, result: await listFolders(this.deps.home, p.path) };
2301
+ break;
2302
+ case "suggest_folders":
2303
+ answer = { ok: true, result: await suggestFolders(this.deps.home, this.deps.sources) };
2304
+ break;
1917
2305
  case "upload_imports":
1918
2306
  answer = await this.upload(p);
1919
2307
  break;
@@ -1921,7 +2309,7 @@ var MachineAgent = class {
1921
2309
  answer = { ok: false, error: { code: "UNKNOWN_COMMAND" } };
1922
2310
  }
1923
2311
  } catch (e) {
1924
- answer = { ok: false, error: { code: e instanceof FolderError || e instanceof LocalWorkspaceError ? e.code : "MACHINE_COMMAND_FAILED" } };
2312
+ answer = { ok: false, error: { code: e instanceof FolderError || e instanceof LocalWorkspaceError || e instanceof FolderBrowseError ? e.code : "MACHINE_COMMAND_FAILED" } };
1925
2313
  }
1926
2314
  await this.deps.client.reply(c.id, answer).catch(() => void 0);
1927
2315
  }
@@ -2025,9 +2413,9 @@ function serialPrompt(ask) {
2025
2413
  }
2026
2414
 
2027
2415
  // src/application/services/workspaceSandbox/WorkspaceEngine.ts
2028
- var import_promises6 = require("fs/promises");
2416
+ var import_promises7 = require("fs/promises");
2029
2417
  var import_fs4 = require("fs");
2030
- var import_path7 = require("path");
2418
+ var import_path8 = require("path");
2031
2419
 
2032
2420
  // src/application/services/execution/LanguageAdapter.ts
2033
2421
  var import_async_hooks = require("async_hooks");
@@ -2116,15 +2504,15 @@ var ApprovalBroker = class {
2116
2504
  return Promise.resolve(ready);
2117
2505
  }
2118
2506
  if (signal?.aborted) return Promise.resolve(null);
2119
- return new Promise((resolve6) => {
2507
+ return new Promise((resolve7) => {
2120
2508
  const onAbort = () => {
2121
2509
  this.waiting.delete(approvalId);
2122
- resolve6(null);
2510
+ resolve7(null);
2123
2511
  };
2124
2512
  signal?.addEventListener("abort", onAbort, { once: true });
2125
2513
  this.waiting.set(approvalId, (d) => {
2126
2514
  signal?.removeEventListener("abort", onAbort);
2127
- resolve6(d);
2515
+ resolve7(d);
2128
2516
  });
2129
2517
  });
2130
2518
  }
@@ -6295,8 +6683,8 @@ var coerce = {
6295
6683
  var NEVER = INVALID;
6296
6684
 
6297
6685
  // src/application/services/workspaceSandbox/toolPolicy.ts
6298
- var import_promises5 = require("fs/promises");
6299
- var import_path5 = require("path");
6686
+ var import_promises6 = require("fs/promises");
6687
+ var import_path6 = require("path");
6300
6688
  var SQ_MCP_SERVER = "scalequality";
6301
6689
  var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
6302
6690
  var DENIED_TOOLS = ["WebFetch", "WebSearch", "Task", "Agent", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
@@ -6334,7 +6722,7 @@ function bashDenial(command, opts = {}) {
6334
6722
  return null;
6335
6723
  }
6336
6724
  function inside(root, abs) {
6337
- return abs === root || abs.startsWith(root + import_path5.sep);
6725
+ return abs === root || abs.startsWith(root + import_path6.sep);
6338
6726
  }
6339
6727
  async function decideToolUse(toolName, input, ctx) {
6340
6728
  if (toolName.startsWith("mcp__")) {
@@ -6351,13 +6739,13 @@ async function decideToolUse(toolName, input, ctx) {
6351
6739
  const abs = await resolveInside(ctx.root, raw);
6352
6740
  let denied = false;
6353
6741
  for (const d of ctx.deniedRoots ?? []) {
6354
- const realDenied = await (0, import_promises5.realpath)(d).catch(() => (0, import_path5.resolve)(d));
6742
+ const realDenied = await (0, import_promises6.realpath)(d).catch(() => (0, import_path6.resolve)(d));
6355
6743
  if (abs && inside(realDenied, abs)) denied = true;
6356
6744
  }
6357
6745
  if (abs && !denied) {
6358
6746
  if (toolName in WRITE_TOOLS) {
6359
- const realRoot = await (0, import_promises5.realpath)(ctx.root).catch(() => (0, import_path5.resolve)(ctx.root));
6360
- const rel = (0, import_path5.relative)(realRoot, abs).split(import_path5.sep);
6747
+ const realRoot = await (0, import_promises6.realpath)(ctx.root).catch(() => (0, import_path6.resolve)(ctx.root));
6748
+ const rel = (0, import_path6.relative)(realRoot, abs).split(import_path6.sep);
6361
6749
  if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
6362
6750
  }
6363
6751
  return { behavior: "allow", updatedInput: input };
@@ -6365,7 +6753,7 @@ async function decideToolUse(toolName, input, ctx) {
6365
6753
  if (toolName in READ_TOOLS) {
6366
6754
  for (const extra of ctx.extraReadRoots ?? []) {
6367
6755
  const e = await resolveInside(extra, raw);
6368
- const realExtra = await (0, import_promises5.realpath)(extra).catch(() => (0, import_path5.resolve)(extra));
6756
+ const realExtra = await (0, import_promises6.realpath)(extra).catch(() => (0, import_path6.resolve)(extra));
6369
6757
  if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
6370
6758
  }
6371
6759
  }
@@ -6501,7 +6889,7 @@ function buildScaleQualityServer(sdk, host) {
6501
6889
  }
6502
6890
 
6503
6891
  // src/application/services/workspaceSandbox/sdkEventMapper.ts
6504
- var import_path6 = require("path");
6892
+ var import_path7 = require("path");
6505
6893
  var TERMINAL_TAIL_BYTES = 64 * 1024;
6506
6894
  var FILE_CHANGING = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit", "Bash"]);
6507
6895
  var SQ_TOOL_LABELS = {
@@ -6713,8 +7101,8 @@ function describeTool(name, input, root) {
6713
7101
  const s = (k) => typeof input[k] === "string" ? input[k] : "";
6714
7102
  const rel = (p) => {
6715
7103
  if (!p) return "";
6716
- if (!(0, import_path6.isAbsolute)(p)) return p;
6717
- const r = (0, import_path6.relative)(root, p);
7104
+ if (!(0, import_path7.isAbsolute)(p)) return p;
7105
+ const r = (0, import_path7.relative)(root, p);
6718
7106
  return r && !r.startsWith("..") ? r : p;
6719
7107
  };
6720
7108
  switch (name) {
@@ -7047,7 +7435,7 @@ var WorkspaceEngine = class {
7047
7435
  register(r) {
7048
7436
  const repo2 = {
7049
7437
  ...r,
7050
- measurer: this.deps.createMeasurer && !this.local ? this.deps.createMeasurer(r.repoFullName ?? (0, import_path7.basename)(r.root), r.root) : null,
7438
+ measurer: this.deps.createMeasurer && !this.local ? this.deps.createMeasurer(r.repoFullName ?? (0, import_path8.basename)(r.root), r.root) : null,
7051
7439
  lastDiff: null
7052
7440
  };
7053
7441
  this.repos.set(r.root, repo2);
@@ -7062,14 +7450,14 @@ var WorkspaceEngine = class {
7062
7450
  const short = folderName(lastSegment(repoFullName));
7063
7451
  const full = folderName(repoFullName.split("/").filter(Boolean).join("__"));
7064
7452
  const clash = this.scope.repos.some((r) => r.repoFullName !== repoFullName && folderName(lastSegment(r.repoFullName)) === short);
7065
- const privateDirs = (this.deps.privateDirs ?? []).map((d) => (0, import_path7.resolve)(d));
7453
+ const privateDirs = (this.deps.privateDirs ?? []).map((d) => (0, import_path8.resolve)(d));
7066
7454
  const taken = (name2) => {
7067
- const dir = (0, import_path7.resolve)(this.deps.root, name2);
7455
+ const dir = (0, import_path8.resolve)(this.deps.root, name2);
7068
7456
  return this.repos.has(dir) || privateDirs.includes(dir) || (0, import_fs4.existsSync)(dir);
7069
7457
  };
7070
7458
  let name = clash || taken(short) ? full : short;
7071
7459
  for (let n = 2; taken(name); n++) name = `${full}-${n}`;
7072
- return (0, import_path7.join)(this.deps.root, name);
7460
+ return (0, import_path8.join)(this.deps.root, name);
7073
7461
  }
7074
7462
  /** Clones one repository into its folder and registers it. The token is dropped either way. */
7075
7463
  async cloneInto(access, onStep) {
@@ -7079,7 +7467,7 @@ var WorkspaceEngine = class {
7079
7467
  try {
7080
7468
  prepared = await this.deps.clone(access, dir, saved, onStep);
7081
7469
  } catch (e) {
7082
- await (0, import_promises6.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
7470
+ await (0, import_promises7.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
7083
7471
  throw e;
7084
7472
  } finally {
7085
7473
  access.token = "";
@@ -7118,7 +7506,7 @@ var WorkspaceEngine = class {
7118
7506
  }
7119
7507
  if (open.length === 1) return { repo: open[0] };
7120
7508
  if (!open.length) return { error: "No repository is open in this workspace. Call list_repositories, then open_repository." };
7121
- return { error: `Several repositories are open (${open.map((o) => o.repoFullName ?? (0, import_path7.basename)(o.root)).join(", ")}). Pass repoFullName.` };
7509
+ return { error: `Several repositories are open (${open.map((o) => o.repoFullName ?? (0, import_path8.basename)(o.root)).join(", ")}). Pass repoFullName.` };
7122
7510
  }
7123
7511
  notInScope(repo2, target) {
7124
7512
  if (this.inScope(target ?? repo2.repoFullName)) return null;
@@ -7623,8 +8011,8 @@ _Measured on an earlier version of this change._`);
7623
8011
  });
7624
8012
  const opened = out?.pullRequest ?? out;
7625
8013
  const url = typeof opened?.url === "string" ? opened.url : "";
7626
- const skipped = final.skipped.length ? ` Not included: ${final.skipped.map((s) => `${s.path} (${s.reason})`).join(", ")}.` : "";
7627
- return text(url ? `Pull request opened on ${target}: ${url} (title: "${editedTitle}").${skipped}` : `The pull request request was accepted.${skipped}`);
8014
+ const skipped2 = final.skipped.length ? ` Not included: ${final.skipped.map((s) => `${s.path} (${s.reason})`).join(", ")}.` : "";
8015
+ return text(url ? `Pull request opened on ${target}: ${url} (title: "${editedTitle}").${skipped2}` : `The pull request request was accepted.${skipped2}`);
7628
8016
  } catch (e) {
7629
8017
  const status = e instanceof SessionGoneError || e instanceof TransportError ? e.status : null;
7630
8018
  if (e instanceof TransportError && e.code === REPOSITORY_NOT_IN_SCOPE) {
@@ -7800,9 +8188,9 @@ function buildQueryOptions(o) {
7800
8188
  }
7801
8189
  async function hasLocalTranscript(configDir, sessionId) {
7802
8190
  if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return false;
7803
- const projects = (0, import_path7.join)(configDir, "projects");
7804
- const dirs = await (0, import_promises6.readdir)(projects).catch(() => []);
7805
- return dirs.some((d) => (0, import_fs4.existsSync)((0, import_path7.join)(projects, d, `${sessionId}.jsonl`)));
8191
+ const projects = (0, import_path8.join)(configDir, "projects");
8192
+ const dirs = await (0, import_promises7.readdir)(projects).catch(() => []);
8193
+ return dirs.some((d) => (0, import_fs4.existsSync)((0, import_path8.join)(projects, d, `${sessionId}.jsonl`)));
7806
8194
  }
7807
8195
 
7808
8196
  // src/main/workspace-connect.ts
@@ -7818,9 +8206,9 @@ var say = (line = "") => err.write(`${line}
7818
8206
  var cliVersion = process.env.SCALEQUALITY_CLI_VERSION || "dev";
7819
8207
  var userAgent = (mode) => `scalequality-cli/${cliVersion} (${mode}; node ${process.versions.node}; ${process.platform})`;
7820
8208
  var HOME = (0, import_os2.homedir)();
7821
- var SQ_HOME = (0, import_path8.join)(HOME, ".scalequality");
7822
- var ENGINE_HOME = (0, import_path8.join)(SQ_HOME, "workspace");
7823
- var credentials = new CredentialStore((0, import_path8.join)(SQ_HOME, "credentials.json"));
8209
+ var SQ_HOME = (0, import_path9.join)(HOME, ".scalequality");
8210
+ var ENGINE_HOME = (0, import_path9.join)(SQ_HOME, "workspace");
8211
+ var credentials = new CredentialStore((0, import_path9.join)(SQ_HOME, "credentials.json"));
7824
8212
  var NotLocalSessionError = class extends Error {
7825
8213
  };
7826
8214
  function startFailure(e, api) {
@@ -7835,8 +8223,8 @@ function startFailure(e, api) {
7835
8223
  return "ScaleQuality could not start this session. Try again in a moment, or get a new code from the AI Workspace.";
7836
8224
  }
7837
8225
  function engineDirs() {
7838
- const configDir = (0, import_path8.join)(ENGINE_HOME, "claude-home");
7839
- const scratch = (0, import_path8.join)(ENGINE_HOME, "tmp");
8226
+ const configDir = (0, import_path9.join)(ENGINE_HOME, "claude-home");
8227
+ const scratch = (0, import_path9.join)(ENGINE_HOME, "tmp");
7840
8228
  (0, import_fs5.mkdirSync)(configDir, { recursive: true, mode: 448 });
7841
8229
  (0, import_fs5.mkdirSync)(scratch, { recursive: true, mode: 448 });
7842
8230
  return { configDir, scratch };
@@ -8068,7 +8456,7 @@ async function upMain(api, verbose) {
8068
8456
  onInterrupt: () => void shutdown()
8069
8457
  }));
8070
8458
  const startSession = ({ sessionId, secret, root }) => {
8071
- const label = (0, import_path8.basename)(root);
8459
+ const label = (0, import_path9.basename)(root);
8072
8460
  const prefix = style.dim(`[${label}] `);
8073
8461
  const consoleLog = new ConsoleLog(style);
8074
8462
  let resolveDone = () => void 0;
@@ -8131,7 +8519,7 @@ async function upMain(api, verbose) {
8131
8519
  }
8132
8520
  async function addMain(api, path) {
8133
8521
  try {
8134
- const real = await addFolder(credentials, api, (0, import_path8.resolve)(path ?? process.cwd()), HOME);
8522
+ const real = await addFolder(credentials, api, (0, import_path9.resolve)(path ?? process.cwd()), HOME);
8135
8523
  say(style.green(`Added ${real}.`));
8136
8524
  const credential = credentials.get(api);
8137
8525
  const client = new MachineClient(api, { token: credential.machineToken, userAgent: userAgent("add") });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in your repository folders, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {