@scalequality/cli 0.3.1 → 0.3.2
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 +18 -0
- package/dist/connect.build.json +2 -2
- package/dist/connect.cjs +68 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,24 @@ While `up` runs, the AI Workspace can:
|
|
|
42
42
|
without them. Only text is imported; tool results are left out and each tool
|
|
43
43
|
call becomes a one-line summary.
|
|
44
44
|
|
|
45
|
+
### What `up` counts to suggest an import
|
|
46
|
+
|
|
47
|
+
When `up` starts, and then at most once every 6 hours while it runs, it counts
|
|
48
|
+
the Claude Code and Codex conversations on this computer, so the AI Workspace
|
|
49
|
+
can offer to import them. It reads the same files the import list reads
|
|
50
|
+
(`~/.claude/projects/*/*.jsonl`, or `CLAUDE_CONFIG_DIR`; `~/.codex/sessions/**/rollout-*.jsonl`,
|
|
51
|
+
or `CODEX_HOME`), at most the 500 most recent, each only up to its first
|
|
52
|
+
message, and prints one line such as:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Found 28 Claude Code and 11 Codex conversations on this computer. You can import them from the browser.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Only the two numbers and the time of the count leave the computer
|
|
59
|
+
(`importable: {claudeCode, codex, countedAt}` in the computer's inventory). No
|
|
60
|
+
title, message, file name or folder name is sent until you open the import
|
|
61
|
+
list in the browser and choose what to import.
|
|
62
|
+
|
|
45
63
|
A Claude Code conversation continued on the same computer and folder resumes
|
|
46
64
|
from its own transcript: a copy without secrets is written into the engine's
|
|
47
65
|
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": "a1f9403a8d595fc75180ebd2f750f879c1cf850f4fbf38052c281d8dbaa365a8",
|
|
4
|
+
"sourceCommit": "2fdc9d6a48aaaf04ae5ae83702dcf30b64bfe628"
|
|
5
5
|
}
|
package/dist/connect.cjs
CHANGED
|
@@ -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,18 @@ 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
|
+
}
|
|
759
776
|
async function findConversation(sources, source, externalId) {
|
|
760
777
|
if (!isExternalId(externalId)) return null;
|
|
761
778
|
if (source === "CLAUDE_CODE") {
|
|
@@ -1553,7 +1570,11 @@ var MACHINE_USAGE = {
|
|
|
1553
1570
|
"Keeps this computer connected: the AI Workspace can run sessions in the",
|
|
1554
1571
|
"folders you added (scalequality add), list your Claude Code and Codex",
|
|
1555
1572
|
"conversations and import the ones you choose. Every command a session",
|
|
1556
|
-
"wants to run is confirmed in this terminal. Ctrl+C disconnects."
|
|
1573
|
+
"wants to run is confirmed in this terminal. Ctrl+C disconnects.",
|
|
1574
|
+
"",
|
|
1575
|
+
"When it starts (and at most every 6 hours), it counts your Claude Code and",
|
|
1576
|
+
"Codex conversations; only the two numbers are sent, so the AI Workspace can",
|
|
1577
|
+
"offer to import them."
|
|
1557
1578
|
].join("\n"),
|
|
1558
1579
|
add: [
|
|
1559
1580
|
"Usage: scalequality add [PATH] [--api URL]",
|
|
@@ -1840,6 +1861,15 @@ async function copyClaudeTranscript(sources, externalId, root, engineConfigDir)
|
|
|
1840
1861
|
return true;
|
|
1841
1862
|
}
|
|
1842
1863
|
var MAX_MACHINE_SESSIONS = 3;
|
|
1864
|
+
var IMPORT_COUNT_INTERVAL_MS = 6 * 60 * 6e4;
|
|
1865
|
+
function importableLine(c) {
|
|
1866
|
+
const parts = [];
|
|
1867
|
+
if (c.claudeCode > 0) parts.push(`${c.claudeCode} Claude Code`);
|
|
1868
|
+
if (c.codex > 0) parts.push(`${c.codex} Codex`);
|
|
1869
|
+
if (!parts.length) return null;
|
|
1870
|
+
const total = c.claudeCode + c.codex;
|
|
1871
|
+
return `Found ${parts.join(" and ")} ${total === 1 ? "conversation" : "conversations"} on this computer. You can import ${total === 1 ? "it" : "them"} from the browser.`;
|
|
1872
|
+
}
|
|
1843
1873
|
var MachineAgent = class {
|
|
1844
1874
|
constructor(deps) {
|
|
1845
1875
|
this.deps = deps;
|
|
@@ -1849,6 +1879,10 @@ var MachineAgent = class {
|
|
|
1849
1879
|
abort = new AbortController();
|
|
1850
1880
|
lastState = "";
|
|
1851
1881
|
stopped = false;
|
|
1882
|
+
importable = null;
|
|
1883
|
+
lastCountAt = null;
|
|
1884
|
+
/** The terminal line of the last count, printed once the push that carries it went through. */
|
|
1885
|
+
foundLine = null;
|
|
1852
1886
|
credential() {
|
|
1853
1887
|
return this.deps.credentials.get(this.deps.api);
|
|
1854
1888
|
}
|
|
@@ -1859,11 +1893,33 @@ var MachineAgent = class {
|
|
|
1859
1893
|
async pushState(force = false) {
|
|
1860
1894
|
const credential = this.credential();
|
|
1861
1895
|
if (!credential) return;
|
|
1862
|
-
const key = JSON.stringify([credential.name, credential.folders]);
|
|
1896
|
+
const key = JSON.stringify([credential.name, credential.folders, this.importable]);
|
|
1863
1897
|
if (!force && key === this.lastState) return;
|
|
1864
|
-
|
|
1898
|
+
const state = await machineState(credential, this.deps.home, this.deps.info);
|
|
1899
|
+
await this.deps.client.state(this.importable ? { ...state, importable: this.importable } : state);
|
|
1865
1900
|
this.lastState = key;
|
|
1866
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Counts the Claude Code and Codex conversations on this computer when `up`
|
|
1904
|
+
* starts and then at most once every 6 hours; the next inventory push carries
|
|
1905
|
+
* the two numbers. Leaves one terminal line when it starts, and again only
|
|
1906
|
+
* when the numbers change. A failed count is skipped until the next interval.
|
|
1907
|
+
*/
|
|
1908
|
+
async countImportable() {
|
|
1909
|
+
const now = (this.deps.now ?? Date.now)();
|
|
1910
|
+
if (this.lastCountAt !== null && now - this.lastCountAt < IMPORT_COUNT_INTERVAL_MS) return;
|
|
1911
|
+
const first = this.lastCountAt === null;
|
|
1912
|
+
this.lastCountAt = now;
|
|
1913
|
+
let counts;
|
|
1914
|
+
try {
|
|
1915
|
+
counts = await (this.deps.countImports ?? countImports)(this.deps.sources);
|
|
1916
|
+
} catch {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const changed = !this.importable || this.importable.claudeCode !== counts.claudeCode || this.importable.codex !== counts.codex;
|
|
1920
|
+
this.importable = { claudeCode: counts.claudeCode, codex: counts.codex, countedAt: new Date(now).toISOString() };
|
|
1921
|
+
if (first || changed) this.foundLine = importableLine(counts);
|
|
1922
|
+
}
|
|
1867
1923
|
stop() {
|
|
1868
1924
|
this.stopped = true;
|
|
1869
1925
|
this.abort.abort();
|
|
@@ -1881,9 +1937,14 @@ var MachineAgent = class {
|
|
|
1881
1937
|
let announced = false;
|
|
1882
1938
|
while (!this.stopped) {
|
|
1883
1939
|
try {
|
|
1940
|
+
await this.countImportable();
|
|
1884
1941
|
await this.pushState(!announced);
|
|
1885
1942
|
if (!announced) this.deps.say("Connected. This computer is available in the ScaleQuality AI Workspace.");
|
|
1886
1943
|
announced = true;
|
|
1944
|
+
if (this.foundLine) {
|
|
1945
|
+
this.deps.say(this.foundLine);
|
|
1946
|
+
this.foundLine = null;
|
|
1947
|
+
}
|
|
1887
1948
|
const { commands = [] } = await this.deps.client.commands(this.deps.waitSeconds ?? 25, this.abort.signal);
|
|
1888
1949
|
backoff = 1e3;
|
|
1889
1950
|
for (const c of commands) await this.handle(c);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalequality/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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": {
|