cortex-sync 0.1.0 → 0.3.0

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 +39 -2
  2. package/dist/cli.js +283 -11
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Claude Code stores your session history in `~/.claude/projects/` using absolute paths. Switch from your Mac to a Linux server and those sessions are gone — the paths don't match. `cortex` fixes that.
6
6
 
7
7
  ```bash
8
- npm install -g cortex-cli
8
+ npm install -g cortex-sync
9
9
  ```
10
10
 
11
11
  ---
@@ -31,7 +31,7 @@ Machine A (Mac) Machine B (Linux)
31
31
 
32
32
  ```bash
33
33
  # 1. Install
34
- npm install -g cortex-cli
34
+ npm install -g cortex-sync
35
35
 
36
36
  # 2. Configure on Machine A
37
37
  cortex init
@@ -59,6 +59,43 @@ Open any project on Machine B — Claude Code shows your full session history.
59
59
  | `cortex pull` | Download, decrypt, remap paths |
60
60
  | `cortex status` | Show what's out of sync (no download) |
61
61
  | `cortex convert <file> --to <target>` | Convert a Claude Code skill |
62
+ | `cortex setup-mcp` | Register cortex as a Claude Code MCP server |
63
+
64
+ ---
65
+
66
+ ## Claude Code MCP integration
67
+
68
+ Use `sync`, `pull`, `status`, `convert`, and `init` directly from the Claude Code chat — one command does everything:
69
+
70
+ ```bash
71
+ npm install -g cortex-sync
72
+ cortex init # configure storage and passphrase
73
+ cortex setup-mcp # registers cortex in Claude Code automatically
74
+ ```
75
+
76
+ That's it. Restart Claude Code and you're done.
77
+
78
+ ### What setup-mcp does
79
+
80
+ `cortex setup-mcp` detects the binary path, prompts for your passphrase once, and runs `claude mcp add` with the correct arguments — no manual PATH configuration needed.
81
+
82
+ ### Available MCP tools
83
+
84
+ | Tool | What it does |
85
+ |---|---|
86
+ | `sync` | Encrypt and upload `~/.claude/` |
87
+ | `pull` | Download, decrypt, remap paths |
88
+ | `status` | Show what's out of sync |
89
+ | `convert` | Convert a skill to Antigravity or Cursor |
90
+ | `init` | Configure storage (non-interactive) |
91
+
92
+ ### Environment variables (advanced / manual setup)
93
+
94
+ | Variable | Required for |
95
+ |---|---|
96
+ | `CORTEX_PASSPHRASE` | `sync`, `pull`, `status` |
97
+ | `ANTHROPIC_API_KEY` | `convert` |
98
+ | `CORTEX_GITHUB_TOKEN` | `init` with GitHub storage |
62
99
 
63
100
  ---
64
101
 
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { createRequire } from "module";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/commands/convert.ts
@@ -58,8 +59,8 @@ function decrypt(blob, derived) {
58
59
  if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) {
59
60
  throw new Error("cortex blob has bad magic bytes");
60
61
  }
61
- const version = blob[MAGIC.length];
62
- if (version !== VERSION) throw new Error(`cortex blob version ${version} not supported`);
62
+ const version2 = blob[MAGIC.length];
63
+ if (version2 !== VERSION) throw new Error(`cortex blob version ${version2} not supported`);
63
64
  const ivStart = MAGIC.length + 1;
64
65
  const iv = blob.subarray(ivStart, ivStart + IV_LEN);
65
66
  const tag = blob.subarray(ivStart + IV_LEN, ivStart + IV_LEN + TAG_LEN);
@@ -85,7 +86,7 @@ async function readPassphrase() {
85
86
 
86
87
  // src/lib/api-key.ts
87
88
  var API_KEY_PATH = join2(CORTEX_DIR, "api-key.enc");
88
- async function loadApiKey() {
89
+ async function loadApiKey(opts = {}) {
89
90
  if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
90
91
  if (existsSync(API_KEY_PATH)) {
91
92
  const config = await loadConfig();
@@ -94,6 +95,11 @@ async function loadApiKey() {
94
95
  const enc = await readFile2(API_KEY_PATH);
95
96
  return decrypt(enc, derived).toString("utf-8").trim();
96
97
  }
98
+ if (opts.nonInteractive) {
99
+ throw new Error(
100
+ 'ANTHROPIC_API_KEY environment variable is not set and no encrypted key found.\nSet it with: export ANTHROPIC_API_KEY="sk-ant-..."'
101
+ );
102
+ }
97
103
  const key = await password2({
98
104
  message: "Anthropic API key (sk-ant-...):",
99
105
  mask: "*",
@@ -257,10 +263,10 @@ async function convertCommand(skillPath, opts) {
257
263
  const skill = parseSkillMeta(source, fallbackName);
258
264
  console.log(`Converting "${skill.name}" \u2192 ${opts.to}
259
265
  `);
260
- const apiKey = await loadApiKey();
266
+ const apiKey = opts.apiKey ?? await loadApiKey();
261
267
  const targets = opts.to === "all" ? ["antigravity", "cursor"] : [opts.to];
262
268
  for (const target of targets) {
263
- process.stdout.write(` ${target}\u2026 `);
269
+ console.log(` ${target}\u2026`);
264
270
  let outPath;
265
271
  if (target === "antigravity") {
266
272
  outPath = await convertToAntigravity(skill, apiKey, outputDir);
@@ -465,7 +471,7 @@ async function initCommand() {
465
471
  mask: "*",
466
472
  validate: (v) => v.trim().startsWith("gh") || "Token should start with gh"
467
473
  });
468
- process.stdout.write("Validating token\u2026 ");
474
+ console.log("Validating token\u2026");
469
475
  githubOwner = await fetchGitHubUser(githubToken.trim());
470
476
  console.log(`\u2713 Authenticated as ${githubOwner}`);
471
477
  githubRepo = await input({
@@ -473,7 +479,7 @@ async function initCommand() {
473
479
  default: "cortex-backup",
474
480
  validate: (v) => /^[a-zA-Z0-9_.-]+$/.test(v.trim()) || "Invalid repo name"
475
481
  });
476
- process.stdout.write(`Creating private repo ${githubOwner}/${githubRepo}\u2026 `);
482
+ console.log(`Creating private repo ${githubOwner}/${githubRepo}\u2026`);
477
483
  await ensureGitHubRepo(githubToken.trim(), githubRepo.trim());
478
484
  console.log("\u2713 Ready");
479
485
  githubToken = githubToken.trim();
@@ -516,6 +522,50 @@ Detected tools: ${detected.length ? detected.join(", ") : "none"}`);
516
522
  console.log('Next step: Google Drive backend is not yet implemented \u2014 use --target <path> with "cortex sync" for now.');
517
523
  }
518
524
  }
525
+ async function initNonInteractive(opts) {
526
+ if (!opts.email) throw new Error("email is required");
527
+ if (!opts.storage) throw new Error('storage is required: "github" or "local"');
528
+ if (opts.storage === "local" && !opts.target) {
529
+ throw new Error('target path is required when storage is "local"');
530
+ }
531
+ let githubToken;
532
+ let githubOwner;
533
+ let githubRepo;
534
+ if (opts.storage === "github") {
535
+ githubToken = process.env.CORTEX_GITHUB_TOKEN;
536
+ if (!githubToken) {
537
+ throw new Error(
538
+ 'CORTEX_GITHUB_TOKEN environment variable is not set.\nCreate a PAT at: https://github.com/settings/tokens/new?scopes=repo\nThen set: export CORTEX_GITHUB_TOKEN="ghp_..."'
539
+ );
540
+ }
541
+ githubOwner = await fetchGitHubUser(githubToken);
542
+ console.log(`\u2713 Authenticated as ${githubOwner}`);
543
+ githubRepo = opts.githubRepo ?? "cortex-backup";
544
+ await ensureGitHubRepo(githubToken, githubRepo);
545
+ console.log(`\u2713 Repo ${githubOwner}/${githubRepo} ready`);
546
+ }
547
+ const detected = await detectInstalledTools();
548
+ await mkdir3(CORTEX_DIR2, { recursive: true });
549
+ const config = {
550
+ version: 1,
551
+ storage: opts.storage,
552
+ email: opts.email,
553
+ target: opts.target,
554
+ githubToken,
555
+ githubOwner,
556
+ githubRepo,
557
+ tools: detected,
558
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
559
+ };
560
+ await writeFile4(CONFIG_PATH2, JSON.stringify(config, null, 2), { mode: 384 });
561
+ await chmod2(CONFIG_PATH2, 384);
562
+ console.log(`\u2713 Configuration saved to ${CONFIG_PATH2}`);
563
+ }
564
+
565
+ // src/commands/mcp.ts
566
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
567
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
568
+ import { z } from "zod";
519
569
 
520
570
  // src/commands/pull.ts
521
571
  import { input as input2 } from "@inquirer/prompts";
@@ -800,11 +850,14 @@ function extractCwdFromJsonl(input3) {
800
850
  }
801
851
 
802
852
  // src/commands/pull.ts
803
- async function resolveLocalPath(projectId, originalPath, mappings) {
853
+ async function resolveLocalPath(projectId, originalPath, mappings, projectMappings, nonInteractive) {
804
854
  const key = projectId ?? originalPath;
855
+ if (projectMappings?.[key]) return projectMappings[key];
856
+ if (projectId && projectMappings?.[originalPath]) return projectMappings[originalPath];
805
857
  if (mappings[key]) return mappings[key];
806
858
  if (projectId && mappings[originalPath]) return mappings[originalPath];
807
859
  if (existsSync5(originalPath)) return originalPath;
860
+ if (nonInteractive) return null;
808
861
  console.log(`
809
862
  Project not found on this machine:`);
810
863
  console.log(` Original path: ${originalPath}`);
@@ -836,6 +889,7 @@ async function pullCommand(opts = {}) {
836
889
  );
837
890
  const mappings = await loadMappings();
838
891
  const dirRemap = /* @__PURE__ */ new Map();
892
+ const pendingMappings = [];
839
893
  if (remote.projects) {
840
894
  let mappingsDirty = false;
841
895
  for (const [encodedDir, meta] of Object.entries(remote.projects)) {
@@ -844,8 +898,21 @@ async function pullCommand(opts = {}) {
844
898
  dirRemap.set(encodedDir, null);
845
899
  continue;
846
900
  }
847
- const localPath = await resolveLocalPath(meta.projectId, meta.originalPath, mappings);
901
+ const localPath = await resolveLocalPath(
902
+ meta.projectId,
903
+ meta.originalPath,
904
+ mappings,
905
+ opts.projectMappings,
906
+ opts.nonInteractive
907
+ );
848
908
  if (localPath === null) {
909
+ if (opts.nonInteractive) {
910
+ pendingMappings.push({
911
+ encodedDir,
912
+ projectId: meta.projectId,
913
+ originalPath: meta.originalPath
914
+ });
915
+ }
849
916
  dirRemap.set(encodedDir, null);
850
917
  continue;
851
918
  }
@@ -893,6 +960,10 @@ async function pullCommand(opts = {}) {
893
960
  await saveManifest(MANIFEST_PATH, remote);
894
961
  console.log(`
895
962
  \u2713 Pull complete \u2014 ${toPull.length} files restored.`);
963
+ return {
964
+ filesRestored: toPull.length,
965
+ pendingMappings: pendingMappings.length > 0 ? pendingMappings : void 0
966
+ };
896
967
  }
897
968
 
898
969
  // src/commands/status.ts
@@ -1072,12 +1143,211 @@ async function syncCommand(opts = {}) {
1072
1143
  );
1073
1144
  }
1074
1145
 
1146
+ // src/commands/mcp.ts
1147
+ var logToStderr = (...args) => process.stderr.write(args.map(String).join(" ") + "\n");
1148
+ console.log = logToStderr;
1149
+ function requirePassphrase() {
1150
+ if (!process.env.CORTEX_PASSPHRASE) {
1151
+ throw new Error(
1152
+ 'CORTEX_PASSPHRASE environment variable is not set.\nRun "cortex init" from your terminal first, then set:\n export CORTEX_PASSPHRASE="your-passphrase"'
1153
+ );
1154
+ }
1155
+ }
1156
+ async function captureOutput(fn) {
1157
+ const lines = [];
1158
+ const prev = console.log;
1159
+ console.log = (...args) => lines.push(args.map(String).join(" "));
1160
+ try {
1161
+ const result = await fn();
1162
+ console.log = prev;
1163
+ return { result, output: lines.join("\n") };
1164
+ } catch (e) {
1165
+ console.log = prev;
1166
+ throw e;
1167
+ }
1168
+ }
1169
+ function ok(text) {
1170
+ return { content: [{ type: "text", text }] };
1171
+ }
1172
+ function toolErr(e) {
1173
+ const msg = e instanceof Error ? e.message : String(e);
1174
+ return { content: [{ type: "text", text: msg }], isError: true };
1175
+ }
1176
+ async function mcpCommand() {
1177
+ const server = new McpServer({ name: "cortex", version: "0.1.0" });
1178
+ server.registerTool(
1179
+ "sync",
1180
+ {
1181
+ description: "Encrypt ~/.claude/ and upload to configured storage. Requires CORTEX_PASSPHRASE env var.",
1182
+ inputSchema: z.object({
1183
+ skipSecretsCheck: z.boolean().optional().describe("Skip the API key detection warning before encrypting"),
1184
+ target: z.string().optional().describe("Override storage to a local folder path")
1185
+ })
1186
+ },
1187
+ async ({ skipSecretsCheck, target }) => {
1188
+ try {
1189
+ requirePassphrase();
1190
+ const { output } = await captureOutput(() => syncCommand({ skipSecretsCheck, target }));
1191
+ return ok(output);
1192
+ } catch (e) {
1193
+ return toolErr(e);
1194
+ }
1195
+ }
1196
+ );
1197
+ server.registerTool(
1198
+ "pull",
1199
+ {
1200
+ description: "Download from storage, decrypt, and remap paths into ~/.claude/. If pendingMappings is returned, call pull again with projectMappings populated.",
1201
+ inputSchema: z.object({
1202
+ target: z.string().optional().describe("Override storage to a local folder path"),
1203
+ projectMappings: z.record(z.string(), z.string()).optional().describe("Map of projectId or originalPath to local path on this machine")
1204
+ })
1205
+ },
1206
+ async ({ target, projectMappings }) => {
1207
+ try {
1208
+ requirePassphrase();
1209
+ const result = await pullCommand({ target, projectMappings, nonInteractive: true });
1210
+ if (result.pendingMappings?.length) {
1211
+ const lines = result.pendingMappings.map(
1212
+ (p) => ` "${p.originalPath}" (projectId: ${p.projectId ?? "none"})`
1213
+ );
1214
+ return ok(
1215
+ `Restored ${result.filesRestored} files.
1216
+
1217
+ These projects need a local path mapping.
1218
+ Call pull again with projectMappings, e.g.:
1219
+ { "${result.pendingMappings[0].projectId ?? result.pendingMappings[0].originalPath}": "/your/local/path" }
1220
+
1221
+ Pending projects:
1222
+ ${lines.join("\n")}`
1223
+ );
1224
+ }
1225
+ return ok(`Pull complete. ${result.filesRestored} files restored.`);
1226
+ } catch (e) {
1227
+ return toolErr(e);
1228
+ }
1229
+ }
1230
+ );
1231
+ server.registerTool(
1232
+ "status",
1233
+ {
1234
+ description: "Show what is out of sync between local ~/.claude/ files and storage.",
1235
+ inputSchema: z.object({
1236
+ target: z.string().optional().describe("Override storage to a local folder path")
1237
+ })
1238
+ },
1239
+ async ({ target }) => {
1240
+ try {
1241
+ requirePassphrase();
1242
+ const { output } = await captureOutput(() => statusCommand({ target }));
1243
+ return ok(output);
1244
+ } catch (e) {
1245
+ return toolErr(e);
1246
+ }
1247
+ }
1248
+ );
1249
+ server.registerTool(
1250
+ "convert",
1251
+ {
1252
+ description: "Convert a Claude Code skill to Antigravity or Cursor format using the Anthropic API. Requires ANTHROPIC_API_KEY env var (or ~/.cortex/api-key.enc).",
1253
+ inputSchema: z.object({
1254
+ skillPath: z.string().describe("Absolute path to the Claude Code skill .md file"),
1255
+ to: z.enum(["antigravity", "cursor", "all"]).describe("Target format"),
1256
+ outputDir: z.string().optional().describe("Project root where output files are written (default: cwd)")
1257
+ })
1258
+ },
1259
+ async ({ skillPath, to, outputDir }) => {
1260
+ try {
1261
+ let apiKey;
1262
+ try {
1263
+ apiKey = await loadApiKey({ nonInteractive: true });
1264
+ } catch (e) {
1265
+ return toolErr(e);
1266
+ }
1267
+ const { output } = await captureOutput(
1268
+ () => convertCommand(skillPath, { to, outputDir, apiKey })
1269
+ );
1270
+ return ok(output);
1271
+ } catch (e) {
1272
+ return toolErr(e);
1273
+ }
1274
+ }
1275
+ );
1276
+ server.registerTool(
1277
+ "init",
1278
+ {
1279
+ description: "Configure cortex storage. For GitHub, requires CORTEX_GITHUB_TOKEN env var.",
1280
+ inputSchema: z.object({
1281
+ email: z.string().describe("Email used as salt for key derivation"),
1282
+ storage: z.enum(["github", "local"]).describe("Storage backend"),
1283
+ githubRepo: z.string().optional().describe("GitHub repo name for backup (default: cortex-backup)"),
1284
+ target: z.string().optional().describe('Local folder path \u2014 required when storage is "local"')
1285
+ })
1286
+ },
1287
+ async (params) => {
1288
+ try {
1289
+ const { output } = await captureOutput(() => initNonInteractive(params));
1290
+ return ok(output);
1291
+ } catch (e) {
1292
+ return toolErr(e);
1293
+ }
1294
+ }
1295
+ );
1296
+ const transport = new StdioServerTransport();
1297
+ await server.connect(transport);
1298
+ process.stderr.write("cortex MCP server running on stdio\n");
1299
+ process.on("SIGINT", async () => {
1300
+ await server.close();
1301
+ process.exit(0);
1302
+ });
1303
+ }
1304
+
1305
+ // src/commands/setup-mcp.ts
1306
+ import { password as password4 } from "@inquirer/prompts";
1307
+ import { existsSync as existsSync7 } from "fs";
1308
+ import { spawnSync } from "child_process";
1309
+ import { dirname as dirname6, join as join11 } from "path";
1310
+ async function setupMcpCommand() {
1311
+ const cortexBin = join11(dirname6(process.execPath), "cortex");
1312
+ if (!existsSync7(cortexBin)) {
1313
+ throw new Error(
1314
+ `cortex binary not found at ${cortexBin}.
1315
+ Make sure cortex-sync is installed globally: npm install -g cortex-sync`
1316
+ );
1317
+ }
1318
+ console.log(`Found cortex at: ${cortexBin}`);
1319
+ const passphrase = await password4({
1320
+ message: "Encryption passphrase (same one used in cortex init):",
1321
+ mask: "*",
1322
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
1323
+ });
1324
+ const result = spawnSync(
1325
+ "claude",
1326
+ ["mcp", "add", "cortex", "-e", `CORTEX_PASSPHRASE=${passphrase}`, "--", cortexBin, "mcp"],
1327
+ { stdio: "inherit" }
1328
+ );
1329
+ if (result.error) {
1330
+ throw new Error(
1331
+ `Failed to run "claude" CLI: ${result.error.message}
1332
+ Make sure Claude Code CLI is installed and "claude" is in your PATH.`
1333
+ );
1334
+ }
1335
+ if (result.status !== 0) {
1336
+ throw new Error('"claude mcp add" failed. Check the output above for details.');
1337
+ }
1338
+ console.log("\n\u2713 cortex MCP server registered in Claude Code.");
1339
+ console.log("Restart Claude Code (or open a new session) to activate it.");
1340
+ console.log("\nTo verify: claude mcp list");
1341
+ }
1342
+
1075
1343
  // src/cli.ts
1344
+ var require2 = createRequire(import.meta.url);
1345
+ var { version } = require2("../package.json");
1076
1346
  var program = new Command();
1077
- program.name("cortex").description("Sync Claude Code context between machines with path remapping").version("0.0.1");
1347
+ program.name("cortex").description("Sync Claude Code context between machines with path remapping").version(version);
1078
1348
  program.command("init").description("Configure Cortex: pick storage, set passphrase, detect tools").action(initCommand);
1079
1349
  program.command("sync").description("Encrypt local files and upload to the configured storage").option("--target <path>", "Override storage to a local folder (overrides config)").option("--skip-secrets-check", "Skip the regex scan for API keys before encrypting").action(syncCommand);
1080
- program.command("pull").description("Download from storage and restore into ~/.claude/").option("--target <path>", "Override storage to a local folder (overrides config)").action(pullCommand);
1350
+ program.command("pull").description("Download from storage and restore into ~/.claude/").option("--target <path>", "Override storage to a local folder (overrides config)").action((opts) => void pullCommand(opts));
1081
1351
  program.command("status").description("Show what is out of sync between local files and storage").option("--target <path>", "Override storage to a local folder (overrides config)").action(statusCommand);
1082
1352
  program.command("convert <skill-file>").description("Convert a Claude Code skill to Antigravity or Cursor format").requiredOption("--to <target>", "Target format: antigravity | cursor | all").option("--output-dir <path>", "Project root where output files are written (default: cwd)").action((skillFile, opts) => {
1083
1353
  const validTargets = ["antigravity", "cursor", "all"];
@@ -1087,4 +1357,6 @@ program.command("convert <skill-file>").description("Convert a Claude Code skill
1087
1357
  }
1088
1358
  return convertCommand(skillFile, { to: opts.to, outputDir: opts.outputDir });
1089
1359
  });
1360
+ program.command("mcp").description("Start the MCP server (for use with claude mcp add cortex -- cortex mcp)").action(mcpCommand);
1361
+ program.command("setup-mcp").description("Register cortex as a Claude Code MCP server automatically").action(setupMcpCommand);
1090
1362
  await program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortex-sync",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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",
@@ -36,7 +36,9 @@
36
36
  "dependencies": {
37
37
  "@anthropic-ai/sdk": "^0.95.2",
38
38
  "@inquirer/prompts": "^7.0.0",
39
- "commander": "^12.1.0"
39
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
+ "commander": "^12.1.0",
41
+ "zod": "^4.4.3"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@types/node": "^22.0.0",