cortex-sync 0.4.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.js +193 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,49 @@ Open any project on Machine B — Claude Code shows your full session history.
65
65
 
66
66
  ---
67
67
 
68
+ ## Team context
69
+
70
+ Share skills, CLAUDE.md, plugins, and chat sessions with your team via a shared GitHub repo.
71
+
72
+ ```bash
73
+ # Tech Lead — one-time setup
74
+ cortex team init --repo https://github.com/your-org/claude-config
75
+
76
+ # Push your local .claude/ context + sessions (if opted in)
77
+ cortex team push
78
+
79
+ # Dev — pull team context + sessions
80
+ cortex team pull
81
+
82
+ # New dev — first-time install
83
+ cortex install --repo https://github.com/your-org/claude-config
84
+ ```
85
+
86
+ ### What gets shared
87
+
88
+ | What | Source | Destination |
89
+ |---|---|---|
90
+ | Skills | `.claude/skills/*.md` | `skills/` in team repo |
91
+ | CLAUDE.md | `.claude/CLAUDE.md` | `CLAUDE.md` in team repo |
92
+ | Plugins | Installed Claude plugins | `cortex.json → plugins[]` |
93
+ | Sessions (opt-in) | `~/.claude/projects/<project>/` | `sessions/<email>/<project-id>/` |
94
+
95
+ ### Session sharing
96
+
97
+ During `cortex team init` you are asked once whether to share your Claude Code chat sessions with the team. If you accept:
98
+
99
+ - Sessions are uploaded on every `cortex team push`
100
+ - Teammates get your sessions (paths remapped to their machine) on `cortex team pull`
101
+ - Claude Code shows all team sessions natively — no extra steps
102
+
103
+ You can choose to encrypt sessions with a shared team passphrase (AES-256-GCM). Share the passphrase with your team via a password manager — it is never stored by cortex.
104
+
105
+ **Two machines, same GitHub user:** Each machine generates unique session IDs, so pushing from both machines never creates duplicates.
106
+
107
+ > **Privacy:** Sessions may contain source code, API calls, and sensitive context. Only opt in if your team has a shared understanding that sessions are visible to all members.
108
+
109
+ ---
110
+
68
111
  ## Claude Code MCP integration
69
112
 
70
113
  Use `sync`, `pull`, `status`, `convert`, and `init` directly from the Claude Code chat — one command does everything:
package/dist/cli.js CHANGED
@@ -1341,9 +1341,9 @@ Make sure Claude Code CLI is installed and "claude" is in your PATH.`
1341
1341
  }
1342
1342
 
1343
1343
  // src/commands/team/init.ts
1344
- import { input as input3 } from "@inquirer/prompts";
1345
- import { writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
1346
- import { join as join16 } from "path";
1344
+ import { input as input3, confirm as confirm3, select as select2, password as password5 } from "@inquirer/prompts";
1345
+ import { writeFile as writeFile12, mkdir as mkdir11 } from "fs/promises";
1346
+ import { join as join17 } from "path";
1347
1347
 
1348
1348
  // src/lib/team-repo.ts
1349
1349
  import { access as access3, mkdir as mkdir8, rm } from "fs/promises";
@@ -1486,6 +1486,107 @@ async function writeProjectConfig(updates, cwd = process.cwd()) {
1486
1486
  await writeFile10(join15(cwd, "cortex.json"), JSON.stringify(merged, null, 2), "utf-8");
1487
1487
  }
1488
1488
 
1489
+ // src/lib/team-sessions.ts
1490
+ import { readdir as readdir4, readFile as readFile13, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
1491
+ import { homedir as homedir5 } from "os";
1492
+ import { join as join16 } from "path";
1493
+ function localSessionsDir(cwd) {
1494
+ return join16(homedir5(), ".claude", "projects", encodeProjectPath(cwd));
1495
+ }
1496
+ function teamSessionsDir(base, email, projectId) {
1497
+ return join16(base, "sessions", email, projectId);
1498
+ }
1499
+ async function readSessionFiles(dir) {
1500
+ const result = /* @__PURE__ */ new Map();
1501
+ try {
1502
+ const entries = await readdir4(dir);
1503
+ for (const entry of entries) {
1504
+ if (entry.endsWith(".jsonl")) {
1505
+ result.set(entry, await readFile13(join16(dir, entry)));
1506
+ }
1507
+ }
1508
+ } catch {
1509
+ }
1510
+ return result;
1511
+ }
1512
+ async function copySessionsToTeamDir(srcDir, destDir, derived) {
1513
+ const sessions = await readSessionFiles(srcDir);
1514
+ if (sessions.size === 0) return 0;
1515
+ await mkdir10(destDir, { recursive: true });
1516
+ for (const [filename, content] of sessions) {
1517
+ if (derived) {
1518
+ await writeFile11(join16(destDir, `${filename}.enc`), encrypt(content, derived));
1519
+ } else {
1520
+ await writeFile11(join16(destDir, filename), content);
1521
+ }
1522
+ }
1523
+ return sessions.size;
1524
+ }
1525
+ async function copySessionsFromRepo(sessionsRoot, projectId, destDir, localCwd, derived) {
1526
+ let devFolders;
1527
+ try {
1528
+ devFolders = await readdir4(sessionsRoot);
1529
+ } catch {
1530
+ return 0;
1531
+ }
1532
+ await mkdir10(destDir, { recursive: true });
1533
+ let count = 0;
1534
+ for (const email of devFolders) {
1535
+ const projDir = join16(sessionsRoot, email, projectId);
1536
+ let files;
1537
+ try {
1538
+ files = await readdir4(projDir);
1539
+ } catch {
1540
+ continue;
1541
+ }
1542
+ for (const file of files) {
1543
+ try {
1544
+ const isEnc = file.endsWith(".jsonl.enc");
1545
+ const isPlain = file.endsWith(".jsonl") && !isEnc;
1546
+ if (!isEnc && !isPlain) continue;
1547
+ if (isEnc && !derived) continue;
1548
+ let data = await readFile13(join16(projDir, file));
1549
+ if (isEnc && derived) {
1550
+ data = decrypt(data, derived);
1551
+ }
1552
+ const remoteCwd = extractCwdFromJsonl(data);
1553
+ if (remoteCwd && remoteCwd !== localCwd) {
1554
+ data = remapJsonlBuffer(data, remoteCwd, localCwd);
1555
+ }
1556
+ const destFilename = isEnc ? file.slice(0, -".enc".length) : file;
1557
+ const destPath = join16(destDir, destFilename);
1558
+ let finalPath = destPath;
1559
+ try {
1560
+ await readFile13(destPath);
1561
+ const initials = email.split("@")[0].slice(0, 3);
1562
+ const base = destFilename.slice(0, -".jsonl".length);
1563
+ finalPath = join16(destDir, `${base}.${initials}.jsonl`);
1564
+ } catch {
1565
+ }
1566
+ await writeFile11(finalPath, data);
1567
+ count++;
1568
+ } catch {
1569
+ continue;
1570
+ }
1571
+ }
1572
+ }
1573
+ return count;
1574
+ }
1575
+ async function pushSessions(email, cwd, derived) {
1576
+ const projectInfo = identifyProject(cwd);
1577
+ if (!projectInfo) return 0;
1578
+ const srcDir = localSessionsDir(cwd);
1579
+ const destDir = teamSessionsDir(TEAM_DIR, email, projectInfo.projectId);
1580
+ return copySessionsToTeamDir(srcDir, destDir, derived);
1581
+ }
1582
+ async function pullSessions(cwd, derived) {
1583
+ const projectInfo = identifyProject(cwd);
1584
+ if (!projectInfo) return 0;
1585
+ const sessionsRoot = join16(TEAM_DIR, "sessions");
1586
+ const destDir = localSessionsDir(cwd);
1587
+ return copySessionsFromRepo(sessionsRoot, projectInfo.projectId, destDir, cwd, derived);
1588
+ }
1589
+
1489
1590
  // src/commands/team/init.ts
1490
1591
  async function teamInitCommand(opts) {
1491
1592
  const config = await loadConfig();
@@ -1500,33 +1601,67 @@ Cloning ${repoUrl} \u2192 ~/.cortex/team/`);
1500
1601
  await cloneTeamRepo(repoUrl, token);
1501
1602
  const skills = await readSkillsFromDir(LOCAL_SKILLS_DIR);
1502
1603
  if (skills.size > 0) {
1503
- await mkdir10(join16(TEAM_DIR, "skills"), { recursive: true });
1604
+ await mkdir11(join17(TEAM_DIR, "skills"), { recursive: true });
1504
1605
  for (const [filename, content] of skills) {
1505
- await writeFile11(join16(TEAM_DIR, "skills", filename), content, "utf-8");
1606
+ await writeFile12(join17(TEAM_DIR, "skills", filename), content, "utf-8");
1506
1607
  }
1507
1608
  console.log(` Copied ${skills.size} skills from .claude/skills/`);
1508
1609
  }
1509
1610
  const claudeMd = await readFileFromPath(LOCAL_CLAUDE_MD);
1510
1611
  if (claudeMd) {
1511
- await writeFile11(join16(TEAM_DIR, "CLAUDE.md"), claudeMd, "utf-8");
1612
+ await writeFile12(join17(TEAM_DIR, "CLAUDE.md"), claudeMd, "utf-8");
1512
1613
  console.log(" Copied .claude/CLAUDE.md");
1513
1614
  }
1514
1615
  const plugins = await getInstalledPluginIds();
1515
1616
  const cortexJson = { version: "1", plugins };
1516
- await writeFile11(join16(TEAM_DIR, "cortex.json"), JSON.stringify(cortexJson, null, 2), "utf-8");
1617
+ await writeFile12(join17(TEAM_DIR, "cortex.json"), JSON.stringify(cortexJson, null, 2), "utf-8");
1517
1618
  console.log(` Generated cortex.json (${plugins.length} plugins)`);
1518
1619
  commitAndPush(repoUrl, token, "feat: initial team Claude Code context");
1519
- await writeProjectConfig({ repo: repoUrl });
1620
+ console.log("\n\u26A0 Compartir sesiones de chat");
1621
+ console.log(" Tus sesiones de Claude Code para este proyecto se subir\xEDan");
1622
+ console.log(" al repo de equipo y ser\xEDan visibles por todos los miembros.");
1623
+ console.log(" Las sesiones pueden contener c\xF3digo privado o informaci\xF3n sensible.");
1624
+ const shareSession = await confirm3({
1625
+ message: "\xBFCompartir sesiones con el equipo?",
1626
+ default: false
1627
+ });
1628
+ let encryptSessions = false;
1629
+ if (shareSession) {
1630
+ const encChoice = await select2({
1631
+ message: "\xBFEncriptar las sesiones?",
1632
+ choices: [
1633
+ { name: "S\xED \u2014 encriptadas con passphrase del equipo (recomendado)", value: true },
1634
+ { name: "No \u2014 JSONL plano (legible directo en GitHub)", value: false }
1635
+ ]
1636
+ });
1637
+ encryptSessions = encChoice;
1638
+ let derived;
1639
+ if (encryptSessions) {
1640
+ const teamPassphrase = await password5({
1641
+ message: "Team passphrase (shared with all devs, min 12 chars):",
1642
+ mask: "*",
1643
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
1644
+ });
1645
+ derived = deriveKey(teamPassphrase, repoUrl);
1646
+ }
1647
+ const count = await pushSessions(config.email, process.cwd(), derived);
1648
+ if (count > 0) {
1649
+ commitAndPush(repoUrl, token, `feat: share ${count} team sessions`);
1650
+ console.log(` Uploaded ${count} sessions`);
1651
+ }
1652
+ }
1653
+ await writeProjectConfig({ repo: repoUrl, shareSession, encryptSessions });
1520
1654
  console.log("\n\u2713 Team repo initialized and pushed.");
1521
1655
  console.log(` Devs can now run: cortex install --repo ${repoUrl}`);
1522
1656
  }
1523
1657
 
1524
1658
  // src/commands/team/push.ts
1525
- import { writeFile as writeFile12, mkdir as mkdir11 } from "fs/promises";
1526
- import { join as join17 } from "path";
1659
+ import { writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
1660
+ import { join as join18 } from "path";
1661
+ import { password as password6 } from "@inquirer/prompts";
1527
1662
  async function teamPushCommand() {
1528
1663
  const config = await loadConfig();
1529
- const { repo: repoUrl } = await readProjectConfig();
1664
+ const { repo: repoUrl, shareSession, encryptSessions } = await readProjectConfig();
1530
1665
  const token = config.githubToken;
1531
1666
  if (!repoUrl) throw new Error('No team repo configured. Run "cortex team init --repo <url>" first.');
1532
1667
  if (!token) throw new Error('No GitHub token found. Run "cortex init" first.');
@@ -1535,31 +1670,45 @@ async function teamPushCommand() {
1535
1670
  pullTeamRepo();
1536
1671
  const skills = await readSkillsFromDir(LOCAL_SKILLS_DIR);
1537
1672
  if (skills.size > 0) {
1538
- await mkdir11(join17(TEAM_DIR, "skills"), { recursive: true });
1673
+ await mkdir12(join18(TEAM_DIR, "skills"), { recursive: true });
1539
1674
  for (const [filename, content] of skills) {
1540
- await writeFile12(join17(TEAM_DIR, "skills", filename), content, "utf-8");
1675
+ await writeFile13(join18(TEAM_DIR, "skills", filename), content, "utf-8");
1541
1676
  }
1542
1677
  }
1543
1678
  const claudeMd = await readFileFromPath(LOCAL_CLAUDE_MD);
1544
1679
  if (claudeMd) {
1545
- await writeFile12(join17(TEAM_DIR, "CLAUDE.md"), claudeMd, "utf-8");
1680
+ await writeFile13(join18(TEAM_DIR, "CLAUDE.md"), claudeMd, "utf-8");
1546
1681
  }
1547
1682
  const plugins = await getInstalledPluginIds();
1548
- await writeFile12(
1549
- join17(TEAM_DIR, "cortex.json"),
1683
+ await writeFile13(
1684
+ join18(TEAM_DIR, "cortex.json"),
1550
1685
  JSON.stringify({ version: "1", plugins }, null, 2),
1551
1686
  "utf-8"
1552
1687
  );
1688
+ if (shareSession) {
1689
+ let derived;
1690
+ if (encryptSessions) {
1691
+ const teamPassphrase = await password6({
1692
+ message: "Team passphrase:",
1693
+ mask: "*",
1694
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
1695
+ });
1696
+ derived = deriveKey(teamPassphrase, repoUrl);
1697
+ }
1698
+ const count = await pushSessions(config.email, process.cwd(), derived);
1699
+ if (count > 0) console.log(` Uploaded ${count} sessions`);
1700
+ }
1553
1701
  commitAndPush(repoUrl, token, "chore: update team Claude Code context");
1554
1702
  console.log("\n\u2713 Pushed to team repo.");
1555
1703
  }
1556
1704
 
1557
1705
  // src/commands/team/pull.ts
1558
- import { readFile as readFile13 } from "fs/promises";
1559
- import { join as join18 } from "path";
1706
+ import { readFile as readFile14 } from "fs/promises";
1707
+ import { join as join19 } from "path";
1708
+ import { password as password7 } from "@inquirer/prompts";
1560
1709
 
1561
1710
  // src/lib/conflict.ts
1562
- import { select as select2 } from "@inquirer/prompts";
1711
+ import { select as select3 } from "@inquirer/prompts";
1563
1712
  function hasConflict(local, remote) {
1564
1713
  return local.trim() !== remote.trim();
1565
1714
  }
@@ -1593,7 +1742,7 @@ function displayDiff(filename, local, remote) {
1593
1742
  }
1594
1743
  async function promptConflict(filename, local, remote) {
1595
1744
  displayDiff(filename, local, remote);
1596
- return select2({
1745
+ return select3({
1597
1746
  message: "How to resolve?",
1598
1747
  choices: [
1599
1748
  { name: "[M] Merge \u2014 keep both versions", value: "merge" },
@@ -1606,12 +1755,12 @@ async function promptConflict(filename, local, remote) {
1606
1755
  // src/commands/team/pull.ts
1607
1756
  async function teamPullCommand() {
1608
1757
  await loadConfig();
1609
- const { repo: repoUrl } = await readProjectConfig();
1758
+ const { repo: repoUrl, encryptSessions } = await readProjectConfig();
1610
1759
  if (!repoUrl) throw new Error('No team repo configured. Run "cortex team init --repo <url>" first.');
1611
1760
  if (!await hasLocalClone()) throw new Error('No local team clone found. Run "cortex install" first.');
1612
1761
  console.log("Pulling from team repo\u2026");
1613
1762
  pullTeamRepo();
1614
- const remoteSkills = await readSkillsFromDir(join18(TEAM_DIR, "skills"));
1763
+ const remoteSkills = await readSkillsFromDir(join19(TEAM_DIR, "skills"));
1615
1764
  const localSkills = await readSkillsFromDir(LOCAL_SKILLS_DIR);
1616
1765
  for (const [filename, remoteContent] of remoteSkills) {
1617
1766
  const localContent = localSkills.get(filename) ?? null;
@@ -1632,7 +1781,7 @@ async function teamPullCommand() {
1632
1781
  console.log(` ~ ${filename} skipped`);
1633
1782
  }
1634
1783
  }
1635
- const remoteMd = await readFileFromPath(join18(TEAM_DIR, "CLAUDE.md"));
1784
+ const remoteMd = await readFileFromPath(join19(TEAM_DIR, "CLAUDE.md"));
1636
1785
  if (remoteMd) {
1637
1786
  const localMd = await readFileFromPath(LOCAL_CLAUDE_MD);
1638
1787
  if (!localMd) {
@@ -1651,24 +1800,37 @@ async function teamPullCommand() {
1651
1800
  }
1652
1801
  }
1653
1802
  }
1654
- let cortexJson = {};
1803
+ let cortexManifest = {};
1655
1804
  try {
1656
- cortexJson = JSON.parse(await readFile13(join18(TEAM_DIR, "cortex.json"), "utf-8"));
1805
+ cortexManifest = JSON.parse(await readFile14(join19(TEAM_DIR, "cortex.json"), "utf-8"));
1657
1806
  } catch {
1658
1807
  }
1659
- for (const pluginId of cortexJson.plugins ?? []) {
1808
+ for (const pluginId of cortexManifest.plugins ?? []) {
1660
1809
  try {
1661
1810
  installPlugin(pluginId);
1662
1811
  } catch (e) {
1663
1812
  console.warn(` \u26A0 Could not install ${pluginId}: ${e.message}`);
1664
1813
  }
1665
1814
  }
1815
+ let derived;
1816
+ if (encryptSessions) {
1817
+ const teamPassphrase = await password7({
1818
+ message: "Team passphrase (to decrypt sessions):",
1819
+ mask: "*",
1820
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
1821
+ });
1822
+ derived = deriveKey(teamPassphrase, repoUrl);
1823
+ }
1824
+ const sessionCount = await pullSessions(process.cwd(), derived);
1825
+ if (sessionCount > 0) {
1826
+ console.log(` + ${sessionCount} sessions installed (restart Claude Code to see them)`);
1827
+ }
1666
1828
  console.log("\n\u2713 Team context applied.");
1667
1829
  }
1668
1830
 
1669
1831
  // src/commands/install.ts
1670
- import { readFile as readFile14 } from "fs/promises";
1671
- import { join as join19 } from "path";
1832
+ import { readFile as readFile15 } from "fs/promises";
1833
+ import { join as join20 } from "path";
1672
1834
  async function installCommand(opts) {
1673
1835
  let config;
1674
1836
  try {
@@ -1685,19 +1847,19 @@ async function installCommand(opts) {
1685
1847
  if (!token) throw new Error('No GitHub token found. Run "cortex init" first.');
1686
1848
  console.log(`Installing from ${repoUrl} into ${process.cwd()}/.claude/`);
1687
1849
  await cloneTeamRepo(repoUrl, token);
1688
- const skills = await readSkillsFromDir(join19(TEAM_DIR, "skills"));
1850
+ const skills = await readSkillsFromDir(join20(TEAM_DIR, "skills"));
1689
1851
  for (const [filename, content] of skills) {
1690
1852
  await writeSkillToDir(LOCAL_SKILLS_DIR, filename, content);
1691
1853
  console.log(` + .claude/skills/${filename}`);
1692
1854
  }
1693
- const claudeMd = await readFileFromPath(join19(TEAM_DIR, "CLAUDE.md"));
1855
+ const claudeMd = await readFileFromPath(join20(TEAM_DIR, "CLAUDE.md"));
1694
1856
  if (claudeMd) {
1695
1857
  await writeFileToPath(LOCAL_CLAUDE_MD, claudeMd);
1696
1858
  console.log(" + .claude/CLAUDE.md");
1697
1859
  }
1698
1860
  let cortexJson = {};
1699
1861
  try {
1700
- cortexJson = JSON.parse(await readFile14(join19(TEAM_DIR, "cortex.json"), "utf-8"));
1862
+ cortexJson = JSON.parse(await readFile15(join20(TEAM_DIR, "cortex.json"), "utf-8"));
1701
1863
  } catch {
1702
1864
  }
1703
1865
  for (const pluginId of cortexJson.plugins ?? []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortex-sync",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Sync Claude Code sessions between machines with automatic path remapping and skill conversion",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",