@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 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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "d634393c324bdcca4062aca8326e5ce8d4cd637e42bf49d37edf5510e1dae6d4",
4
- "sourceCommit": "d9d14ca31dd80a5a7bc14e489b362de9385261c3"
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 scanImports(sources) {
737
- const files = [
738
+ async function importFiles(sources) {
739
+ return [
738
740
  ...(await claudeCodeFiles(sources.claudeDir)).map((f) => ({ ...f, source: "CLAUDE_CODE" })),
739
741
  ...(await codexFiles(sources.codexDir)).map((f) => ({ ...f, source: "CODEX" }))
740
742
  ].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
743
+ }
744
+ async function scanImports(sources) {
745
+ const files = await importFiles(sources);
741
746
  const items = [];
742
747
  const seen = /* @__PURE__ */ new Set();
743
748
  for (const f of files) {
@@ -756,6 +761,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
- await this.deps.client.state(await machineState(credential, this.deps.home, this.deps.info));
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.1",
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": {