@teammy/cli 0.0.9 → 0.0.10

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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/dist/main.js +615 -256
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // apps/client/main.ts
2
2
  import { parseArgs as parseArgs4 } from "node:util";
3
- import { resolve as resolve8 } from "node:path";
3
+ import { resolve as resolve9 } from "node:path";
4
4
 
5
5
  // packages/protocol/project.ts
6
6
  import { z as z3 } from "zod";
@@ -518,10 +518,10 @@ var HubClient = class {
518
518
  };
519
519
 
520
520
  // apps/client/sessions/command.ts
521
- import { createHash as createHash6 } from "node:crypto";
521
+ import { createHash as createHash7 } from "node:crypto";
522
522
  import { realpath as realpath4 } from "node:fs/promises";
523
- import { homedir } from "node:os";
524
- import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
523
+ import { homedir as homedir2 } from "node:os";
524
+ import { dirname as dirname5, join as join7, resolve as resolve4 } from "node:path";
525
525
  import { parseArgs } from "node:util";
526
526
 
527
527
  // packages/state/sqlite.ts
@@ -1513,6 +1513,28 @@ var sessionClientMigration = String.raw`
1513
1513
  root TEXT NOT NULL, path TEXT NOT NULL, signature TEXT NOT NULL, PRIMARY KEY(root, path)
1514
1514
  );
1515
1515
  `;
1516
+ var sessionSourcesMigration = String.raw`
1517
+ CREATE TABLE session_hook_receipts (root TEXT NOT NULL, agent TEXT NOT NULL, received_at INTEGER NOT NULL, PRIMARY KEY(root,agent));
1518
+ CREATE TABLE session_publications (
1519
+ root TEXT NOT NULL REFERENCES connections(root), session_key TEXT NOT NULL,
1520
+ revision INTEGER NOT NULL DEFAULT 0, hash TEXT, selected_path TEXT,
1521
+ pending_json TEXT, pending_signature TEXT, pending_path TEXT,
1522
+ attempts INTEGER NOT NULL DEFAULT 0, retry_at INTEGER NOT NULL DEFAULT 0,
1523
+ PRIMARY KEY(root,session_key)
1524
+ );
1525
+ INSERT INTO session_publications(root,session_key,revision,hash,selected_path,pending_json,pending_signature,pending_path,attempts,retry_at)
1526
+ SELECT root,session_key,revision,hash,path,pending_json,pending_signature,path,attempts,retry_at FROM session_sources WHERE session_key IS NOT NULL;
1527
+ ALTER TABLE session_sources RENAME TO session_sources_old;
1528
+ DROP INDEX session_source_scheduling;
1529
+ CREATE TABLE session_sources (
1530
+ root TEXT NOT NULL REFERENCES connections(root), path TEXT NOT NULL, agent TEXT NOT NULL, project_dir TEXT NOT NULL,
1531
+ session_key TEXT, signature TEXT, checked_at INTEGER NOT NULL DEFAULT 0, verified_at INTEGER NOT NULL DEFAULT 0,
1532
+ last_error TEXT, adopt_existing INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(root,path)
1533
+ );
1534
+ INSERT INTO session_sources SELECT root,path,agent,project_dir,session_key,signature,checked_at,verified_at,last_error,adopt_existing FROM session_sources_old;
1535
+ DROP TABLE session_sources_old;
1536
+ CREATE INDEX session_source_scheduling ON session_sources(root,checked_at,path);
1537
+ `;
1516
1538
  var SessionState = class {
1517
1539
  constructor(state, root) {
1518
1540
  this.state = state;
@@ -1534,23 +1556,27 @@ var SessionState = class {
1534
1556
  }
1535
1557
  sources(path, limit = 2147483647) {
1536
1558
  return this.state.db.prepare(
1537
- `SELECT * FROM session_sources WHERE root=? ${path === void 0 ? "" : "AND path=?"} ORDER BY checked_at, path LIMIT ?`
1559
+ `SELECT s.*, p.revision,p.hash,p.selected_path,p.pending_json,p.pending_signature,p.pending_path,p.attempts,p.retry_at
1560
+ FROM session_sources s LEFT JOIN session_publications p USING(root,session_key)
1561
+ WHERE s.root=? ${path === void 0 ? "" : "AND s.path=?"} ORDER BY s.checked_at, s.path LIMIT ?`
1538
1562
  ).all(this.root, ...path === void 0 ? [] : [path], limit).map((row) => ({
1539
1563
  path: String(row.path),
1540
1564
  agent: row.agent,
1541
1565
  projectDir: String(row.project_dir),
1542
1566
  key: row.session_key,
1543
- revision: Number(row.revision),
1544
- hash: row.hash,
1567
+ revision: Number(row.revision ?? 0),
1568
+ hash: row.hash ?? null,
1545
1569
  signature: row.signature,
1546
1570
  checkedAt: Number(row.checked_at),
1547
1571
  verifiedAt: Number(row.verified_at),
1548
1572
  lastError: row.last_error,
1549
1573
  adoptExisting: Boolean(row.adopt_existing),
1550
1574
  pending: row.pending_json ? SessionCommitSchema.parse(JSON.parse(String(row.pending_json))) : null,
1551
- pendingSignature: row.pending_signature,
1552
- attempts: Number(row.attempts),
1553
- retryAt: Number(row.retry_at)
1575
+ pendingSignature: row.pending_signature ?? null,
1576
+ pendingPath: row.pending_path ?? null,
1577
+ selectedPath: row.selected_path ?? null,
1578
+ attempts: Number(row.attempts ?? 0),
1579
+ retryAt: Number(row.retry_at ?? 0)
1554
1580
  }));
1555
1581
  }
1556
1582
  status() {
@@ -1560,32 +1586,61 @@ var SessionState = class {
1560
1586
  }));
1561
1587
  }
1562
1588
  untrack(path) {
1563
- this.state.db.prepare("DELETE FROM session_sources WHERE root=? AND path=?").run(this.root, path);
1589
+ transaction(this.state.db, () => {
1590
+ const source = this.get(path);
1591
+ this.state.db.prepare("DELETE FROM session_sources WHERE root=? AND path=?").run(this.root, path);
1592
+ if (!source?.key) return;
1593
+ if (!this.state.db.prepare("SELECT 1 FROM session_sources WHERE root=? AND session_key=?").get(this.root, source.key)) {
1594
+ this.state.db.prepare("DELETE FROM session_publications WHERE root=? AND session_key=?").run(this.root, source.key);
1595
+ } else {
1596
+ this.state.db.prepare(
1597
+ "UPDATE session_publications SET selected_path=NULL WHERE root=? AND session_key=? AND selected_path=?"
1598
+ ).run(this.root, source.key, path);
1599
+ this.state.db.prepare(
1600
+ "UPDATE session_publications SET pending_json=NULL,pending_signature=NULL,pending_path=NULL,attempts=0,retry_at=0 WHERE root=? AND session_key=? AND pending_path=?"
1601
+ ).run(this.root, source.key, path);
1602
+ }
1603
+ });
1564
1604
  }
1565
1605
  checked(path, error = null) {
1566
- this.state.db.prepare("UPDATE session_sources SET checked_at=?, last_error=? WHERE root=? AND path=?").run(Date.now(), error, this.root, path);
1606
+ this.state.db.prepare("UPDATE session_sources SET checked_at=?,last_error=? WHERE root=? AND path=?").run(Date.now(), error, this.root, path);
1567
1607
  }
1568
1608
  stage(path, request, signature2) {
1569
1609
  this.state.db.prepare(
1570
- "UPDATE session_sources SET session_key=?, pending_json=?, pending_signature=?, attempts=0, retry_at=0 WHERE root=? AND path=? AND pending_json IS NULL"
1571
- ).run(request.sessionKey, JSON.stringify(request), signature2, this.root, path);
1610
+ `UPDATE session_publications SET pending_json=?,pending_signature=?,pending_path=?,attempts=0,retry_at=0
1611
+ WHERE root=? AND session_key=? AND pending_json IS NULL`
1612
+ ).run(JSON.stringify(request), signature2, path, this.root, request.sessionKey);
1572
1613
  }
1573
1614
  acknowledge(path, revision, hash2, signature2) {
1615
+ const source = this.get(path);
1616
+ this.state.db.prepare(
1617
+ `UPDATE session_publications SET revision=?,hash=?,selected_path=?,pending_json=NULL,pending_signature=NULL,pending_path=NULL,attempts=0,retry_at=0 WHERE root=? AND session_key=?`
1618
+ ).run(revision, hash2, path, this.root, source.key);
1619
+ this.observed(path, signature2);
1620
+ }
1621
+ observed(path, signature2) {
1574
1622
  this.state.db.prepare(
1575
- `UPDATE session_sources SET revision=?, hash=?, signature=?, verified_at=?, pending_json=NULL, pending_signature=NULL, attempts=0, retry_at=0, last_error=NULL, adopt_existing=0 WHERE root=? AND path=?`
1576
- ).run(revision, hash2, signature2, Date.now(), this.root, path);
1623
+ "UPDATE session_sources SET signature=?,verified_at=?,last_error=NULL,adopt_existing=0 WHERE root=? AND path=?"
1624
+ ).run(signature2, Date.now(), this.root, path);
1577
1625
  }
1578
1626
  baseline(path, key, revision) {
1579
- this.state.db.prepare("UPDATE session_sources SET session_key=?, revision=? WHERE root=? AND path=?").run(key, revision, this.root, path);
1627
+ this.state.db.prepare(
1628
+ "INSERT OR IGNORE INTO session_publications(root,session_key,revision) VALUES (?,?,?)"
1629
+ ).run(this.root, key, revision);
1630
+ this.state.db.prepare("UPDATE session_sources SET session_key=? WHERE root=? AND path=?").run(key, this.root, path);
1631
+ }
1632
+ publication(key) {
1633
+ return this.state.db.prepare("SELECT 1 FROM session_publications WHERE root=? AND session_key=?").get(this.root, key);
1580
1634
  }
1581
1635
  retry() {
1582
- this.state.db.prepare("UPDATE session_sources SET retry_at=0 WHERE root=?").run(this.root);
1636
+ this.state.db.prepare("UPDATE session_publications SET retry_at=0 WHERE root=?").run(this.root);
1583
1637
  }
1584
1638
  postpone(source, error, blocked) {
1585
1639
  const at = blocked ? Number.MAX_SAFE_INTEGER : Date.now() + Math.min(9e5, 6e4 * 2 ** Math.min(source.attempts, 4));
1586
1640
  this.state.db.prepare(
1587
- "UPDATE session_sources SET retry_at=?, attempts=attempts+1, last_error=? WHERE root=? AND path=?"
1588
- ).run(at, error, this.root, source.path);
1641
+ "UPDATE session_publications SET retry_at=?,attempts=attempts+1 WHERE root=? AND session_key=?"
1642
+ ).run(at, this.root, source.key);
1643
+ this.checked(source.path, error);
1589
1644
  }
1590
1645
  discover(agent, directory, projectDir) {
1591
1646
  this.state.db.prepare(
@@ -1593,6 +1648,14 @@ var SessionState = class {
1593
1648
  ).run(this.root, directory, agent, projectDir);
1594
1649
  this.state.db.prepare("DELETE FROM session_discovery_cache WHERE root=?").run(this.root);
1595
1650
  }
1651
+ receivedHook(agent) {
1652
+ this.state.db.prepare(
1653
+ "INSERT INTO session_hook_receipts VALUES (?,?,?) ON CONFLICT(root,agent) DO UPDATE SET received_at=excluded.received_at"
1654
+ ).run(this.root, agent, Date.now());
1655
+ }
1656
+ hooks() {
1657
+ return this.state.db.prepare("SELECT agent,received_at FROM session_hook_receipts WHERE root=? ORDER BY agent").all(this.root);
1658
+ }
1596
1659
  directories() {
1597
1660
  return this.state.db.prepare("SELECT * FROM session_discovery WHERE root=? ORDER BY directory").all(this.root).map((row) => ({
1598
1661
  directory: String(row.directory),
@@ -1624,7 +1687,8 @@ var ClientState = class extends FilePublicationState {
1624
1687
  projectClientMigration,
1625
1688
  controlClientMigration,
1626
1689
  runtimeMigration,
1627
- projectFilesClientMigration
1690
+ projectFilesClientMigration,
1691
+ sessionSourcesMigration
1628
1692
  ]);
1629
1693
  super(db);
1630
1694
  }
@@ -1687,11 +1751,14 @@ async function stateLocation(root, directory) {
1687
1751
  return canonical;
1688
1752
  }
1689
1753
 
1690
- // apps/client/sessions/hooks.ts
1691
- import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
1692
- import { mkdir as mkdir4, open as open2, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
1693
- import { isAbsolute as isAbsolute3, join as join5 } from "node:path";
1694
- import { z as z10 } from "zod";
1754
+ // apps/client/sessions/setup.ts
1755
+ import { execFile as execFile2 } from "node:child_process";
1756
+ import { promisify as promisify2 } from "node:util";
1757
+ import { access, mkdir as mkdir4, readFile, writeFile } from "node:fs/promises";
1758
+ import { constants as constants2 } from "node:fs";
1759
+ import { homedir } from "node:os";
1760
+ import { dirname as dirname4, join as join5, resolve as resolve3 } from "node:path";
1761
+ import { createHash as createHash5 } from "node:crypto";
1695
1762
 
1696
1763
  // apps/client/sessions/normalize.ts
1697
1764
  function object(value) {
@@ -1702,91 +1769,104 @@ function textContent(value) {
1702
1769
  if (!Array.isArray(value)) return "";
1703
1770
  return value.flatMap((block) => {
1704
1771
  const part = object(block);
1705
- return ["text", "input_text", "output_text"].includes(String(part.type)) && typeof part.text === "string" ? [part.text] : [];
1772
+ return ["text", "Text", "input_text", "output_text"].includes(String(part.type)) && typeof part.text === "string" ? [part.text] : [];
1706
1773
  }).join("\n");
1707
1774
  }
1708
- function parseJsonl(text) {
1709
- const lines = text.split("\n");
1710
- const rows = [];
1711
- for (let index = 0; index < lines.length; index++) {
1712
- const line = lines[index];
1713
- if (!line.trim()) continue;
1714
- if (Buffer.byteLength(line) > 8 * 1024 * 1024) throw new Error("Session record exceeds 8 MiB.");
1715
- let parsed;
1716
- try {
1717
- parsed = JSON.parse(line);
1718
- } catch {
1719
- if (index === lines.length - 1 && !text.endsWith("\n")) return { rows, incomplete: true };
1720
- throw new Error(`Invalid session JSON at line ${index + 1}.`);
1721
- }
1722
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1723
- throw new Error(`Invalid session record at line ${index + 1}.`);
1724
- rows.push({ line: index + 1, value: object(parsed) });
1775
+ function parseRecord(line) {
1776
+ try {
1777
+ const value = JSON.parse(line);
1778
+ if (value && typeof value === "object" && !Array.isArray(value)) return object(value);
1779
+ } catch {
1725
1780
  }
1726
- return { rows, incomplete: false };
1781
+ return void 0;
1727
1782
  }
1728
- function normalizeJsonl(agent, text) {
1729
- const { rows, incomplete } = parseJsonl(text);
1730
- let nativeId;
1731
- let cwd;
1732
- const messages = [];
1733
- const ids = /* @__PURE__ */ new Map();
1734
- const add = (role, content, id, line) => {
1783
+ var JsonlNormalizer = class {
1784
+ constructor(agent, cursor) {
1785
+ this.agent = agent;
1786
+ if (agent === "cursor" && cursor) {
1787
+ this.nativeId = cursor.nativeId;
1788
+ this.cwd = cursor.cwd;
1789
+ }
1790
+ }
1791
+ agent;
1792
+ nativeId;
1793
+ cwd;
1794
+ turn = "legacy";
1795
+ candidates = [];
1796
+ add(role, content, id, line, visible = false) {
1735
1797
  if (role !== "user" && role !== "assistant") return;
1736
- const text2 = textContent(content);
1737
- if (!text2.trim()) return;
1738
- const base = typeof id === "string" && id ? id : `line:${line}`;
1739
- const count = (ids.get(base) ?? 0) + 1;
1740
- ids.set(base, count);
1741
- messages.push({ id: `${base}#${count}`, role, text: text2, source: `line:${line}` });
1742
- };
1743
- for (const { line, value: row } of rows) {
1744
- if (agent === "claude-code") {
1745
- if (row.isSidechain === true) continue;
1798
+ const text = textContent(content);
1799
+ if (text.trim())
1800
+ this.candidates.push({ role, text, id, line, group: `${this.turn}:${role}`, visible });
1801
+ }
1802
+ consume(row, line) {
1803
+ if (this.agent === "claude-code") {
1804
+ if (row.isSidechain === true) return;
1746
1805
  if (row.sessionId) {
1747
- if (nativeId && nativeId !== row.sessionId)
1806
+ if (this.nativeId && this.nativeId !== row.sessionId)
1748
1807
  throw new Error("Mixed Claude session identities.");
1749
- nativeId = row.sessionId;
1808
+ this.nativeId = row.sessionId;
1750
1809
  }
1751
- if (row.cwd && !cwd) cwd = row.cwd;
1752
- if (row.type === "user" || row.type === "assistant") {
1810
+ if (row.cwd && !this.cwd) this.cwd = row.cwd;
1811
+ if ((row.type === "user" || row.type === "assistant") && row.isMeta !== true) {
1753
1812
  const message2 = object(row.message);
1754
- add(message2.role ?? row.type, message2.content, row.uuid, line);
1813
+ this.add(message2.role ?? row.type, message2.content, row.uuid, line);
1755
1814
  }
1756
- } else if (agent === "codex") {
1815
+ } else if (this.agent === "codex") {
1757
1816
  const payload = object(row.payload);
1758
1817
  if (row.type === "session_meta") {
1759
- if (nativeId && nativeId !== payload.id) throw new Error("Mixed Codex session identities.");
1760
- nativeId = payload.id;
1761
- cwd = payload.cwd;
1818
+ if (this.nativeId && this.nativeId !== payload.id)
1819
+ throw new Error("Mixed Codex session identities.");
1820
+ this.nativeId = payload.id;
1821
+ this.cwd = payload.cwd;
1762
1822
  }
1763
- if (row.type === "response_item" && payload.type === "message" && payload.channel !== "analysis")
1764
- add(payload.role, payload.content, payload.id, line);
1765
- } else if (agent === "pi") {
1823
+ if (typeof payload.turn_id === "string") this.turn = payload.turn_id;
1824
+ if (row.type === "event_msg" && payload.type === "item_completed") {
1825
+ const item = object(payload.item);
1826
+ if (item.type === "UserMessage" || item.type === "AgentMessage")
1827
+ this.add(
1828
+ item.type === "UserMessage" ? "user" : "assistant",
1829
+ item.content,
1830
+ item.id,
1831
+ line,
1832
+ true
1833
+ );
1834
+ }
1835
+ if (row.type === "response_item" && payload.type === "message" && payload.channel !== "analysis") {
1836
+ const kinds = object(payload.internal_chat_message_metadata_passthrough).content_item_kinds;
1837
+ if (payload.role === "user" && Array.isArray(kinds) && !kinds.some((k) => String(k).startsWith("user.")))
1838
+ return;
1839
+ this.add(payload.role, payload.content, payload.id, line);
1840
+ }
1841
+ } else if (this.agent === "cursor") {
1842
+ const message2 = object(row.message);
1843
+ if (row.isSidechain === true) return;
1844
+ this.add(row.role ?? message2.role, message2.content ?? row.content, row.id ?? row.uuid, line);
1845
+ } else if (this.agent === "pi") {
1766
1846
  if (row.type === "session") {
1767
1847
  if (row.version !== 3) throw new Error("Unsupported Pi session version (expected 3).");
1768
- if (nativeId) throw new Error("Multiple Pi session headers.");
1769
- nativeId = row.id;
1770
- cwd = row.cwd;
1848
+ if (this.nativeId) throw new Error("Multiple Pi session headers.");
1849
+ this.nativeId = row.id;
1850
+ this.cwd = row.cwd;
1771
1851
  }
1772
1852
  if (row.type === "message") {
1773
1853
  const message2 = object(row.message);
1774
- add(message2.role, message2.content, row.id, line);
1854
+ this.add(message2.role, message2.content, row.id, line);
1775
1855
  }
1776
1856
  } else {
1777
1857
  if (row.type === "session") {
1778
1858
  if (row.version !== 0) throw new Error("Unsupported DSH session version (expected 0).");
1779
- if (nativeId) throw new Error("Multiple DSH session headers.");
1859
+ if (this.nativeId) throw new Error("Multiple DSH session headers.");
1780
1860
  if (Number(row.delegationDepth ?? 0) > 0)
1781
1861
  throw new Error("DSH delegated sessions are not collected.");
1782
- nativeId = row.id;
1783
- cwd = row.cwd;
1862
+ this.nativeId = row.id;
1863
+ this.cwd = row.cwd;
1784
1864
  }
1785
1865
  if (row.type === "user/message" || row.type === "assistant/message") {
1786
1866
  const data = object(row.data);
1787
1867
  const message2 = row.type === "assistant/message" ? object(data.message) : data;
1788
- if (object(message2.source).kind === "plugin") continue;
1789
- add(
1868
+ if (object(message2.source).kind === "plugin") return;
1869
+ this.add(
1790
1870
  row.type === "user/message" ? "user" : "assistant",
1791
1871
  message2.content,
1792
1872
  `seq:${row.seq}`,
@@ -1795,35 +1875,205 @@ function normalizeJsonl(agent, text) {
1795
1875
  }
1796
1876
  }
1797
1877
  }
1798
- return {
1799
- snapshot: SessionSnapshotSchema.parse({
1878
+ finish() {
1879
+ const visibleGroups = new Set(this.candidates.filter((m) => m.visible).map((m) => m.group));
1880
+ const ids = /* @__PURE__ */ new Map();
1881
+ const messages = this.candidates.filter((m) => m.visible || !visibleGroups.has(m.group)).map((m) => {
1882
+ const base = typeof m.id === "string" && m.id ? m.id : `line:${m.line}`;
1883
+ const count = (ids.get(base) ?? 0) + 1;
1884
+ ids.set(base, count);
1885
+ return { id: `${base}#${count}`, role: m.role, text: m.text, source: `line:${m.line}` };
1886
+ });
1887
+ return SessionSnapshotSchema.parse({
1800
1888
  formatVersion: 1,
1801
1889
  adapterVersion: 1,
1802
- agent,
1803
- nativeId,
1804
- cwd,
1890
+ agent: this.agent,
1891
+ nativeId: this.nativeId,
1892
+ cwd: this.cwd,
1805
1893
  messages
1806
- }),
1807
- incomplete
1808
- };
1894
+ });
1895
+ }
1896
+ };
1897
+
1898
+ // apps/client/sessions/setup.ts
1899
+ var exec2 = promisify2(execFile2);
1900
+ var quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
1901
+ async function exists(path, mode = constants2.F_OK) {
1902
+ try {
1903
+ await access(path, mode);
1904
+ return true;
1905
+ } catch {
1906
+ return false;
1907
+ }
1908
+ }
1909
+ async function setupSessions(state, root, directory, options = {}) {
1910
+ const home = options.home ?? homedir(), env = options.env ?? process.env;
1911
+ const project = dirname4(root), sessions = new SessionState(state, root);
1912
+ const defaults = [
1913
+ {
1914
+ agent: "claude-code",
1915
+ executables: ["claude"],
1916
+ storage: env.CLAUDE_CONFIG_DIR ?? join5(home, ".claude"),
1917
+ directories: ["projects"],
1918
+ config: ".claude/settings.local.json",
1919
+ events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
1920
+ },
1921
+ {
1922
+ agent: "codex",
1923
+ executables: ["codex"],
1924
+ storage: env.CODEX_HOME ?? join5(home, ".codex"),
1925
+ directories: ["sessions"],
1926
+ config: ".codex/hooks.json",
1927
+ events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
1928
+ },
1929
+ {
1930
+ agent: "cursor",
1931
+ executables: ["agent", "cursor-agent"],
1932
+ storage: env.CURSOR_CONFIG_DIR ?? join5(home, ".cursor"),
1933
+ directories: ["chats", "projects"],
1934
+ config: ".cursor/hooks.json",
1935
+ events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
1936
+ },
1937
+ {
1938
+ agent: "pi",
1939
+ executables: ["pi"],
1940
+ storage: env.PI_CODING_AGENT_DIR ?? join5(home, ".pi", "agent"),
1941
+ directories: ["sessions"]
1942
+ }
1943
+ ];
1944
+ const issues = [], configured = [];
1945
+ let gitRoot = project;
1946
+ try {
1947
+ gitRoot = (await exec2("git", ["-C", project, "rev-parse", "--show-toplevel"])).stdout.trim();
1948
+ } catch {
1949
+ }
1950
+ for (const adapter of defaults) {
1951
+ const installed = options.agents ? options.agents.includes(adapter.agent) : await exists(adapter.storage) || (await Promise.all(
1952
+ (env.PATH ?? "").split(":").flatMap(
1953
+ (dir) => adapter.executables.map((name2) => exists(join5(dir, name2), constants2.X_OK))
1954
+ )
1955
+ )).some(Boolean);
1956
+ if (!installed) continue;
1957
+ for (const sub of adapter.directories)
1958
+ sessions.discover(adapter.agent, resolve3(adapter.storage, sub), project);
1959
+ if (adapter.agent === "cursor")
1960
+ sessions.discover("cursor", join5(home, ".cursor", "projects"), project);
1961
+ configured.push(adapter.agent);
1962
+ if (!adapter.config) continue;
1963
+ const configRoot = adapter.agent === "cursor" ? gitRoot : project;
1964
+ const path = join5(configRoot, adapter.config);
1965
+ try {
1966
+ await mkdir4(dirname4(path), { recursive: true });
1967
+ let original = null;
1968
+ try {
1969
+ original = await readStable(configRoot, adapter.config);
1970
+ } catch (error) {
1971
+ if (!missing(error)) throw error;
1972
+ }
1973
+ const config = original ? JSON.parse(original.bytes.toString("utf8")) : {};
1974
+ if (!config || typeof config !== "object" || Array.isArray(config))
1975
+ throw new Error("Hook configuration must be a JSON object.");
1976
+ if (config.hooks !== void 0 && (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)))
1977
+ throw new Error("Invalid existing hooks object.");
1978
+ const hooks = object(config.hooks);
1979
+ const marker = `teammy-session-${rootKey(root)}`;
1980
+ const command = [
1981
+ process.execPath,
1982
+ options.entry ?? resolve3(process.argv[1]),
1983
+ "session",
1984
+ "hook",
1985
+ "--agent",
1986
+ adapter.agent,
1987
+ "--root",
1988
+ root,
1989
+ "--state-dir",
1990
+ directory,
1991
+ "--project-dir",
1992
+ project
1993
+ ].map(quote).join(" ") + ` # ${marker}`;
1994
+ for (const event of adapter.events) {
1995
+ const entries = hooks[event] ?? [];
1996
+ if (!Array.isArray(entries)) throw new Error(`Invalid ${event} hooks.`);
1997
+ const retained = entries.flatMap((entry) => {
1998
+ const value = object(entry);
1999
+ if (adapter.agent === "cursor")
2000
+ return typeof value.command === "string" && value.command.includes(marker) ? [] : [entry];
2001
+ if (!Array.isArray(value.hooks)) return [entry];
2002
+ const nested = value.hooks.filter(
2003
+ (hook) => !(typeof object(hook).command === "string" && String(object(hook).command).includes(marker))
2004
+ );
2005
+ return nested.length ? [{ ...value, hooks: nested }] : [];
2006
+ });
2007
+ hooks[event] = [
2008
+ ...retained,
2009
+ adapter.agent === "cursor" ? { command, timeout: 3 } : { hooks: [{ type: "command", command, timeout: 3 }] }
2010
+ ];
2011
+ }
2012
+ config.hooks = hooks;
2013
+ if (adapter.agent === "cursor") config.version ??= 1;
2014
+ const bytes = Buffer.from(JSON.stringify(config, null, 2) + "\n");
2015
+ if (original?.bytes.equals(bytes)) continue;
2016
+ if (original) {
2017
+ const backupDir = join5(directory, "hook-backups", rootKey(root));
2018
+ await mkdir4(backupDir, { recursive: true, mode: 448 });
2019
+ const hash2 = createHash5("sha256").update(original.bytes).digest("hex");
2020
+ await writeFile(join5(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
2021
+ mode: 384
2022
+ });
2023
+ }
2024
+ await replaceFile(configRoot, adapter.config, bytes, original?.signature ?? null);
2025
+ try {
2026
+ const relativePath = path.slice(gitRoot.length + 1);
2027
+ const tracked = (await exec2("git", ["-C", gitRoot, "ls-files", "--", relativePath])).stdout;
2028
+ if (!tracked) {
2029
+ const exclude = (await exec2("git", [
2030
+ "-C",
2031
+ gitRoot,
2032
+ "rev-parse",
2033
+ "--path-format=absolute",
2034
+ "--git-path",
2035
+ "info/exclude"
2036
+ ])).stdout.trim();
2037
+ const old = await readFile(exclude, "utf8").catch((e) => {
2038
+ if (missing(e)) return "";
2039
+ throw e;
2040
+ });
2041
+ const rule = "/" + relativePath.replace(/[\\*?[\]#! ]/g, "\\$&");
2042
+ if (!old.split(/\r?\n/).includes(rule)) {
2043
+ await mkdir4(dirname4(exclude), { recursive: true });
2044
+ await writeFile(exclude, old + (old && !old.endsWith("\n") ? "\n" : "") + rule + "\n");
2045
+ }
2046
+ }
2047
+ } catch {
2048
+ }
2049
+ } catch (error) {
2050
+ issues.push(`${path}: ${error instanceof Error ? error.message : "Hook setup failed."}`);
2051
+ }
2052
+ }
2053
+ return { configured, issues };
1809
2054
  }
1810
2055
 
1811
2056
  // apps/client/sessions/hooks.ts
2057
+ import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
2058
+ import { mkdir as mkdir5, open as open2, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
2059
+ import { isAbsolute as isAbsolute3, join as join6 } from "node:path";
2060
+ import { z as z10 } from "zod";
1812
2061
  var HintSchema = z10.object({
1813
2062
  agent: AgentSchema,
1814
- path: z10.string().min(1).max(4096).refine(isAbsolute3),
2063
+ path: z10.string().min(1).max(4096).refine(isAbsolute3).optional(),
2064
+ nativeId: z10.string().min(1).max(512).optional(),
1815
2065
  projectDir: z10.string().min(1).max(4096).refine(isAbsolute3)
1816
2066
  });
1817
2067
  function inbox(directory, root) {
1818
- return join5(directory, "session-hints", createHash5("sha256").update(root).digest("hex"));
2068
+ return join6(directory, "session-hints", createHash6("sha256").update(root).digest("hex"));
1819
2069
  }
1820
2070
  async function recordHint(directory, root, input) {
1821
2071
  const hint = HintSchema.parse(input);
1822
2072
  const targetDir = inbox(directory, root);
1823
- await mkdir4(targetDir, { recursive: true, mode: 448 });
1824
- const target = join5(
2073
+ await mkdir5(targetDir, { recursive: true, mode: 448 });
2074
+ const target = join6(
1825
2075
  targetDir,
1826
- `${createHash5("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
2076
+ `${createHash6("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
1827
2077
  );
1828
2078
  const temporary = `${target}.${randomUUID4()}.tmp`;
1829
2079
  const handle = await open2(temporary, "wx", 384);
@@ -1834,7 +2084,7 @@ async function recordHint(directory, root, input) {
1834
2084
  }
1835
2085
  await rename2(temporary, target);
1836
2086
  }
1837
- async function consumeHints(directory, root, sessions) {
2087
+ async function consumeHints(directory, root, sessions, refresh) {
1838
2088
  const issues = [];
1839
2089
  let dir;
1840
2090
  try {
@@ -1847,7 +2097,7 @@ async function consumeHints(directory, root, sessions) {
1847
2097
  for await (const entry of dir) {
1848
2098
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
1849
2099
  if (count++ >= 50) break;
1850
- const path = join5(inbox(directory, root), entry.name);
2100
+ const path = join6(inbox(directory, root), entry.name);
1851
2101
  try {
1852
2102
  const handle = await open2(path, "r");
1853
2103
  let body;
@@ -1858,7 +2108,9 @@ async function consumeHints(directory, root, sessions) {
1858
2108
  await handle.close();
1859
2109
  }
1860
2110
  const hint = HintSchema.parse(JSON.parse(body));
1861
- sessions.track(hint.agent, hint.path, hint.projectDir);
2111
+ sessions.receivedHook(hint.agent);
2112
+ if (hint.path) sessions.track(hint.agent, hint.path, hint.projectDir);
2113
+ if (hint.nativeId) await refresh?.();
1862
2114
  await unlink2(path);
1863
2115
  } catch (error) {
1864
2116
  issues.push(
@@ -1876,7 +2128,7 @@ async function hookPayload() {
1876
2128
  for await (const part of process.stdin) {
1877
2129
  const bytes = Buffer.from(part);
1878
2130
  size += bytes.length;
1879
- if (size > 16384) throw new Error("Hook payload too large.");
2131
+ if (size > 4 * 1024 * 1024) throw new Error("Hook payload too large.");
1880
2132
  parts.push(bytes);
1881
2133
  }
1882
2134
  return object(JSON.parse(Buffer.concat(parts).toString("utf8")));
@@ -1888,6 +2140,7 @@ async function hookPayload() {
1888
2140
  // apps/client/sessions/command.ts
1889
2141
  var help = `Usage: teammy session COMMAND [options]
1890
2142
 
2143
+ setup Configure project hooks and native discovery (also done by connect)
1891
2144
  track --agent AGENT --file PATH [--project-dir DIR] [--adopt-existing]
1892
2145
  untrack --file PATH (does not delete server history)
1893
2146
  discover --agent AGENT [--directory DIR] [--project-dir DIR]
@@ -1913,8 +2166,8 @@ async function runSessionCommand(args) {
1913
2166
  const { values } = parseArgs({
1914
2167
  args: args.slice(1),
1915
2168
  options: {
1916
- root: { type: "string", default: resolve3("workspaces") },
1917
- "state-dir": { type: "string", default: join6(homedir(), ".teammy", "client") },
2169
+ root: { type: "string", default: resolve4("workspaces") },
2170
+ "state-dir": { type: "string", default: join7(homedir2(), ".teammy", "client") },
1918
2171
  agent: { type: "string" },
1919
2172
  file: { type: "string" },
1920
2173
  directory: { type: "string" },
@@ -1933,16 +2186,18 @@ async function runSessionCommand(args) {
1933
2186
  process.stdout.write(help);
1934
2187
  return 0;
1935
2188
  }
1936
- const root = await realpath4(resolve3(values.root));
2189
+ const root = await realpath4(resolve4(values.root));
1937
2190
  const directory = await stateLocation(root, values["state-dir"]);
1938
- const projectDir = await realpath4(resolve3(values["project-dir"] ?? dirname4(root)));
2191
+ const projectDir = await realpath4(resolve4(values["project-dir"] ?? dirname5(root)));
1939
2192
  if (command === "hook") {
1940
2193
  const payload = values.file ? {} : await hookPayload();
1941
2194
  const file = values.file ?? payload.transcript_path ?? payload.session_file ?? payload.sessionFile;
1942
- if (typeof file === "string" && file)
2195
+ const nativeId = payload.session_id ?? payload.conversation_id;
2196
+ if (typeof file === "string" && file || typeof nativeId === "string")
1943
2197
  await recordHint(directory, root, {
1944
2198
  agent: AgentSchema.parse(values.agent),
1945
- path: resolve3(file),
2199
+ ...typeof file === "string" && file ? { path: resolve4(file) } : {},
2200
+ ...typeof nativeId === "string" ? { nativeId } : {},
1946
2201
  projectDir
1947
2202
  });
1948
2203
  return 0;
@@ -1954,9 +2209,10 @@ async function runSessionCommand(args) {
1954
2209
  const sessions = new SessionState(state, root);
1955
2210
  const client = new HubClient(binding.serverUrl, 1e4, binding);
1956
2211
  let output;
1957
- if (command === "track" || command === "untrack") {
2212
+ if (command === "setup") output = await setupSessions(state, root, directory);
2213
+ else if (command === "track" || command === "untrack") {
1958
2214
  if (!values.file) throw new Error("--file is required.");
1959
- const path = resolve3(values.file);
2215
+ const path = resolve4(values.file);
1960
2216
  if (command === "track")
1961
2217
  sessions.track(
1962
2218
  AgentSchema.parse(values.agent),
@@ -1969,27 +2225,34 @@ async function runSessionCommand(args) {
1969
2225
  } else if (command === "discover" || command === "undiscover") {
1970
2226
  if (command === "undiscover") {
1971
2227
  if (!values.directory) throw new Error("--directory is required.");
1972
- state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, await realpath4(resolve3(values.directory)));
2228
+ state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, await realpath4(resolve4(values.directory)));
1973
2229
  } else {
1974
2230
  const agent = AgentSchema.parse(values.agent);
1975
2231
  const defaults = {
1976
- "claude-code": join6(homedir(), ".claude", "projects"),
1977
- codex: join6(process.env.CODEX_HOME ?? join6(homedir(), ".codex"), "sessions"),
1978
- cursor: join6(
1979
- process.env.CURSOR_CONFIG_DIR ?? join6(homedir(), ".cursor"),
2232
+ "claude-code": join7(
2233
+ process.env.CLAUDE_CONFIG_DIR ?? join7(homedir2(), ".claude"),
2234
+ "projects"
2235
+ ),
2236
+ codex: join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "sessions"),
2237
+ cursor: join7(
2238
+ process.env.CURSOR_CONFIG_DIR ?? join7(homedir2(), ".cursor"),
1980
2239
  "chats",
1981
- createHash6("md5").update(projectDir).digest("hex")
2240
+ createHash7("md5").update(projectDir).digest("hex")
1982
2241
  ),
1983
- pi: join6(process.env.PI_CODING_AGENT_DIR ?? join6(homedir(), ".pi", "agent"), "sessions"),
2242
+ pi: join7(process.env.PI_CODING_AGENT_DIR ?? join7(homedir2(), ".pi", "agent"), "sessions"),
1984
2243
  dsh: void 0
1985
2244
  };
1986
2245
  const path = values.directory ?? defaults[agent];
1987
2246
  if (!path) throw new Error("DSH requires its configured persistence --directory.");
1988
- sessions.discover(agent, await realpath4(resolve3(path)), projectDir);
2247
+ sessions.discover(agent, await realpath4(resolve4(path)), projectDir);
1989
2248
  }
1990
2249
  output = sessions.directories();
1991
2250
  } else if (command === "sources")
1992
- output = { sources: sessions.status(), discovery: sessions.directories() };
2251
+ output = {
2252
+ sources: sessions.status(),
2253
+ discovery: sessions.directories(),
2254
+ hooks: sessions.hooks()
2255
+ };
1993
2256
  else {
1994
2257
  const after = Number(values.after);
1995
2258
  const limit = Number(values.limit);
@@ -2025,22 +2288,22 @@ import { setTimeout as setTimeout4 } from "node:timers/promises";
2025
2288
 
2026
2289
  // apps/client/client-command.ts
2027
2290
  import { parseArgs as parseArgs2 } from "node:util";
2028
- import { join as join11, resolve as resolve5 } from "node:path";
2029
- import { homedir as homedir3 } from "node:os";
2291
+ import { join as join12, resolve as resolve6 } from "node:path";
2292
+ import { homedir as homedir4 } from "node:os";
2030
2293
  import { createInterface as createInterface2 } from "node:readline/promises";
2031
2294
 
2032
2295
  // apps/client/connection.ts
2033
- import { readFile, mkdir as mkdir5 } from "node:fs/promises";
2296
+ import { readFile as readFile2, mkdir as mkdir6 } from "node:fs/promises";
2034
2297
  import { createInterface } from "node:readline/promises";
2035
2298
 
2036
2299
  // apps/client/root-lock.ts
2037
- import { createHash as createHash7 } from "node:crypto";
2038
- import { join as join7 } from "node:path";
2300
+ import { createHash as createHash8 } from "node:crypto";
2301
+ import { join as join8 } from "node:path";
2039
2302
  function acquireRootLock(root, stateDirectory) {
2040
2303
  let lock;
2041
2304
  try {
2042
2305
  lock = openDatabase(
2043
- join7(stateDirectory, "locks", `${createHash7("sha256").update(root).digest("hex")}.sqlite3`),
2306
+ join8(stateDirectory, "locks", `${createHash8("sha256").update(root).digest("hex")}.sqlite3`),
2044
2307
  [],
2045
2308
  0
2046
2309
  );
@@ -2055,9 +2318,9 @@ function acquireRootLock(root, stateDirectory) {
2055
2318
  }
2056
2319
 
2057
2320
  // apps/client/local-config.ts
2058
- import { constants as constants2 } from "node:fs";
2059
- import { open as open3, writeFile } from "node:fs/promises";
2060
- import { join as join8 } from "node:path";
2321
+ import { constants as constants3 } from "node:fs";
2322
+ import { open as open3, writeFile as writeFile2 } from "node:fs/promises";
2323
+ import { join as join9 } from "node:path";
2061
2324
  import { z as z11 } from "zod";
2062
2325
  import ignore from "ignore";
2063
2326
  var LOCAL_CONFIG_NAME = ".teammy-local.json";
@@ -2066,8 +2329,8 @@ var LocalConfigSchema = z11.object({
2066
2329
  }).strict();
2067
2330
  async function ensureLocalConfig(root) {
2068
2331
  try {
2069
- await writeFile(
2070
- join8(root, LOCAL_CONFIG_NAME),
2332
+ await writeFile2(
2333
+ join9(root, LOCAL_CONFIG_NAME),
2071
2334
  JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
2072
2335
  { flag: "wx", mode: 384 }
2073
2336
  );
@@ -2079,8 +2342,8 @@ async function loadLocalRules(root) {
2079
2342
  let rules = [];
2080
2343
  try {
2081
2344
  const file = await open3(
2082
- join8(root, LOCAL_CONFIG_NAME),
2083
- constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
2345
+ join9(root, LOCAL_CONFIG_NAME),
2346
+ constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
2084
2347
  );
2085
2348
  try {
2086
2349
  const before = await file.stat();
@@ -2136,7 +2399,7 @@ async function connectProject(state, root, directory, options) {
2136
2399
  const client2 = new HubClient(existing.serverUrl, 1e4, existing);
2137
2400
  await client2.members();
2138
2401
  } else if (options.tokenFile) {
2139
- const token = CredentialSchema.parse((await readFile(options.tokenFile, "utf8")).trim());
2402
+ const token = CredentialSchema.parse((await readFile2(options.tokenFile, "utf8")).trim());
2140
2403
  const deviceId = state.deviceFor(root);
2141
2404
  const client2 = new HubClient(options.url, 1e4, { token, deviceId });
2142
2405
  const binding2 = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
@@ -2185,7 +2448,7 @@ async function connectProject(state, root, directory, options) {
2185
2448
  }
2186
2449
  const own = await metadata(root, binding.memberId);
2187
2450
  if (own && !own.directory) throw new Error("Member path is not a directory.");
2188
- if (!own) await mkdir5(await safePath(root, binding.memberId));
2451
+ if (!own) await mkdir6(await safePath(root, binding.memberId));
2189
2452
  await ensureLocalConfig(root);
2190
2453
  const { token: _, ...visible } = binding;
2191
2454
  return { ...visible, configRevision: project.cached()?.config.configRevision ?? null };
@@ -2196,23 +2459,23 @@ async function connectProject(state, root, directory, options) {
2196
2459
 
2197
2460
  // apps/client/runtime/ipc.ts
2198
2461
  import { createConnection } from "node:net";
2199
- import { mkdir as mkdir6, lstat as lstat3, chmod } from "node:fs/promises";
2200
- import { join as join9 } from "node:path";
2462
+ import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
2463
+ import { join as join10 } from "node:path";
2201
2464
  import { z as z12 } from "zod";
2202
2465
  var RequestSchema = z12.object({
2203
2466
  method: z12.enum(["ping", "wake", "status", "once"]),
2204
2467
  root: z12.string().max(4096).optional()
2205
2468
  }).strict();
2206
2469
  function socketPath(directory) {
2207
- return join9(
2470
+ return join10(
2208
2471
  "/tmp",
2209
2472
  `teammy-${process.getuid?.() ?? "user"}-${rootKey(directory)}`,
2210
2473
  "control.sock"
2211
2474
  );
2212
2475
  }
2213
2476
  async function prepareSocketDirectory(directory) {
2214
- const path = join9(socketPath(directory), "..");
2215
- await mkdir6(path, { recursive: true, mode: 448 });
2477
+ const path = join10(socketPath(directory), "..");
2478
+ await mkdir7(path, { recursive: true, mode: 448 });
2216
2479
  const stat2 = await lstat3(path);
2217
2480
  if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.())
2218
2481
  throw new Error("Unsafe daemon socket directory.");
@@ -2220,7 +2483,7 @@ async function prepareSocketDirectory(directory) {
2220
2483
  }
2221
2484
  async function daemonRequest(directory, input, timeout = 15e3) {
2222
2485
  RequestSchema.parse(input);
2223
- return new Promise((resolve9, reject) => {
2486
+ return new Promise((resolve10, reject) => {
2224
2487
  const socket = createConnection(socketPath(directory));
2225
2488
  socket.setEncoding("utf8");
2226
2489
  let body = "";
@@ -2230,7 +2493,7 @@ async function daemonRequest(directory, input, timeout = 15e3) {
2230
2493
  done = true;
2231
2494
  socket.destroy();
2232
2495
  if (error) reject(error);
2233
- else resolve9(result2);
2496
+ else resolve10(result2);
2234
2497
  };
2235
2498
  socket.setTimeout(
2236
2499
  timeout,
@@ -2271,20 +2534,20 @@ async function runningDaemon(directory) {
2271
2534
  }
2272
2535
 
2273
2536
  // apps/client/runtime/service.ts
2274
- import { execFile as execFile2 } from "node:child_process";
2275
- import { promisify as promisify2 } from "node:util";
2276
- import { homedir as homedir2 } from "node:os";
2277
- import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
2278
- import { mkdir as mkdir7, readFile as readFile2, writeFile as writeFile2, rename as rename3 } from "node:fs/promises";
2537
+ import { execFile as execFile3 } from "node:child_process";
2538
+ import { promisify as promisify3 } from "node:util";
2539
+ import { homedir as homedir3 } from "node:os";
2540
+ import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
2541
+ import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "node:fs/promises";
2279
2542
  import { setTimeout as setTimeout3 } from "node:timers/promises";
2280
- var execute = promisify2(execFile2);
2281
- var exec2 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
2543
+ var execute = promisify3(execFile3);
2544
+ var exec3 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
2282
2545
  var xml = (s) => s.replace(
2283
2546
  /[<>&"']/g,
2284
2547
  (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&apos;" })[c]
2285
2548
  );
2286
2549
  var unit = (s) => '"' + s.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, () => "$$") + '"';
2287
- function serviceDefinition(directory, executable, entry, platform = process.platform, userHome = homedir2()) {
2550
+ function serviceDefinition(directory, executable, entry, platform = process.platform, userHome = homedir3()) {
2288
2551
  for (const value of [directory, executable, entry])
2289
2552
  if (/[\r\n\0]/.test(value)) throw new Error("Invalid service path.");
2290
2553
  const id = `com.teammy.client.${rootKey(directory)}`;
@@ -2292,7 +2555,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
2292
2555
  if (platform === "darwin")
2293
2556
  return {
2294
2557
  id,
2295
- path: join10(userHome, "Library", "LaunchAgents", id + ".plist"),
2558
+ path: join11(userHome, "Library", "LaunchAgents", id + ".plist"),
2296
2559
  content: `<?xml version="1.0" encoding="UTF-8"?>
2297
2560
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2298
2561
  <plist version="1.0"><dict><key>Label</key><string>${id}</string><key>ProgramArguments</key><array>${args.map((a) => `<string>${xml(a)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer><key>WorkingDirectory</key><string>${xml(userHome)}</string></dict></plist>
@@ -2301,7 +2564,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
2301
2564
  if (platform === "linux")
2302
2565
  return {
2303
2566
  id: id + ".service",
2304
- path: join10(userHome, ".config", "systemd", "user", id + ".service"),
2567
+ path: join11(userHome, ".config", "systemd", "user", id + ".service"),
2305
2568
  content: `[Unit]
2306
2569
  Description=Teammy background synchronization
2307
2570
  [Service]
@@ -2323,10 +2586,10 @@ WantedBy=default.target
2323
2586
  async function checkServiceSupport() {
2324
2587
  try {
2325
2588
  if (process.platform === "darwin")
2326
- await exec2("launchctl", ["print", `gui/${process.getuid()}`], {
2589
+ await exec3("launchctl", ["print", `gui/${process.getuid()}`], {
2327
2590
  maxBuffer: 4 * 1024 * 1024
2328
2591
  });
2329
- else if (process.platform === "linux") await exec2("systemctl", ["--user", "show-environment"]);
2592
+ else if (process.platform === "linux") await exec3("systemctl", ["--user", "show-environment"]);
2330
2593
  else throw new Error("unsupported");
2331
2594
  } catch {
2332
2595
  throw new Error(
@@ -2338,7 +2601,7 @@ async function bootstrapLaunchAgent(path) {
2338
2601
  const deadline = Date.now() + 1e4;
2339
2602
  for (; ; ) {
2340
2603
  try {
2341
- await exec2("launchctl", ["bootstrap", `gui/${process.getuid()}`, path]);
2604
+ await exec3("launchctl", ["bootstrap", `gui/${process.getuid()}`, path]);
2342
2605
  return;
2343
2606
  } catch (error) {
2344
2607
  if (error.code !== 5 || Date.now() >= deadline) throw error;
@@ -2347,40 +2610,40 @@ async function bootstrapLaunchAgent(path) {
2347
2610
  }
2348
2611
  }
2349
2612
  async function ensureDaemon(directory, version) {
2350
- const definition = serviceDefinition(directory, process.execPath, resolve4(process.argv[1]));
2613
+ const definition = serviceDefinition(directory, process.execPath, resolve5(process.argv[1]));
2351
2614
  let old = "";
2352
2615
  try {
2353
- old = await readFile2(definition.path, "utf8");
2616
+ old = await readFile3(definition.path, "utf8");
2354
2617
  } catch (e) {
2355
2618
  if (e.code !== "ENOENT") throw e;
2356
2619
  }
2357
2620
  const alive = await runningDaemon(directory);
2358
2621
  if (alive && old === definition.content && alive.version === version) return;
2359
2622
  await checkServiceSupport();
2360
- await mkdir7(dirname5(definition.path), { recursive: true });
2623
+ await mkdir8(dirname6(definition.path), { recursive: true });
2361
2624
  if (old !== definition.content) {
2362
2625
  const temp = definition.path + `.${process.pid}.tmp`;
2363
- await writeFile2(temp, definition.content, { mode: 384 });
2626
+ await writeFile3(temp, definition.content, { mode: 384 });
2364
2627
  await rename3(temp, definition.path);
2365
2628
  }
2366
2629
  if (process.platform === "darwin") {
2367
2630
  const domain = `gui/${process.getuid()}`, target = `${domain}/${definition.id}`;
2368
2631
  let loaded = false;
2369
2632
  try {
2370
- await exec2("launchctl", ["print", target]);
2633
+ await exec3("launchctl", ["print", target]);
2371
2634
  loaded = true;
2372
2635
  } catch {
2373
2636
  }
2374
2637
  if (loaded && (old !== definition.content || alive?.version !== version)) {
2375
- await exec2("launchctl", ["bootout", target]);
2638
+ await exec3("launchctl", ["bootout", target]);
2376
2639
  loaded = false;
2377
2640
  }
2378
2641
  if (!loaded) await bootstrapLaunchAgent(definition.path);
2379
- await exec2("launchctl", ["kickstart", target]);
2642
+ await exec3("launchctl", ["kickstart", target]);
2380
2643
  } else {
2381
- await exec2("systemctl", ["--user", "daemon-reload"]);
2382
- await exec2("systemctl", ["--user", "enable", definition.id]);
2383
- await exec2("systemctl", [
2644
+ await exec3("systemctl", ["--user", "daemon-reload"]);
2645
+ await exec3("systemctl", ["--user", "enable", definition.id]);
2646
+ await exec3("systemctl", [
2384
2647
  "--user",
2385
2648
  old !== definition.content || alive?.version !== version ? "restart" : "start",
2386
2649
  definition.id
@@ -2419,8 +2682,8 @@ function json(value) {
2419
2682
 
2420
2683
  // apps/client/client-command.ts
2421
2684
  var locationOptions = {
2422
- root: { type: "string", default: resolve5("workspaces") },
2423
- "state-dir": { type: "string", default: join11(homedir3(), ".teammy", "client") },
2685
+ root: { type: "string", default: resolve6("workspaces") },
2686
+ "state-dir": { type: "string", default: join12(homedir4(), ".teammy", "client") },
2424
2687
  json: { type: "boolean" },
2425
2688
  help: { type: "boolean", short: "h" }
2426
2689
  };
@@ -2523,12 +2786,16 @@ async function runClientCommand(command, args, version) {
2523
2786
  tokenFile: values["token-file"],
2524
2787
  reconnect: values.reconnect
2525
2788
  });
2789
+ const collection = await setupSessions(state, root, directory);
2790
+ for (const issue of collection.issues) log.write("sessions.setup.failed", root, issue);
2526
2791
  completePreparation(state, root);
2527
2792
  runtime.enable(root, !values["no-start"]);
2528
2793
  log.write("workspace.connected", root);
2529
2794
  if (!values["no-start"]) await wakeDaemon(directory, version);
2530
- if (values.json) json({ ...connected, syncEnabled: !values["no-start"] });
2795
+ if (values.json) json({ ...connected, collection, syncEnabled: !values["no-start"] });
2531
2796
  else result(values["no-start"] ? "\u9879\u76EE\u5DF2\u8FDE\u63A5" : "\u5DF2\u8FDE\u63A5\uFF0C\u540E\u53F0\u540C\u6B65\u5DF2\u542F\u52A8", root);
2797
+ if (collection.issues.length && !values.json)
2798
+ result("\u90E8\u5206\u4F1A\u8BDD Hook \u672A\u914D\u7F6E", "\u540E\u53F0\u53D1\u73B0\u4ECD\u53EF\u7528\uFF0C\u8BE6\u60C5\u89C1 teammy logs");
2532
2799
  return 0;
2533
2800
  } catch (e) {
2534
2801
  log.write(`${command}.failed`, root, diagnostic(e));
@@ -2541,16 +2808,16 @@ async function runClientCommand(command, args, version) {
2541
2808
  // apps/client/files/synchronizer.ts
2542
2809
  import { randomUUID as randomUUID6 } from "node:crypto";
2543
2810
  import { watch } from "node:fs";
2544
- import { lstat as lstat4, mkdir as mkdir9 } from "node:fs/promises";
2545
- import { join as join13, sep as sep4 } from "node:path";
2811
+ import { lstat as lstat4, mkdir as mkdir10 } from "node:fs/promises";
2812
+ import { join as join14, sep as sep4 } from "node:path";
2546
2813
  import { performance } from "node:perf_hooks";
2547
2814
 
2548
2815
  // packages/storage/local-blob-store.ts
2549
- import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
2550
- import { link, mkdir as mkdir8, open as open4, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
2551
- import { dirname as dirname6, join as join12, resolve as resolve6 } from "node:path";
2816
+ import { createHash as createHash9, randomUUID as randomUUID5 } from "node:crypto";
2817
+ import { link, mkdir as mkdir9, open as open4, readFile as readFile4, unlink as unlink3 } from "node:fs/promises";
2818
+ import { dirname as dirname7, join as join13, resolve as resolve7 } from "node:path";
2552
2819
  function hash(content) {
2553
- return createHash8("sha256").update(content).digest("hex");
2820
+ return createHash9("sha256").update(content).digest("hex");
2554
2821
  }
2555
2822
  function hasCode(error, code) {
2556
2823
  return error instanceof Error && "code" in error && error.code === code;
@@ -2566,13 +2833,13 @@ var BlobIntegrityError = class extends Error {
2566
2833
  var LocalBlobStore = class {
2567
2834
  root;
2568
2835
  constructor(root) {
2569
- this.root = resolve6(root);
2836
+ this.root = resolve7(root);
2570
2837
  }
2571
2838
  async put(content) {
2572
2839
  const bytes = Buffer.from(content);
2573
2840
  const sha256 = hash(bytes);
2574
2841
  const target = this.pathFor(sha256);
2575
- await mkdir8(dirname6(target), { recursive: true, mode: 448 });
2842
+ await mkdir9(dirname7(target), { recursive: true, mode: 448 });
2576
2843
  const temporary = `${target}.${randomUUID5()}.tmp`;
2577
2844
  const file = await open4(temporary, "wx", 384);
2578
2845
  try {
@@ -2589,7 +2856,7 @@ var LocalBlobStore = class {
2589
2856
  await this.get(sha256);
2590
2857
  }
2591
2858
  if (process.platform !== "win32") {
2592
- let directory = dirname6(target);
2859
+ let directory = dirname7(target);
2593
2860
  for (; ; ) {
2594
2861
  const handle = await open4(directory, "r");
2595
2862
  try {
@@ -2597,8 +2864,8 @@ var LocalBlobStore = class {
2597
2864
  } finally {
2598
2865
  await handle.close();
2599
2866
  }
2600
- if (directory === dirname6(this.root)) break;
2601
- directory = dirname6(directory);
2867
+ if (directory === dirname7(this.root)) break;
2868
+ directory = dirname7(directory);
2602
2869
  }
2603
2870
  }
2604
2871
  } finally {
@@ -2610,7 +2877,7 @@ var LocalBlobStore = class {
2610
2877
  const target = this.pathFor(sha256);
2611
2878
  let content;
2612
2879
  try {
2613
- content = await readFile3(target);
2880
+ content = await readFile4(target);
2614
2881
  } catch (error) {
2615
2882
  if (hasCode(error, "ENOENT")) return void 0;
2616
2883
  throw error;
@@ -2620,7 +2887,7 @@ var LocalBlobStore = class {
2620
2887
  }
2621
2888
  pathFor(sha256) {
2622
2889
  if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
2623
- return join12(this.root, "sha256", sha256.slice(0, 2), sha256);
2890
+ return join13(this.root, "sha256", sha256.slice(0, 2), sha256);
2624
2891
  }
2625
2892
  };
2626
2893
 
@@ -2680,7 +2947,7 @@ var FileSynchronizer = class _FileSynchronizer {
2680
2947
  if (!binding) throw new Error("Connect this root before synchronizing.");
2681
2948
  this.binding = binding;
2682
2949
  this.client = new HubClient(binding.serverUrl, 3e4, binding, this.abort.signal);
2683
- this.blobs = new LocalBlobStore(join13(stateDirectory, "blobs"));
2950
+ this.blobs = new LocalBlobStore(join14(stateDirectory, "blobs"));
2684
2951
  this.projectScope = options.scope === "project";
2685
2952
  this.owner = this.projectScope ? PROJECT_FILE_OWNER : binding.memberId;
2686
2953
  this.publication = new FilePublicationState(state.db, options.scope);
@@ -2690,7 +2957,7 @@ var FileSynchronizer = class _FileSynchronizer {
2690
2957
  commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch, this.sharedConfig.revision) : this.client.commitFiles(batch)
2691
2958
  });
2692
2959
  this.index = new SyncState(state, root, options.scope);
2693
- this.ownRoot = this.projectScope ? root : join13(root, binding.memberId);
2960
+ this.ownRoot = this.projectScope ? root : join14(root, binding.memberId);
2694
2961
  this.common = this.projectScope ? void 0 : new _FileSynchronizer(state, root, stateDirectory, { ...options, scope: "project" });
2695
2962
  const started = performance.now();
2696
2963
  const epoch = Date.now();
@@ -2889,7 +3156,7 @@ var FileSynchronizer = class _FileSynchronizer {
2889
3156
  }
2890
3157
  }
2891
3158
  const ownerPath = this.projectScope ? this.root : await safePath(this.root, this.owner);
2892
- await mkdir9(ownerPath).catch((error) => {
3159
+ await mkdir10(ownerPath).catch((error) => {
2893
3160
  if (error.code !== "EEXIST") throw error;
2894
3161
  });
2895
3162
  this.publication.initializePublication(this.root, manifest.revision, manifest.files);
@@ -3075,7 +3342,7 @@ var FileSynchronizer = class _FileSynchronizer {
3075
3342
  }
3076
3343
  const job = this.index.job(owner);
3077
3344
  const ownerRoot = this.projectScope ? this.root : await safePath(this.root, owner);
3078
- await mkdir9(ownerRoot).catch((error) => {
3345
+ await mkdir10(ownerRoot).catch((error) => {
3079
3346
  if (error.code !== "EEXIST") throw error;
3080
3347
  });
3081
3348
  const ownerStat = await lstat4(ownerRoot);
@@ -3131,22 +3398,24 @@ function message(error) {
3131
3398
 
3132
3399
  // apps/client/sessions/synchronizer.ts
3133
3400
  import { randomUUID as randomUUID7 } from "node:crypto";
3134
- import { join as join15 } from "node:path";
3401
+ import { join as join17 } from "node:path";
3135
3402
 
3136
3403
  // packages/protocol/session-key.ts
3137
- import { createHash as createHash9 } from "node:crypto";
3404
+ import { createHash as createHash10 } from "node:crypto";
3138
3405
  function sessionKey(agent, nativeId) {
3139
- return createHash9("sha256").update(JSON.stringify([agent, nativeId])).digest("hex");
3406
+ return createHash10("sha256").update(JSON.stringify([agent, nativeId])).digest("hex");
3140
3407
  }
3141
3408
 
3142
3409
  // apps/client/sessions/source.ts
3143
- import { open as open5, stat, realpath as realpath5 } from "node:fs/promises";
3144
- import { relative as relative4, isAbsolute as isAbsolute4, sep as sep5 } from "node:path";
3410
+ import { createReadStream, realpathSync } from "node:fs";
3411
+ import { stat, realpath as realpath5, readdir as readdir2 } from "node:fs/promises";
3412
+ import { basename as basename3, dirname as dirname9, relative as relative4, isAbsolute as isAbsolute4, sep as sep5, join as join15 } from "node:path";
3413
+ import { createInterface as createInterface3 } from "node:readline";
3145
3414
  import * as zlib from "node:zlib";
3146
3415
 
3147
3416
  // apps/client/sessions/cursor.ts
3148
- import { createHash as createHash10 } from "node:crypto";
3149
- import { basename as basename2, dirname as dirname7, resolve as resolve7 } from "node:path";
3417
+ import { createHash as createHash11 } from "node:crypto";
3418
+ import { basename as basename2, dirname as dirname8, resolve as resolve8 } from "node:path";
3150
3419
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
3151
3420
  import { fileURLToPath } from "node:url";
3152
3421
  function fields(bytes) {
@@ -3184,7 +3453,7 @@ function fields(bytes) {
3184
3453
  }
3185
3454
  return result2;
3186
3455
  }
3187
- function readCursor(path, projectDir) {
3456
+ function readCursor(path, projectDir, checkCwd) {
3188
3457
  const db = new DatabaseSync2(path, { readOnly: true });
3189
3458
  try {
3190
3459
  db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=100; BEGIN");
@@ -3195,7 +3464,7 @@ function readCursor(path, projectDir) {
3195
3464
  if (typeof hex !== "string" || !/^(?:[a-fA-F0-9]{2})+$/.test(hex))
3196
3465
  throw new Error("Invalid Cursor metadata.");
3197
3466
  const meta = object(JSON.parse(Buffer.from(hex, "hex").toString("utf8")));
3198
- if (typeof meta.agentId !== "string" || meta.agentId !== basename2(dirname7(path)) || meta.subagentInfo != null)
3467
+ if (typeof meta.agentId !== "string" || meta.agentId !== basename2(dirname8(path)) || meta.subagentInfo != null)
3199
3468
  throw new Error("Invalid or delegated Cursor session identity.");
3200
3469
  let budget = 64 * 1024 * 1024;
3201
3470
  const blob = (id) => {
@@ -3205,13 +3474,13 @@ function readCursor(path, projectDir) {
3205
3474
  throw new Error("Cursor history missing or exceeds 64 MiB.");
3206
3475
  const bytes = Buffer.from(value);
3207
3476
  budget -= bytes.length;
3208
- if (createHash10("sha256").update(bytes).digest("hex") !== id)
3477
+ if (createHash11("sha256").update(bytes).digest("hex") !== id)
3209
3478
  throw new Error("Cursor blob failed integrity verification.");
3210
3479
  return bytes;
3211
3480
  };
3212
3481
  const root = fields(blob(String(meta.latestRootBlobId)));
3213
- const bucket = basename2(dirname7(dirname7(path)));
3214
- const md5 = (cwd2) => createHash10("md5").update(resolve7(cwd2)).digest("hex");
3482
+ const bucket = basename2(dirname8(dirname8(path)));
3483
+ const md5 = (cwd2) => createHash11("md5").update(resolve8(cwd2)).digest("hex");
3215
3484
  let cwd = md5(projectDir) === bucket ? projectDir : void 0;
3216
3485
  for (const uri of root.get(9) ?? []) {
3217
3486
  try {
@@ -3224,6 +3493,7 @@ function readCursor(path, projectDir) {
3224
3493
  }
3225
3494
  }
3226
3495
  if (!cwd) throw new Error("Cursor workspace cannot be verified against its storage bucket.");
3496
+ checkCwd?.(cwd);
3227
3497
  const refs = [];
3228
3498
  for (const archive of root.get(13) ?? root.get(11) ?? [])
3229
3499
  refs.push(...fields(blob(archive.toString("hex"))).get(1) ?? []);
@@ -3246,7 +3516,7 @@ function readCursor(path, projectDir) {
3246
3516
  if (start >= 0 && end > start) text = text.slice(start + 12, end).trim();
3247
3517
  }
3248
3518
  if (!text.trim()) continue;
3249
- const id = createHash10("sha256").update(JSON.stringify([message2.role, cursor.requestId ?? "", text])).digest("hex");
3519
+ const id = createHash11("sha256").update(JSON.stringify([message2.role, cursor.requestId ?? "", text])).digest("hex");
3250
3520
  const count = (counts.get(id) ?? 0) + 1;
3251
3521
  counts.set(id, count);
3252
3522
  messages.push({
@@ -3270,9 +3540,14 @@ function readCursor(path, projectDir) {
3270
3540
  }
3271
3541
 
3272
3542
  // apps/client/sessions/source.ts
3273
- var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
3543
+ function within(project, cwd) {
3544
+ const rel = relative4(project, cwd);
3545
+ return rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute4(rel);
3546
+ }
3547
+ var cursorEncoded = (cwd) => cwd.replace(/^[/\\]+/, "").replace(/[/\\]/g, "-");
3548
+ var cursorDatabase = (path) => basename3(path) === "store.db";
3274
3549
  async function sourceSignature(path, agent) {
3275
- const paths = agent === "cursor" ? [path, `${path}-wal`] : [path];
3550
+ const paths = agent === "cursor" && cursorDatabase(path) ? [path, `${path}-wal`] : [path];
3276
3551
  const signatures = [];
3277
3552
  for (const file of paths) {
3278
3553
  try {
@@ -3293,60 +3568,109 @@ async function sourceSignature(path, agent) {
3293
3568
  }
3294
3569
  return JSON.stringify(signatures);
3295
3570
  }
3571
+ async function cursorScope(path, project) {
3572
+ let container = dirname9(path);
3573
+ while (basename3(container) !== "agent-transcripts" && dirname9(container) !== container)
3574
+ container = dirname9(container);
3575
+ if (basename3(container) !== "agent-transcripts")
3576
+ throw new Error("Cursor transcript workspace cannot be verified.");
3577
+ const encoded = basename3(dirname9(container));
3578
+ const matches = [];
3579
+ const visit = async (directory) => {
3580
+ const prefix = cursorEncoded(directory);
3581
+ if (encoded === prefix) {
3582
+ matches.push(directory);
3583
+ return;
3584
+ }
3585
+ if (!encoded.startsWith(prefix + "-")) return;
3586
+ for (const entry of await readdir2(directory, { withFileTypes: true })) {
3587
+ if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(join15(directory, entry.name));
3588
+ }
3589
+ };
3590
+ await visit(project);
3591
+ if (matches.length !== 1)
3592
+ throw new Error("Session cwd is outside the registered project or ambiguous.");
3593
+ return matches[0];
3594
+ }
3296
3595
  async function readSource(agent, path, projectDir) {
3297
3596
  const signature2 = await sourceSignature(path, agent);
3298
- let result2;
3299
- if (agent === "cursor") result2 = { snapshot: readCursor(path, projectDir), incomplete: false };
3300
- else {
3301
- const handle = await open5(path, "r");
3302
- let bytes;
3303
- try {
3304
- const size = (await handle.stat()).size;
3305
- if (size > MAX_SOURCE_BYTES) throw new Error("Session source exceeds 64 MiB.");
3306
- bytes = Buffer.alloc(size + 1);
3307
- let offset = 0;
3308
- while (offset < bytes.length) {
3309
- const read = await handle.read(bytes, offset, bytes.length - offset, offset);
3310
- if (!read.bytesRead) break;
3311
- offset += read.bytesRead;
3312
- }
3313
- bytes = bytes.subarray(0, offset);
3314
- if (offset > size) throw new Error("Session source changed while reading; retry next cycle.");
3315
- } finally {
3316
- await handle.close();
3597
+ const project = await realpath5(projectDir);
3598
+ if (agent === "cursor" && cursorDatabase(path)) {
3599
+ const snapshot2 = readCursor(path, projectDir, (cwd) => {
3600
+ if (!within(project, realpathSync(cwd)))
3601
+ throw new Error("Session cwd is outside the registered project.");
3602
+ });
3603
+ return { snapshot: snapshot2, incomplete: false, skipped: 0, signature: signature2 };
3604
+ }
3605
+ const cursor = agent === "cursor" ? { nativeId: basename3(path, ".jsonl"), cwd: await cursorScope(path, projectDir) } : void 0;
3606
+ const normalizer = new JsonlNormalizer(agent, cursor);
3607
+ const stream = createReadStream(path, {
3608
+ end: Math.max(0, Number(JSON.parse(signature2)[0][2]) - 1)
3609
+ });
3610
+ let input = stream;
3611
+ if (agent === "dsh" && path.endsWith(".zstd")) {
3612
+ const decompress = zlib.createZstdDecompress;
3613
+ if (!decompress) {
3614
+ stream.destroy();
3615
+ throw new Error("Compressed DSH sessions require Node.js with zstd support (22.15+ or 24+).");
3317
3616
  }
3318
- if (agent === "dsh" && path.endsWith(".zstd")) {
3319
- const decompress = zlib.zstdDecompressSync;
3320
- if (!decompress)
3321
- throw new Error(
3322
- "Compressed DSH sessions require Node.js with zstd support (22.15+ or 24+)."
3323
- );
3324
- bytes = decompress(bytes, { maxOutputLength: MAX_SOURCE_BYTES });
3617
+ const decoder = decompress();
3618
+ stream.on("error", (e) => decoder.destroy(e));
3619
+ input = stream.pipe(decoder);
3620
+ }
3621
+ const lines = createInterface3({ input, crlfDelay: Infinity });
3622
+ const checkedCwds = /* @__PURE__ */ new Set();
3623
+ let line = 0, skipped = 0, lastBad = false;
3624
+ try {
3625
+ for await (const text of lines) {
3626
+ line++;
3627
+ if (!text.trim()) continue;
3628
+ const row = parseRecord(text);
3629
+ lastBad = !row;
3630
+ if (!row) {
3631
+ skipped++;
3632
+ continue;
3633
+ }
3634
+ const cwd = row.cwd ?? (row.type === "session_meta" ? object(row.payload).cwd : void 0);
3635
+ if (typeof cwd === "string" && !checkedCwds.has(cwd)) {
3636
+ const actual = await realpath5(cwd).catch((error) => {
3637
+ if (error.code === "ENOENT" && !within(project, cwd))
3638
+ throw new Error("Session cwd is outside the registered project.");
3639
+ throw error;
3640
+ });
3641
+ if (!within(project, actual))
3642
+ throw new Error("Session cwd is outside the registered project.");
3643
+ checkedCwds.add(cwd);
3644
+ }
3645
+ normalizer.consume(row, line);
3325
3646
  }
3326
- result2 = normalizeJsonl(agent, new TextDecoder("utf-8", { fatal: true }).decode(bytes));
3647
+ } finally {
3648
+ lines.close();
3649
+ stream.destroy();
3650
+ if (input !== stream) input.destroy();
3327
3651
  }
3328
- if (signature2 !== await sourceSignature(path, agent))
3652
+ const before = JSON.parse(signature2)[0];
3653
+ const after = JSON.parse(await sourceSignature(path, agent))[0];
3654
+ if (before[0] !== after[0] || before[1] !== after[1] || Number(after[2]) < Number(before[2]) || before[2] === after[2] && before.join() !== after.join())
3329
3655
  throw new Error("Session source changed while reading; retry next cycle.");
3330
- const cwd = await realpath5(result2.snapshot.cwd);
3331
- const project = await realpath5(projectDir);
3332
- const rel = relative4(project, cwd);
3333
- if (rel === ".." || rel.startsWith(`..${sep5}`) || isAbsolute4(rel))
3656
+ const snapshot = normalizer.finish();
3657
+ if (!within(project, await realpath5(snapshot.cwd)))
3334
3658
  throw new Error("Session cwd is outside the registered project.");
3335
- return { ...result2, signature: signature2 };
3659
+ return { snapshot, incomplete: lastBad, skipped, signature: signature2 };
3336
3660
  }
3337
3661
 
3338
3662
  // apps/client/sessions/discovery.ts
3339
3663
  import { opendir as opendir2 } from "node:fs/promises";
3340
- import { join as join14 } from "node:path";
3664
+ import { join as join16 } from "node:path";
3341
3665
  async function* candidates(directory, agent) {
3342
3666
  const queue = [directory];
3343
3667
  while (queue.length) {
3344
3668
  const current = queue.shift();
3345
3669
  const dir = await opendir2(current);
3346
3670
  for await (const entry of dir) {
3347
- const path = join14(current, entry.name);
3671
+ const path = join16(current, entry.name);
3348
3672
  if (entry.isDirectory()) queue.push(path);
3349
- if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
3673
+ if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" || entry.name.endsWith(".jsonl") : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
3350
3674
  yield path;
3351
3675
  else yield null;
3352
3676
  }
@@ -3393,25 +3717,27 @@ var SessionDiscovery = class {
3393
3717
  "SELECT 1 FROM session_discovery_cache WHERE root=? AND path=? AND signature=?"
3394
3718
  ).get(this.state.root, path, signature2))
3395
3719
  continue;
3396
- reads--;
3397
3720
  try {
3398
3721
  const result2 = await readSource(config.agent, path, config.projectDir);
3399
- if (result2.incomplete) continue;
3722
+ if (!result2.snapshot.messages.length) continue;
3723
+ reads--;
3400
3724
  this.state.track(config.agent, path, config.projectDir);
3401
3725
  } catch (error) {
3402
3726
  const message2 = error instanceof Error ? error.message : "Discovery failed.";
3403
3727
  if (!message2.includes("outside the registered project"))
3404
3728
  issues.push(`${path}: ${message2}`);
3405
- if (!message2.includes("outside the registered project")) continue;
3729
+ if (error.code === "ENOENT" || /locked|busy|changed while reading/i.test(message2))
3730
+ continue;
3406
3731
  }
3407
3732
  db.prepare(
3408
3733
  "INSERT INTO session_discovery_cache VALUES (?,?,?) ON CONFLICT(root,path) DO UPDATE SET signature=excluded.signature"
3409
3734
  ).run(this.state.root, path, signature2);
3410
3735
  }
3411
3736
  } catch (error) {
3412
- issues.push(
3413
- `${config.directory}: ${error instanceof Error ? error.message : "Discovery failed."}`
3414
- );
3737
+ if (error.code !== "ENOENT")
3738
+ issues.push(
3739
+ `${config.directory}: ${error instanceof Error ? error.message : "Discovery failed."}`
3740
+ );
3415
3741
  await scan.iterator.return(void 0);
3416
3742
  scan.iterator = candidates(config.directory, config.agent);
3417
3743
  scan.nextAt = Date.now() + 3e5;
@@ -3419,6 +3745,9 @@ var SessionDiscovery = class {
3419
3745
  }
3420
3746
  return issues;
3421
3747
  }
3748
+ async refresh() {
3749
+ for (const scan of this.scans.values()) scan.nextAt = 0;
3750
+ }
3422
3751
  async close() {
3423
3752
  for (const scan of this.scans.values()) await scan.iterator.return(void 0);
3424
3753
  }
@@ -3432,7 +3761,7 @@ var SessionSynchronizer = class {
3432
3761
  const binding = state.binding(root);
3433
3762
  if (!binding) throw new Error("Connect this root before collecting sessions.");
3434
3763
  this.client = new HubClient(binding.serverUrl, 1e4, binding, signal);
3435
- this.blobs = new LocalBlobStore(join15(directory, "blobs"));
3764
+ this.blobs = new LocalBlobStore(join17(directory, "blobs"));
3436
3765
  this.index = new SessionState(state, root);
3437
3766
  this.discovery = new SessionDiscovery(this.index);
3438
3767
  }
@@ -3444,9 +3773,16 @@ var SessionSynchronizer = class {
3444
3773
  discovery;
3445
3774
  capable = false;
3446
3775
  async cycle() {
3447
- const report = { published: 0, checked: 0, issues: [] };
3448
- report.issues.push(...await consumeHints(this.directory, this.root, this.index));
3449
- report.issues.push(...await this.discovery.cycle());
3776
+ const report = { published: 0, checked: 0, issues: [], warnings: [] };
3777
+ report.issues.push(
3778
+ ...await consumeHints(
3779
+ this.directory,
3780
+ this.root,
3781
+ this.index,
3782
+ () => this.discovery.refresh()
3783
+ )
3784
+ );
3785
+ report.warnings.push(...await this.discovery.cycle());
3450
3786
  const sources = this.index.sources(void 0, 200);
3451
3787
  if (sources.length && !this.capable) {
3452
3788
  const info = await this.client.serverInfo();
@@ -3458,12 +3794,14 @@ var SessionSynchronizer = class {
3458
3794
  }
3459
3795
  let reads = 5;
3460
3796
  let uploads = 5;
3461
- for (const source of sources) {
3797
+ for (const scheduled of sources) {
3798
+ let source = this.index.get(scheduled.path);
3462
3799
  if (reads <= 0 || uploads <= 0 || report.checked >= 200) break;
3463
3800
  report.checked++;
3464
3801
  this.index.checked(source.path, source.lastError);
3465
3802
  try {
3466
3803
  if (source.pending) {
3804
+ if (source.pendingPath !== source.path) continue;
3467
3805
  if (source.retryAt > Date.now()) {
3468
3806
  if (source.lastError)
3469
3807
  report.issues.push(`${source.path}: ${source.lastError} (retry deferred)`);
@@ -3481,12 +3819,24 @@ var SessionSynchronizer = class {
3481
3819
  }
3482
3820
  reads--;
3483
3821
  const observed = await readSource(source.agent, source.path, source.projectDir);
3484
- if (observed.incomplete)
3485
- throw new Error("Final session record is incomplete; waiting for the next save.");
3486
3822
  if (!observed.snapshot.messages.length) {
3487
3823
  this.index.checked(source.path);
3488
3824
  continue;
3489
3825
  }
3826
+ if (observed.skipped)
3827
+ report.warnings.push(
3828
+ `${source.path}: skipped ${observed.skipped} unreadable record(s); retained complete dialogue.`
3829
+ );
3830
+ if (observed.skipped && source.hash) {
3831
+ const previous = await this.blobs.get(source.hash);
3832
+ if (previous && JSON.parse(Buffer.from(previous).toString("utf8")).messages.length > observed.snapshot.messages.length) {
3833
+ this.index.observed(source.path, observed.signature);
3834
+ report.warnings.push(
3835
+ `${source.path}: kept the previous snapshot because the damaged copy contains fewer messages.`
3836
+ );
3837
+ continue;
3838
+ }
3839
+ }
3490
3840
  const key = sessionKey(source.agent, observed.snapshot.nativeId);
3491
3841
  if (source.key && source.key !== key)
3492
3842
  throw new Error(
@@ -3497,7 +3847,7 @@ var SessionSynchronizer = class {
3497
3847
  throw new Error("Normalized session exceeds the 10 MiB snapshot limit.");
3498
3848
  const blob = await this.blobs.put(bytes);
3499
3849
  let revision = source.revision;
3500
- if (!source.key) {
3850
+ if (!source.key && !this.index.publication(key)) {
3501
3851
  try {
3502
3852
  const owner = this.index.state.binding(this.root).memberId;
3503
3853
  const current = await this.client.session(owner, key);
@@ -3510,7 +3860,14 @@ var SessionSynchronizer = class {
3510
3860
  if (!(error instanceof HubRequestError && error.code === "SESSION_NOT_FOUND"))
3511
3861
  throw error;
3512
3862
  }
3513
- this.index.baseline(source.path, key, revision);
3863
+ }
3864
+ this.index.baseline(source.path, key, revision);
3865
+ source = this.index.get(source.path);
3866
+ revision = source.revision;
3867
+ if (source.pending) continue;
3868
+ if (source.agent === "cursor" && source.selectedPath && cursorDatabase(source.selectedPath) && !cursorDatabase(source.path)) {
3869
+ this.index.observed(source.path, observed.signature);
3870
+ continue;
3514
3871
  }
3515
3872
  if (blob.sha256 === source.hash) {
3516
3873
  this.index.acknowledge(source.path, revision, blob.sha256, observed.signature);
@@ -3670,7 +4027,7 @@ var SyncJob = class {
3670
4027
  pendingPublication: false,
3671
4028
  issues: []
3672
4029
  };
3673
- let sessions = { published: 0, checked: 0, issues: [] };
4030
+ let sessions = { published: 0, checked: 0, issues: [], warnings: [] };
3674
4031
  try {
3675
4032
  await verifyWorkspace(this.state, this.root);
3676
4033
  if (!this.controlStarted) {
@@ -3680,6 +4037,8 @@ var SyncJob = class {
3680
4037
  }
3681
4038
  if (this.files) report = await this.files.cycle();
3682
4039
  sessions = await this.sessions.cycle();
4040
+ for (const warning of sessions.warnings)
4041
+ this.log.write("sessions.partial", this.root, warning);
3683
4042
  } catch (e) {
3684
4043
  report.issues.push(diagnostic(e));
3685
4044
  }
@@ -3730,7 +4089,7 @@ var SyncJob = class {
3730
4089
  );
3731
4090
  }
3732
4091
  this.lastIssue = issueKey;
3733
- this.nextAt = now + (issue ? Math.min(9e5, Math.max(this.intervalMs, 1e4) * 2 ** Math.min(this.issueCount - 1, 5)) : pendingDownloads ? 250 : this.intervalMs);
4092
+ this.nextAt = now + (report.issues.length || this.control.enabled && (this.control.status.configError || this.control.status.reportError) ? Math.min(9e5, Math.max(this.intervalMs, 1e4) * 2 ** Math.min(this.issueCount - 1, 5)) : pendingDownloads ? 250 : this.intervalMs);
3734
4093
  this.runtime.save(this.root, {
3735
4094
  state: this.stopped ? "stopped" : issue ? "waiting" : "idle",
3736
4095
  checkedAt,
@@ -3996,8 +4355,8 @@ async function runDaemon(directory, version) {
3996
4355
  let timer;
3997
4356
  let resolveStop = () => {
3998
4357
  };
3999
- const stopped = new Promise((resolve9) => {
4000
- resolveStop = resolve9;
4358
+ const stopped = new Promise((resolve10) => {
4359
+ resolveStop = resolve10;
4001
4360
  });
4002
4361
  const stop = () => {
4003
4362
  closing = true;
@@ -4008,11 +4367,11 @@ async function runDaemon(directory, version) {
4008
4367
  await unlink4(socketPath(directory)).catch((e) => {
4009
4368
  if (e.code !== "ENOENT") throw e;
4010
4369
  });
4011
- await new Promise((resolve9, reject) => {
4370
+ await new Promise((resolve10, reject) => {
4012
4371
  server.once("error", reject);
4013
4372
  server.listen(socketPath(directory), () => {
4014
4373
  server.off("error", reject);
4015
- resolve9();
4374
+ resolve10();
4016
4375
  });
4017
4376
  });
4018
4377
  await chmod2(socketPath(directory), 384);
@@ -4035,7 +4394,7 @@ async function runDaemon(directory, version) {
4035
4394
  await Promise.all([...jobs.values(), ...temporaryJobs].map((job) => job.close()));
4036
4395
  await Promise.allSettled(oneShots.values());
4037
4396
  for (const socket of sockets) socket.destroy();
4038
- await new Promise((resolve9) => server.close(() => resolve9()));
4397
+ await new Promise((resolve10) => server.close(() => resolve10()));
4039
4398
  await unlink4(socketPath(directory)).catch(() => {
4040
4399
  });
4041
4400
  log.write("daemon.stopped");
@@ -4094,7 +4453,7 @@ async function runCli(args, version) {
4094
4453
  options: { "state-dir": { type: "string" } }
4095
4454
  });
4096
4455
  if (!values["state-dir"]) throw new Error("Missing daemon state directory.");
4097
- const directory = resolve8(values["state-dir"]);
4456
+ const directory = resolve9(values["state-dir"]);
4098
4457
  try {
4099
4458
  await runDaemon(directory, version);
4100
4459
  return 0;