@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 +25 -0
- package/dist/connect.build.json +2 -2
- package/dist/connect.cjs +456 -68
- package/package.json +1 -1
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.
|
package/dist/connect.build.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sdkVersion": "0.3.281",
|
|
3
|
-
"sha256": "
|
|
4
|
-
"sourceCommit": "
|
|
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
|
|
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((
|
|
380
|
-
const t = setTimeout(
|
|
379
|
+
return new Promise((resolve7) => {
|
|
380
|
+
const t = setTimeout(resolve7, ms);
|
|
381
381
|
signal?.addEventListener("abort", () => {
|
|
382
382
|
clearTimeout(t);
|
|
383
|
-
|
|
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,
|
|
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,
|
|
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
|
|
737
|
-
|
|
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((
|
|
837
|
-
out.on("finish",
|
|
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((
|
|
1359
|
+
return (q, signal) => new Promise((resolve7) => {
|
|
1312
1360
|
if (!o.input.isTTY) {
|
|
1313
|
-
|
|
1361
|
+
resolve7(null);
|
|
1314
1362
|
return;
|
|
1315
1363
|
}
|
|
1316
1364
|
if (signal?.aborted) {
|
|
1317
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
1787
|
-
const realHome = await (0,
|
|
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,
|
|
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,
|
|
2157
|
+
const projects = (0, import_path5.join)(sources.claudeDir, "projects");
|
|
1817
2158
|
let original = null;
|
|
1818
|
-
for (const dir of await (0,
|
|
1819
|
-
const candidate = (0,
|
|
1820
|
-
const st = await (0,
|
|
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,
|
|
1828
|
-
const target = (0,
|
|
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,
|
|
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,
|
|
1835
|
-
await (0,
|
|
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,
|
|
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
|
-
|
|
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
|
|
2416
|
+
var import_promises7 = require("fs/promises");
|
|
2029
2417
|
var import_fs4 = require("fs");
|
|
2030
|
-
var
|
|
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((
|
|
2507
|
+
return new Promise((resolve7) => {
|
|
2120
2508
|
const onAbort = () => {
|
|
2121
2509
|
this.waiting.delete(approvalId);
|
|
2122
|
-
|
|
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
|
-
|
|
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
|
|
6299
|
-
var
|
|
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 +
|
|
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,
|
|
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,
|
|
6360
|
-
const rel = (0,
|
|
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,
|
|
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
|
|
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,
|
|
6717
|
-
const r = (0,
|
|
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,
|
|
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,
|
|
7453
|
+
const privateDirs = (this.deps.privateDirs ?? []).map((d) => (0, import_path8.resolve)(d));
|
|
7066
7454
|
const taken = (name2) => {
|
|
7067
|
-
const dir = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
7627
|
-
return text(url ? `Pull request opened on ${target}: ${url} (title: "${editedTitle}").${
|
|
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,
|
|
7804
|
-
const dirs = await (0,
|
|
7805
|
-
return dirs.some((d) => (0, import_fs4.existsSync)((0,
|
|
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,
|
|
7822
|
-
var ENGINE_HOME = (0,
|
|
7823
|
-
var credentials = new CredentialStore((0,
|
|
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,
|
|
7839
|
-
const scratch = (0,
|
|
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,
|
|
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,
|
|
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.
|
|
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": {
|