@scalequality/cli 0.3.2 → 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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "a1f9403a8d595fc75180ebd2f750f879c1cf850f4fbf38052c281d8dbaa365a8",
4
- "sourceCommit": "2fdc9d6a48aaaf04ae5ae83702dcf30b64bfe628"
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) {
@@ -773,6 +773,37 @@ async function countImports(sources) {
773
773
  }
774
774
  return counts;
775
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
+ }
776
807
  async function findConversation(sources, source, externalId) {
777
808
  if (!isExternalId(externalId)) return null;
778
809
  if (source === "CLAUDE_CODE") {
@@ -850,8 +881,8 @@ function scrubRecord(record) {
850
881
  async function writeScrubbedTranscript(source, target) {
851
882
  const out = (0, import_fs.createWriteStream)(target, { mode: 384, flags: "wx" });
852
883
  let removed = 0;
853
- const done = new Promise((resolve6, reject) => {
854
- out.on("finish", resolve6);
884
+ const done = new Promise((resolve7, reject) => {
885
+ out.on("finish", resolve7);
855
886
  out.on("error", reject);
856
887
  });
857
888
  const rl = (0, import_readline.createInterface)({ input: (0, import_fs.createReadStream)(source, { encoding: "utf8" }), crlfDelay: Infinity });
@@ -1325,13 +1356,13 @@ ${indent}`);
1325
1356
  function terminalCommandPrompt(o) {
1326
1357
  const bold = (s) => o.color ? `\x1B[1m${s}\x1B[22m` : s;
1327
1358
  const dim = (s) => o.color ? `\x1B[2m${s}\x1B[22m` : s;
1328
- return (q, signal) => new Promise((resolve6) => {
1359
+ return (q, signal) => new Promise((resolve7) => {
1329
1360
  if (!o.input.isTTY) {
1330
- resolve6(null);
1361
+ resolve7(null);
1331
1362
  return;
1332
1363
  }
1333
1364
  if (signal?.aborted) {
1334
- resolve6(null);
1365
+ resolve7(null);
1335
1366
  return;
1336
1367
  }
1337
1368
  const rl = (0, import_readline2.createInterface)({ input: o.input, output: o.output, terminal: true });
@@ -1341,7 +1372,7 @@ function terminalCommandPrompt(o) {
1341
1372
  done = true;
1342
1373
  signal?.removeEventListener("abort", onAbort);
1343
1374
  rl.close();
1344
- resolve6(a);
1375
+ resolve7(a);
1345
1376
  };
1346
1377
  const onAbort = () => {
1347
1378
  o.output.write(`
@@ -1637,8 +1668,297 @@ function parseMachineArgs(argv) {
1637
1668
 
1638
1669
  // src/application/services/workspaceSandbox/machineCli.ts
1639
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
1640
1675
  var import_promises4 = require("fs/promises");
1641
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
1642
1962
  var CredentialStore = class {
1643
1963
  constructor(file) {
1644
1964
  this.file = file;
@@ -1672,7 +1992,7 @@ var CredentialStore = class {
1672
1992
  }
1673
1993
  }
1674
1994
  write(data) {
1675
- (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 });
1676
1996
  const tmp = `${this.file}.${process.pid}.tmp`;
1677
1997
  (0, import_fs3.writeFileSync)(tmp, `${JSON.stringify(data, null, 2)}
1678
1998
  `, { mode: 384 });
@@ -1802,10 +2122,10 @@ var FOLDER_MESSAGES = {
1802
2122
  async function checkFolder(path, home) {
1803
2123
  const shape = folderPathProblem(path, home);
1804
2124
  if (shape) throw new FolderError(shape, FOLDER_MESSAGES[shape] ?? "That folder cannot be connected.");
1805
- const st = await (0, import_promises4.stat)(path).catch(() => null);
2125
+ const st = await (0, import_promises5.stat)(path).catch(() => null);
1806
2126
  if (!st?.isDirectory()) throw new FolderError("FOLDER_NOT_FOUND", FOLDER_MESSAGES.FOLDER_NOT_FOUND);
1807
- const real = await (0, import_promises4.realpath)(path);
1808
- 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);
1809
2129
  const problem = folderPathProblem(real, realHome);
1810
2130
  if (problem) throw new FolderError(problem, FOLDER_MESSAGES[problem] ?? "That folder cannot be connected.");
1811
2131
  return real;
@@ -1813,7 +2133,7 @@ async function checkFolder(path, home) {
1813
2133
  async function folderRemote(path) {
1814
2134
  try {
1815
2135
  const top = (await git(["rev-parse", "--show-toplevel"], { cwd: path })).trim();
1816
- const realTop = await (0, import_promises4.realpath)(top).catch(() => top);
2136
+ const realTop = await (0, import_promises5.realpath)(top).catch(() => top);
1817
2137
  if (realTop !== path) return null;
1818
2138
  const url = (await git(["config", "--get", "remote.origin.url"], { cwd: path })).trim();
1819
2139
  return url ? stripRemoteCredentials(url) : null;
@@ -1834,28 +2154,28 @@ function claudeProjectDir(cwd) {
1834
2154
  }
1835
2155
  async function copyClaudeTranscript(sources, externalId, root, engineConfigDir) {
1836
2156
  if (!isExternalId(externalId)) return false;
1837
- const projects = (0, import_path4.join)(sources.claudeDir, "projects");
2157
+ const projects = (0, import_path5.join)(sources.claudeDir, "projects");
1838
2158
  let original = null;
1839
- for (const dir of await (0, import_promises4.readdir)(projects).catch(() => [])) {
1840
- const candidate = (0, import_path4.join)(projects, dir, `${externalId}.jsonl`);
1841
- 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);
1842
2162
  if (st?.isFile()) {
1843
2163
  original = candidate;
1844
2164
  break;
1845
2165
  }
1846
2166
  }
1847
2167
  if (!original) return false;
1848
- const targetDir = (0, import_path4.join)(engineConfigDir, "projects", claudeProjectDir(root));
1849
- 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`);
1850
2170
  if ((0, import_fs3.existsSync)(target)) return true;
1851
- await (0, import_promises4.mkdir)(targetDir, { recursive: true, mode: 448 });
2171
+ await (0, import_promises5.mkdir)(targetDir, { recursive: true, mode: 448 });
1852
2172
  const partial = `${target}.${process.pid}.partial`;
1853
2173
  try {
1854
2174
  await writeScrubbedTranscript(original, partial);
1855
- await (0, import_promises4.chmod)(partial, 384).catch(() => void 0);
1856
- 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);
1857
2177
  } catch {
1858
- await (0, import_promises4.rm)(partial, { force: true }).catch(() => void 0);
2178
+ await (0, import_promises5.rm)(partial, { force: true }).catch(() => void 0);
1859
2179
  return false;
1860
2180
  }
1861
2181
  return true;
@@ -1975,6 +2295,13 @@ var MachineAgent = class {
1975
2295
  case "scan_imports":
1976
2296
  answer = { ok: true, result: { items: await scanImports(this.deps.sources) } };
1977
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;
1978
2305
  case "upload_imports":
1979
2306
  answer = await this.upload(p);
1980
2307
  break;
@@ -1982,7 +2309,7 @@ var MachineAgent = class {
1982
2309
  answer = { ok: false, error: { code: "UNKNOWN_COMMAND" } };
1983
2310
  }
1984
2311
  } catch (e) {
1985
- 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" } };
1986
2313
  }
1987
2314
  await this.deps.client.reply(c.id, answer).catch(() => void 0);
1988
2315
  }
@@ -2086,9 +2413,9 @@ function serialPrompt(ask) {
2086
2413
  }
2087
2414
 
2088
2415
  // src/application/services/workspaceSandbox/WorkspaceEngine.ts
2089
- var import_promises6 = require("fs/promises");
2416
+ var import_promises7 = require("fs/promises");
2090
2417
  var import_fs4 = require("fs");
2091
- var import_path7 = require("path");
2418
+ var import_path8 = require("path");
2092
2419
 
2093
2420
  // src/application/services/execution/LanguageAdapter.ts
2094
2421
  var import_async_hooks = require("async_hooks");
@@ -2177,15 +2504,15 @@ var ApprovalBroker = class {
2177
2504
  return Promise.resolve(ready);
2178
2505
  }
2179
2506
  if (signal?.aborted) return Promise.resolve(null);
2180
- return new Promise((resolve6) => {
2507
+ return new Promise((resolve7) => {
2181
2508
  const onAbort = () => {
2182
2509
  this.waiting.delete(approvalId);
2183
- resolve6(null);
2510
+ resolve7(null);
2184
2511
  };
2185
2512
  signal?.addEventListener("abort", onAbort, { once: true });
2186
2513
  this.waiting.set(approvalId, (d) => {
2187
2514
  signal?.removeEventListener("abort", onAbort);
2188
- resolve6(d);
2515
+ resolve7(d);
2189
2516
  });
2190
2517
  });
2191
2518
  }
@@ -6356,8 +6683,8 @@ var coerce = {
6356
6683
  var NEVER = INVALID;
6357
6684
 
6358
6685
  // src/application/services/workspaceSandbox/toolPolicy.ts
6359
- var import_promises5 = require("fs/promises");
6360
- var import_path5 = require("path");
6686
+ var import_promises6 = require("fs/promises");
6687
+ var import_path6 = require("path");
6361
6688
  var SQ_MCP_SERVER = "scalequality";
6362
6689
  var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
6363
6690
  var DENIED_TOOLS = ["WebFetch", "WebSearch", "Task", "Agent", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
@@ -6395,7 +6722,7 @@ function bashDenial(command, opts = {}) {
6395
6722
  return null;
6396
6723
  }
6397
6724
  function inside(root, abs) {
6398
- return abs === root || abs.startsWith(root + import_path5.sep);
6725
+ return abs === root || abs.startsWith(root + import_path6.sep);
6399
6726
  }
6400
6727
  async function decideToolUse(toolName, input, ctx) {
6401
6728
  if (toolName.startsWith("mcp__")) {
@@ -6412,13 +6739,13 @@ async function decideToolUse(toolName, input, ctx) {
6412
6739
  const abs = await resolveInside(ctx.root, raw);
6413
6740
  let denied = false;
6414
6741
  for (const d of ctx.deniedRoots ?? []) {
6415
- 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));
6416
6743
  if (abs && inside(realDenied, abs)) denied = true;
6417
6744
  }
6418
6745
  if (abs && !denied) {
6419
6746
  if (toolName in WRITE_TOOLS) {
6420
- const realRoot = await (0, import_promises5.realpath)(ctx.root).catch(() => (0, import_path5.resolve)(ctx.root));
6421
- 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);
6422
6749
  if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
6423
6750
  }
6424
6751
  return { behavior: "allow", updatedInput: input };
@@ -6426,7 +6753,7 @@ async function decideToolUse(toolName, input, ctx) {
6426
6753
  if (toolName in READ_TOOLS) {
6427
6754
  for (const extra of ctx.extraReadRoots ?? []) {
6428
6755
  const e = await resolveInside(extra, raw);
6429
- 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));
6430
6757
  if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
6431
6758
  }
6432
6759
  }
@@ -6562,7 +6889,7 @@ function buildScaleQualityServer(sdk, host) {
6562
6889
  }
6563
6890
 
6564
6891
  // src/application/services/workspaceSandbox/sdkEventMapper.ts
6565
- var import_path6 = require("path");
6892
+ var import_path7 = require("path");
6566
6893
  var TERMINAL_TAIL_BYTES = 64 * 1024;
6567
6894
  var FILE_CHANGING = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit", "Bash"]);
6568
6895
  var SQ_TOOL_LABELS = {
@@ -6774,8 +7101,8 @@ function describeTool(name, input, root) {
6774
7101
  const s = (k) => typeof input[k] === "string" ? input[k] : "";
6775
7102
  const rel = (p) => {
6776
7103
  if (!p) return "";
6777
- if (!(0, import_path6.isAbsolute)(p)) return p;
6778
- 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);
6779
7106
  return r && !r.startsWith("..") ? r : p;
6780
7107
  };
6781
7108
  switch (name) {
@@ -7108,7 +7435,7 @@ var WorkspaceEngine = class {
7108
7435
  register(r) {
7109
7436
  const repo2 = {
7110
7437
  ...r,
7111
- 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,
7112
7439
  lastDiff: null
7113
7440
  };
7114
7441
  this.repos.set(r.root, repo2);
@@ -7123,14 +7450,14 @@ var WorkspaceEngine = class {
7123
7450
  const short = folderName(lastSegment(repoFullName));
7124
7451
  const full = folderName(repoFullName.split("/").filter(Boolean).join("__"));
7125
7452
  const clash = this.scope.repos.some((r) => r.repoFullName !== repoFullName && folderName(lastSegment(r.repoFullName)) === short);
7126
- 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));
7127
7454
  const taken = (name2) => {
7128
- const dir = (0, import_path7.resolve)(this.deps.root, name2);
7455
+ const dir = (0, import_path8.resolve)(this.deps.root, name2);
7129
7456
  return this.repos.has(dir) || privateDirs.includes(dir) || (0, import_fs4.existsSync)(dir);
7130
7457
  };
7131
7458
  let name = clash || taken(short) ? full : short;
7132
7459
  for (let n = 2; taken(name); n++) name = `${full}-${n}`;
7133
- return (0, import_path7.join)(this.deps.root, name);
7460
+ return (0, import_path8.join)(this.deps.root, name);
7134
7461
  }
7135
7462
  /** Clones one repository into its folder and registers it. The token is dropped either way. */
7136
7463
  async cloneInto(access, onStep) {
@@ -7140,7 +7467,7 @@ var WorkspaceEngine = class {
7140
7467
  try {
7141
7468
  prepared = await this.deps.clone(access, dir, saved, onStep);
7142
7469
  } catch (e) {
7143
- 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);
7144
7471
  throw e;
7145
7472
  } finally {
7146
7473
  access.token = "";
@@ -7179,7 +7506,7 @@ var WorkspaceEngine = class {
7179
7506
  }
7180
7507
  if (open.length === 1) return { repo: open[0] };
7181
7508
  if (!open.length) return { error: "No repository is open in this workspace. Call list_repositories, then open_repository." };
7182
- 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.` };
7183
7510
  }
7184
7511
  notInScope(repo2, target) {
7185
7512
  if (this.inScope(target ?? repo2.repoFullName)) return null;
@@ -7684,8 +8011,8 @@ _Measured on an earlier version of this change._`);
7684
8011
  });
7685
8012
  const opened = out?.pullRequest ?? out;
7686
8013
  const url = typeof opened?.url === "string" ? opened.url : "";
7687
- const skipped = final.skipped.length ? ` Not included: ${final.skipped.map((s) => `${s.path} (${s.reason})`).join(", ")}.` : "";
7688
- 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}`);
7689
8016
  } catch (e) {
7690
8017
  const status = e instanceof SessionGoneError || e instanceof TransportError ? e.status : null;
7691
8018
  if (e instanceof TransportError && e.code === REPOSITORY_NOT_IN_SCOPE) {
@@ -7861,9 +8188,9 @@ function buildQueryOptions(o) {
7861
8188
  }
7862
8189
  async function hasLocalTranscript(configDir, sessionId) {
7863
8190
  if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return false;
7864
- const projects = (0, import_path7.join)(configDir, "projects");
7865
- const dirs = await (0, import_promises6.readdir)(projects).catch(() => []);
7866
- 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`)));
7867
8194
  }
7868
8195
 
7869
8196
  // src/main/workspace-connect.ts
@@ -7879,9 +8206,9 @@ var say = (line = "") => err.write(`${line}
7879
8206
  var cliVersion = process.env.SCALEQUALITY_CLI_VERSION || "dev";
7880
8207
  var userAgent = (mode) => `scalequality-cli/${cliVersion} (${mode}; node ${process.versions.node}; ${process.platform})`;
7881
8208
  var HOME = (0, import_os2.homedir)();
7882
- var SQ_HOME = (0, import_path8.join)(HOME, ".scalequality");
7883
- var ENGINE_HOME = (0, import_path8.join)(SQ_HOME, "workspace");
7884
- 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"));
7885
8212
  var NotLocalSessionError = class extends Error {
7886
8213
  };
7887
8214
  function startFailure(e, api) {
@@ -7896,8 +8223,8 @@ function startFailure(e, api) {
7896
8223
  return "ScaleQuality could not start this session. Try again in a moment, or get a new code from the AI Workspace.";
7897
8224
  }
7898
8225
  function engineDirs() {
7899
- const configDir = (0, import_path8.join)(ENGINE_HOME, "claude-home");
7900
- 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");
7901
8228
  (0, import_fs5.mkdirSync)(configDir, { recursive: true, mode: 448 });
7902
8229
  (0, import_fs5.mkdirSync)(scratch, { recursive: true, mode: 448 });
7903
8230
  return { configDir, scratch };
@@ -8129,7 +8456,7 @@ async function upMain(api, verbose) {
8129
8456
  onInterrupt: () => void shutdown()
8130
8457
  }));
8131
8458
  const startSession = ({ sessionId, secret, root }) => {
8132
- const label = (0, import_path8.basename)(root);
8459
+ const label = (0, import_path9.basename)(root);
8133
8460
  const prefix = style.dim(`[${label}] `);
8134
8461
  const consoleLog = new ConsoleLog(style);
8135
8462
  let resolveDone = () => void 0;
@@ -8192,7 +8519,7 @@ async function upMain(api, verbose) {
8192
8519
  }
8193
8520
  async function addMain(api, path) {
8194
8521
  try {
8195
- 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);
8196
8523
  say(style.green(`Added ${real}.`));
8197
8524
  const credential = credentials.get(api);
8198
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.2",
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": {