cortex-sync 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/convert.ts
7
+ import { readFile as readFile5 } from "fs/promises";
8
+ import { basename, extname, resolve } from "path";
9
+
10
+ // src/lib/api-key.ts
11
+ import { password as password2, confirm } from "@inquirer/prompts";
12
+ import { existsSync } from "fs";
13
+ import { chmod, readFile as readFile2, writeFile, mkdir } from "fs/promises";
14
+ import { join as join2, dirname } from "path";
15
+
16
+ // src/lib/config.ts
17
+ import { readFile } from "fs/promises";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ var CORTEX_DIR = join(homedir(), ".cortex");
21
+ var CONFIG_PATH = join(CORTEX_DIR, "config.json");
22
+ var MANIFEST_PATH = join(CORTEX_DIR, "manifest.json");
23
+ async function loadConfig() {
24
+ let buf;
25
+ try {
26
+ buf = await readFile(CONFIG_PATH);
27
+ } catch {
28
+ throw new Error(`No cortex config found at ${CONFIG_PATH}. Run "cortex init" first.`);
29
+ }
30
+ return JSON.parse(buf.toString("utf-8"));
31
+ }
32
+
33
+ // src/lib/crypto.ts
34
+ import { createCipheriv, createDecipheriv, createHash, pbkdf2Sync, randomBytes } from "crypto";
35
+ var MAGIC = Buffer.from("CTXP", "utf-8");
36
+ var VERSION = 1;
37
+ var IV_LEN = 12;
38
+ var TAG_LEN = 16;
39
+ var KEY_LEN = 32;
40
+ var HEADER_LEN = MAGIC.length + 1 + IV_LEN + TAG_LEN;
41
+ var PBKDF2_ITERATIONS = 6e5;
42
+ function deriveKey(passphrase, email) {
43
+ if (!passphrase) throw new Error("passphrase is required");
44
+ if (!email) throw new Error("email is required");
45
+ const salt = createHash("sha256").update(email.toLowerCase().trim()).digest();
46
+ const key = pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
47
+ return { key };
48
+ }
49
+ function encrypt(plaintext, derived) {
50
+ const iv = randomBytes(IV_LEN);
51
+ const cipher = createCipheriv("aes-256-gcm", derived.key, iv);
52
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
53
+ const tag = cipher.getAuthTag();
54
+ return Buffer.concat([MAGIC, Buffer.from([VERSION]), iv, tag, ciphertext]);
55
+ }
56
+ function decrypt(blob, derived) {
57
+ if (blob.length < HEADER_LEN) throw new Error("cortex blob too short");
58
+ if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) {
59
+ throw new Error("cortex blob has bad magic bytes");
60
+ }
61
+ const version = blob[MAGIC.length];
62
+ if (version !== VERSION) throw new Error(`cortex blob version ${version} not supported`);
63
+ const ivStart = MAGIC.length + 1;
64
+ const iv = blob.subarray(ivStart, ivStart + IV_LEN);
65
+ const tag = blob.subarray(ivStart + IV_LEN, ivStart + IV_LEN + TAG_LEN);
66
+ const ciphertext = blob.subarray(HEADER_LEN);
67
+ const decipher = createDecipheriv("aes-256-gcm", derived.key, iv);
68
+ decipher.setAuthTag(tag);
69
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
70
+ }
71
+ function checksumSha256(content) {
72
+ return createHash("sha256").update(content).digest("hex");
73
+ }
74
+
75
+ // src/lib/passphrase.ts
76
+ import { password } from "@inquirer/prompts";
77
+ async function readPassphrase() {
78
+ if (process.env.CORTEX_PASSPHRASE) return process.env.CORTEX_PASSPHRASE;
79
+ return password({
80
+ message: "Encryption passphrase:",
81
+ mask: "*",
82
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
83
+ });
84
+ }
85
+
86
+ // src/lib/api-key.ts
87
+ var API_KEY_PATH = join2(CORTEX_DIR, "api-key.enc");
88
+ async function loadApiKey() {
89
+ if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
90
+ if (existsSync(API_KEY_PATH)) {
91
+ const config = await loadConfig();
92
+ const passphrase = await readPassphrase();
93
+ const derived = deriveKey(passphrase, config.email);
94
+ const enc = await readFile2(API_KEY_PATH);
95
+ return decrypt(enc, derived).toString("utf-8").trim();
96
+ }
97
+ const key = await password2({
98
+ message: "Anthropic API key (sk-ant-...):",
99
+ mask: "*",
100
+ validate: (v) => v.trim().startsWith("sk-ant-") || "Key should start with sk-ant-"
101
+ });
102
+ const save = await confirm({
103
+ message: "Save encrypted to ~/.cortex/api-key.enc? (requires your sync passphrase)",
104
+ default: true
105
+ });
106
+ if (save) {
107
+ const config = await loadConfig();
108
+ const passphrase = await readPassphrase();
109
+ const derived = deriveKey(passphrase, config.email);
110
+ const enc = encrypt(Buffer.from(key.trim(), "utf-8"), derived);
111
+ await mkdir(dirname(API_KEY_PATH), { recursive: true });
112
+ await writeFile(API_KEY_PATH, enc, { mode: 384 });
113
+ await chmod(API_KEY_PATH, 384);
114
+ console.log(`\u2713 API key saved to ${API_KEY_PATH}`);
115
+ }
116
+ return key.trim();
117
+ }
118
+
119
+ // src/converters/shared.ts
120
+ import Anthropic from "@anthropic-ai/sdk";
121
+ function parseSkillMeta(content, fallbackName) {
122
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
123
+ let name = fallbackName;
124
+ let description;
125
+ if (fm) {
126
+ const nameMatch = fm[1].match(/^name:\s*(.+)$/m);
127
+ const descMatch = fm[1].match(/^description:\s*(.+)$/m);
128
+ if (nameMatch) name = nameMatch[1].trim();
129
+ if (descMatch) description = descMatch[1].trim();
130
+ }
131
+ return { name, description, body: content };
132
+ }
133
+ async function callClaude(apiKey, systemPrompt, userMessage) {
134
+ const client = new Anthropic({ apiKey });
135
+ const msg = await client.messages.create({
136
+ model: "claude-sonnet-4-6",
137
+ max_tokens: 4096,
138
+ system: systemPrompt,
139
+ messages: [{ role: "user", content: userMessage }]
140
+ });
141
+ const block = msg.content[0];
142
+ if (block.type !== "text") throw new Error("Unexpected response type from Claude API");
143
+ return block.text;
144
+ }
145
+
146
+ // src/converters/to-antigravity.ts
147
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
148
+ import { existsSync as existsSync2 } from "fs";
149
+ import { join as join3 } from "path";
150
+ var SYSTEM_PROMPT = `You are an expert at converting Claude Code skills to Antigravity format.
151
+
152
+ Claude Code skills are markdown files with frontmatter and instructions that tell the AI assistant how to behave or execute a specific workflow.
153
+
154
+ Antigravity uses a similar but distinct format:
155
+ - Frontmatter with: name, description, type: skill
156
+ - The skill body should be written as clear, direct instructions
157
+ - Use <artifact> tags for any templates, code patterns, or structured outputs the skill should produce
158
+ - Brain artifacts (content inside <artifact> tags) should have a clear id and optional title
159
+ - Preserve the full intent and precision of the original \u2014 do NOT simplify or water down the instructions
160
+ - If the original skill has step-by-step checklists, keep them
161
+ - If the original skill has conditional logic ("if X do Y"), keep it
162
+ - Adapt Claude Code specific references (like "TodoWrite", "Edit tool") to Antigravity equivalents where possible, or generalize them
163
+
164
+ Output ONLY the converted skill file content \u2014 no explanations, no markdown code fences.`;
165
+ async function convertToAntigravity(skill, apiKey, outputDir) {
166
+ const userMessage = `Convert this Claude Code skill to Antigravity format:
167
+
168
+ ${skill.body}`;
169
+ const converted = await callClaude(apiKey, SYSTEM_PROMPT, userMessage);
170
+ const skillDir = join3(outputDir, ".agent", "skills", skill.name);
171
+ const outPath = join3(skillDir, "SKILL.md");
172
+ const alreadyExists = existsSync2(outPath);
173
+ if (alreadyExists) {
174
+ const existing = await readFile3(outPath, "utf-8");
175
+ if (existing.trim() === converted.trim()) {
176
+ return outPath;
177
+ }
178
+ }
179
+ await mkdir2(skillDir, { recursive: true });
180
+ await writeFile2(outPath, converted, "utf-8");
181
+ return outPath;
182
+ }
183
+
184
+ // src/converters/to-cursor.ts
185
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
186
+ import { existsSync as existsSync3 } from "fs";
187
+ import { join as join4 } from "path";
188
+ var SYSTEM_PROMPT2 = `You are an expert at converting Claude Code skills to Cursor rules.
189
+
190
+ Claude Code skills are markdown files that encode workflows and behavioral instructions for the AI.
191
+
192
+ Cursor rules go in a .cursorrules file and guide the Cursor AI assistant in a project. They are:
193
+ - Written as direct instructions ("Always...", "When X, do Y", "Never...")
194
+ - Concise but complete \u2014 don't omit important constraints
195
+ - Project/workflow-focused rather than tool-specific
196
+ - Plain text with minimal markdown (headings and bullet lists are fine)
197
+
198
+ When converting:
199
+ - Preserve the core intent and all important constraints from the original
200
+ - Replace Claude Code specific tool references with behavior descriptions
201
+ (e.g. "use the Edit tool" \u2192 "edit files in place", "use TodoWrite" \u2192 "maintain a task list")
202
+ - If the skill has a multi-step workflow, convert it to numbered steps
203
+ - Do NOT lose conditional logic, edge cases, or guardrails \u2014 they are important
204
+
205
+ Output ONLY the rule content \u2014 no section headers, no explanations, no markdown code fences.
206
+ The calling code will wrap your output with the appropriate section markers.`;
207
+ var SECTION_START = (name) => `## ${name}`;
208
+ var SECTION_END = (name) => `## end ${name}`;
209
+ async function convertToCursor(skill, apiKey, outputDir) {
210
+ const userMessage = `Convert this Claude Code skill to a Cursor rule block:
211
+
212
+ ${skill.body}`;
213
+ const converted = await callClaude(apiKey, SYSTEM_PROMPT2, userMessage);
214
+ const outPath = join4(outputDir, ".cursorrules");
215
+ let existing = "";
216
+ if (existsSync3(outPath)) {
217
+ existing = await readFile4(outPath, "utf-8");
218
+ }
219
+ const start = SECTION_START(skill.name);
220
+ const end = SECTION_END(skill.name);
221
+ const sectionRegex = new RegExp(
222
+ `${escapeRegex(start)}[\\s\\S]*?${escapeRegex(end)}\\n?`,
223
+ "g"
224
+ );
225
+ const block = `${start}
226
+ ${converted.trim()}
227
+ ${end}
228
+ `;
229
+ const sectionPresent = new RegExp(
230
+ `${escapeRegex(start)}[\\s\\S]*?${escapeRegex(end)}`
231
+ ).test(existing);
232
+ let updated;
233
+ if (sectionPresent) {
234
+ updated = existing.replace(sectionRegex, block);
235
+ } else {
236
+ const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
237
+ updated = existing + separator + block;
238
+ }
239
+ await writeFile3(outPath, updated, "utf-8");
240
+ return outPath;
241
+ }
242
+ function escapeRegex(s) {
243
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
244
+ }
245
+
246
+ // src/commands/convert.ts
247
+ async function convertCommand(skillPath, opts) {
248
+ const absSkillPath = resolve(skillPath);
249
+ const outputDir = resolve(opts.outputDir ?? process.cwd());
250
+ let source;
251
+ try {
252
+ source = await readFile5(absSkillPath, "utf-8");
253
+ } catch {
254
+ throw new Error(`Cannot read skill file: ${absSkillPath}`);
255
+ }
256
+ const fallbackName = basename(absSkillPath, extname(absSkillPath)).toLowerCase();
257
+ const skill = parseSkillMeta(source, fallbackName);
258
+ console.log(`Converting "${skill.name}" \u2192 ${opts.to}
259
+ `);
260
+ const apiKey = await loadApiKey();
261
+ const targets = opts.to === "all" ? ["antigravity", "cursor"] : [opts.to];
262
+ for (const target of targets) {
263
+ process.stdout.write(` ${target}\u2026 `);
264
+ let outPath;
265
+ if (target === "antigravity") {
266
+ outPath = await convertToAntigravity(skill, apiKey, outputDir);
267
+ } else {
268
+ outPath = await convertToCursor(skill, apiKey, outputDir);
269
+ }
270
+ console.log(`\u2713 ${outPath}`);
271
+ }
272
+ console.log("\nDone.");
273
+ }
274
+
275
+ // src/commands/init.ts
276
+ import { confirm as confirm2, input, password as password3, select } from "@inquirer/prompts";
277
+ import { access, chmod as chmod2, mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
278
+ import { homedir as homedir3 } from "os";
279
+ import { join as join6 } from "path";
280
+
281
+ // src/adapters/paths.ts
282
+ import { homedir as homedir2, platform } from "os";
283
+ import { join as join5 } from "path";
284
+ function resolveToolPath(tool) {
285
+ const home = homedir2();
286
+ switch (tool) {
287
+ case "claude-code":
288
+ return join5(home, ".claude");
289
+ case "antigravity":
290
+ return join5(home, ".antigravity");
291
+ case "cursor":
292
+ return join5(home, ".cursor");
293
+ }
294
+ }
295
+
296
+ // src/storage/github.ts
297
+ var GitHubBackend = class {
298
+ constructor(token, owner, repo) {
299
+ this.token = token;
300
+ this.owner = owner;
301
+ this.repo = repo;
302
+ this.name = `GitHub (${owner}/${repo})`;
303
+ this.apiBase = `https://api.github.com/repos/${owner}/${repo}`;
304
+ this.headers = {
305
+ Authorization: `Bearer ${token}`,
306
+ Accept: "application/vnd.github+json",
307
+ "X-GitHub-Api-Version": "2022-11-28",
308
+ "User-Agent": "cortex-cli"
309
+ };
310
+ }
311
+ token;
312
+ owner;
313
+ repo;
314
+ name;
315
+ apiBase;
316
+ headers;
317
+ url(path) {
318
+ return `${this.apiBase}/contents/${path}`;
319
+ }
320
+ async getSha(path) {
321
+ const res = await fetch(this.url(path), { headers: this.headers });
322
+ if (res.status === 404) return null;
323
+ if (!res.ok) throw new Error(`GitHub API error ${res.status} on GET ${path}`);
324
+ const data = await res.json();
325
+ return data.sha;
326
+ }
327
+ async has(path) {
328
+ const res = await fetch(this.url(path), { headers: this.headers });
329
+ if (res.status === 404) return false;
330
+ if (!res.ok) throw new Error(`GitHub API error ${res.status} on has(${path})`);
331
+ return true;
332
+ }
333
+ async read(path) {
334
+ const res = await fetch(this.url(path), { headers: this.headers });
335
+ if (!res.ok) throw new Error(`GitHub read failed (${res.status}): ${path}`);
336
+ const data = await res.json();
337
+ return Buffer.from(data.content.replace(/\n/g, ""), "base64");
338
+ }
339
+ async write(path, content) {
340
+ const sha = await this.getSha(path);
341
+ const body = {
342
+ message: `cortex: update ${path}`,
343
+ content: content.toString("base64")
344
+ };
345
+ if (sha) body.sha = sha;
346
+ const res = await fetch(this.url(path), {
347
+ method: "PUT",
348
+ headers: { ...this.headers, "Content-Type": "application/json" },
349
+ body: JSON.stringify(body)
350
+ });
351
+ if (!res.ok) {
352
+ const text = await res.text();
353
+ throw new Error(`GitHub write failed (${res.status}): ${text}`);
354
+ }
355
+ }
356
+ async remove(path) {
357
+ const sha = await this.getSha(path);
358
+ if (!sha) return;
359
+ const res = await fetch(this.url(path), {
360
+ method: "DELETE",
361
+ headers: { ...this.headers, "Content-Type": "application/json" },
362
+ body: JSON.stringify({ message: `cortex: remove ${path}`, sha })
363
+ });
364
+ if (!res.ok) {
365
+ const text = await res.text();
366
+ throw new Error(`GitHub delete failed (${res.status}): ${text}`);
367
+ }
368
+ }
369
+ async list() {
370
+ const res = await fetch(`${this.apiBase}/git/trees/HEAD?recursive=1`, {
371
+ headers: this.headers
372
+ });
373
+ if (res.status === 404 || res.status === 409) return [];
374
+ if (!res.ok) throw new Error(`GitHub list failed (${res.status})`);
375
+ const data = await res.json();
376
+ return data.tree.filter((n) => n.type === "blob").map((n) => ({ path: n.path, size: n.size ?? 0 }));
377
+ }
378
+ };
379
+ async function fetchGitHubUser(token) {
380
+ const res = await fetch("https://api.github.com/user", {
381
+ headers: {
382
+ Authorization: `Bearer ${token}`,
383
+ Accept: "application/vnd.github+json",
384
+ "X-GitHub-Api-Version": "2022-11-28",
385
+ "User-Agent": "cortex-cli"
386
+ }
387
+ });
388
+ if (!res.ok) throw new Error(`GitHub token validation failed (${res.status}). Check your PAT.`);
389
+ const data = await res.json();
390
+ return data.login;
391
+ }
392
+ async function ensureGitHubRepo(token, repo) {
393
+ const res = await fetch("https://api.github.com/user/repos", {
394
+ method: "POST",
395
+ headers: {
396
+ Authorization: `Bearer ${token}`,
397
+ Accept: "application/vnd.github+json",
398
+ "X-GitHub-Api-Version": "2022-11-28",
399
+ "User-Agent": "cortex-cli",
400
+ "Content-Type": "application/json"
401
+ },
402
+ body: JSON.stringify({
403
+ name: repo,
404
+ private: true,
405
+ description: "cortex encrypted backup \u2014 do not modify manually",
406
+ auto_init: true
407
+ // creates initial commit so HEAD exists
408
+ })
409
+ });
410
+ if (!res.ok && res.status !== 422) {
411
+ const text = await res.text();
412
+ throw new Error(`Failed to create GitHub repo (${res.status}): ${text}`);
413
+ }
414
+ }
415
+
416
+ // src/commands/init.ts
417
+ var CORTEX_DIR2 = join6(homedir3(), ".cortex");
418
+ var CONFIG_PATH2 = join6(CORTEX_DIR2, "config.json");
419
+ async function pathExists(p) {
420
+ try {
421
+ await access(p);
422
+ return true;
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+ async function detectInstalledTools() {
428
+ const candidates = ["claude-code", "antigravity", "cursor"];
429
+ const detected = [];
430
+ for (const tool of candidates) {
431
+ if (await pathExists(resolveToolPath(tool))) detected.push(tool);
432
+ }
433
+ return detected;
434
+ }
435
+ async function initCommand() {
436
+ console.log("cortex init \u2014 configure your sync setup\n");
437
+ const email = await input({
438
+ message: "Your email (used as salt for key derivation, never sent anywhere):",
439
+ validate: (v) => /.+@.+\..+/.test(v) || "Enter a valid email"
440
+ });
441
+ const storage = await select({
442
+ message: "Where do you want to store your synced files?",
443
+ choices: [
444
+ { name: "GitHub private repo (PAT \u2014 no OAuth app needed)", value: "github" },
445
+ { name: "Local folder (Dropbox / iCloud Drive / Syncthing)", value: "local" },
446
+ { name: "Google Drive \u2014 coming in a later release", value: "gdrive" }
447
+ ]
448
+ });
449
+ let target;
450
+ let githubToken;
451
+ let githubOwner;
452
+ let githubRepo;
453
+ if (storage === "local") {
454
+ target = await input({
455
+ message: "Path to the synced folder (e.g. ~/Dropbox/cortex-backup):",
456
+ validate: (v) => v.trim().length > 0 || "Required"
457
+ });
458
+ target = target.replace(/^~(?=\/|$)/, homedir3());
459
+ }
460
+ if (storage === "github") {
461
+ console.log('\nYou need a GitHub Personal Access Token with "repo" scope.');
462
+ console.log("Create one at: https://github.com/settings/tokens/new?scopes=repo\n");
463
+ githubToken = await password3({
464
+ message: "Paste your GitHub PAT (ghp_...):",
465
+ mask: "*",
466
+ validate: (v) => v.trim().startsWith("gh") || "Token should start with gh"
467
+ });
468
+ process.stdout.write("Validating token\u2026 ");
469
+ githubOwner = await fetchGitHubUser(githubToken.trim());
470
+ console.log(`\u2713 Authenticated as ${githubOwner}`);
471
+ githubRepo = await input({
472
+ message: "Repo name for the backup:",
473
+ default: "cortex-backup",
474
+ validate: (v) => /^[a-zA-Z0-9_.-]+$/.test(v.trim()) || "Invalid repo name"
475
+ });
476
+ process.stdout.write(`Creating private repo ${githubOwner}/${githubRepo}\u2026 `);
477
+ await ensureGitHubRepo(githubToken.trim(), githubRepo.trim());
478
+ console.log("\u2713 Ready");
479
+ githubToken = githubToken.trim();
480
+ githubRepo = githubRepo.trim();
481
+ }
482
+ await password3({
483
+ message: "Encryption passphrase (min 12 chars, never stored \u2014 keep it safe):",
484
+ mask: "*",
485
+ validate: (v) => v.length >= 12 || "Minimum 12 characters"
486
+ });
487
+ const detected = await detectInstalledTools();
488
+ console.log(`
489
+ Detected tools: ${detected.length ? detected.join(", ") : "none"}`);
490
+ const proceed = await confirm2({ message: "Save configuration?", default: true });
491
+ if (!proceed) {
492
+ console.log("Aborted.");
493
+ return;
494
+ }
495
+ await mkdir3(CORTEX_DIR2, { recursive: true });
496
+ const config = {
497
+ version: 1,
498
+ storage,
499
+ email,
500
+ target,
501
+ githubToken,
502
+ githubOwner,
503
+ githubRepo,
504
+ tools: detected,
505
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
506
+ };
507
+ await writeFile4(CONFIG_PATH2, JSON.stringify(config, null, 2), { mode: 384 });
508
+ await chmod2(CONFIG_PATH2, 384);
509
+ console.log(`
510
+ \u2713 Configuration saved to ${CONFIG_PATH2}`);
511
+ if (storage === "github") {
512
+ console.log(`Next step: run "cortex sync" \u2014 files will be encrypted and pushed to ${githubOwner}/${githubRepo}.`);
513
+ } else if (storage === "local") {
514
+ console.log('Next step: run "cortex sync" to encrypt and upload your files to the local folder.');
515
+ } else {
516
+ console.log('Next step: Google Drive backend is not yet implemented \u2014 use --target <path> with "cortex sync" for now.');
517
+ }
518
+ }
519
+
520
+ // src/commands/pull.ts
521
+ import { input as input2 } from "@inquirer/prompts";
522
+ import { existsSync as existsSync5 } from "fs";
523
+ import { isAbsolute } from "path";
524
+
525
+ // src/adapters/claude-code.ts
526
+ import { readdir, readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4, stat } from "fs/promises";
527
+ import { dirname as dirname2, join as join7, relative, resolve as resolve2, sep } from "path";
528
+ var ClaudeCodeAdapter = class {
529
+ tool = "claude-code";
530
+ root;
531
+ constructor(root) {
532
+ this.root = root ?? resolveToolPath("claude-code");
533
+ }
534
+ async *getFiles() {
535
+ yield* this.walk(this.root);
536
+ }
537
+ async putFiles(files) {
538
+ const resolvedRoot = resolve2(this.root);
539
+ for await (const f of files) {
540
+ const dest = resolve2(join7(resolvedRoot, f.relativePath));
541
+ if (dest !== resolvedRoot && !dest.startsWith(resolvedRoot + sep)) {
542
+ throw new Error(`Path traversal attempt blocked: ${f.relativePath}`);
543
+ }
544
+ await mkdir4(dirname2(dest), { recursive: true });
545
+ await writeFile5(dest, f.content);
546
+ }
547
+ }
548
+ // Encoded names are lossy (see Fase 0 findings) — callers must resolve the
549
+ // canonical path from the JSONL `cwd` field, not from this name.
550
+ getProjectPath(encodedProjectName) {
551
+ return join7(this.root, "projects", encodedProjectName);
552
+ }
553
+ async *walk(dir) {
554
+ const entries = await readdir(dir, { withFileTypes: true });
555
+ for (const e of entries) {
556
+ const full = join7(dir, e.name);
557
+ if (e.isDirectory()) {
558
+ yield* this.walk(full);
559
+ } else if (e.isFile()) {
560
+ const content = await readFile6(full);
561
+ const relativePath = relative(this.root, full).split(sep).join("/");
562
+ const s = await stat(full);
563
+ yield { relativePath, content, mode: s.mode };
564
+ }
565
+ }
566
+ }
567
+ };
568
+
569
+ // src/storage/local.ts
570
+ import { access as access2, mkdir as mkdir5, readdir as readdir2, readFile as readFile7, stat as stat2, unlink, writeFile as writeFile6 } from "fs/promises";
571
+ import { dirname as dirname3, join as join8, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
572
+ var LocalFilesystemBackend = class {
573
+ constructor(root) {
574
+ this.root = root;
575
+ this.resolvedRoot = resolve3(root);
576
+ }
577
+ root;
578
+ name = "local";
579
+ resolvedRoot;
580
+ /** Resolve and validate that `path` stays within root — throws on traversal. */
581
+ safePath(path) {
582
+ const resolved = resolve3(join8(this.resolvedRoot, path));
583
+ if (resolved !== this.resolvedRoot && !resolved.startsWith(this.resolvedRoot + sep2)) {
584
+ throw new Error(`Path traversal attempt blocked: ${path}`);
585
+ }
586
+ return resolved;
587
+ }
588
+ async list() {
589
+ const out = [];
590
+ await this.walk(this.resolvedRoot, out);
591
+ return out;
592
+ }
593
+ async has(path) {
594
+ const safe = this.safePath(path);
595
+ try {
596
+ await access2(safe);
597
+ return true;
598
+ } catch {
599
+ return false;
600
+ }
601
+ }
602
+ async read(path) {
603
+ return readFile7(this.safePath(path));
604
+ }
605
+ async write(path, content) {
606
+ const dest = this.safePath(path);
607
+ await mkdir5(dirname3(dest), { recursive: true });
608
+ await writeFile6(dest, content);
609
+ }
610
+ async remove(path) {
611
+ try {
612
+ await unlink(this.safePath(path));
613
+ } catch (e) {
614
+ if (e.code !== "ENOENT") throw e;
615
+ }
616
+ }
617
+ async walk(dir, out) {
618
+ let entries;
619
+ try {
620
+ entries = await readdir2(dir, { withFileTypes: true });
621
+ } catch (e) {
622
+ if (e.code === "ENOENT") return;
623
+ throw e;
624
+ }
625
+ for (const e of entries) {
626
+ const full = join8(dir, e.name);
627
+ if (e.isDirectory()) {
628
+ await this.walk(full, out);
629
+ } else if (e.isFile()) {
630
+ const s = await stat2(full);
631
+ const path = relative2(this.resolvedRoot, full).split(sep2).join("/");
632
+ out.push({ path, size: s.size, mtime: s.mtimeMs });
633
+ }
634
+ }
635
+ }
636
+ };
637
+
638
+ // src/lib/backend-resolver.ts
639
+ function resolveBackend(config, override) {
640
+ if (override?.target) {
641
+ return new LocalFilesystemBackend(override.target);
642
+ }
643
+ if (config.storage === "local") {
644
+ if (!config.target) throw new Error('Local storage requires a target path. Run "cortex init" again.');
645
+ return new LocalFilesystemBackend(config.target);
646
+ }
647
+ if (config.storage === "github") {
648
+ if (!config.githubToken || !config.githubOwner || !config.githubRepo) {
649
+ throw new Error('GitHub storage is not fully configured. Run "cortex init" again.');
650
+ }
651
+ return new GitHubBackend(config.githubToken, config.githubOwner, config.githubRepo);
652
+ }
653
+ throw new Error(
654
+ `Storage backend "${config.storage}" is not yet implemented. Pass --target <path> to use a local folder for now.`
655
+ );
656
+ }
657
+
658
+ // src/lib/manifest.ts
659
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
660
+ import { dirname as dirname4 } from "path";
661
+ function emptyManifest(tool) {
662
+ return { version: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), tool, files: {} };
663
+ }
664
+ function diffManifests(local, remote) {
665
+ const diff = { added: [], modified: [], removed: [], unchanged: [] };
666
+ const remotePaths = new Set(Object.keys(remote.files));
667
+ for (const [path, entry] of Object.entries(local.files)) {
668
+ const remoteEntry = remote.files[path];
669
+ if (!remoteEntry) diff.added.push(path);
670
+ else if (remoteEntry.checksum !== entry.checksum) diff.modified.push(path);
671
+ else diff.unchanged.push(path);
672
+ remotePaths.delete(path);
673
+ }
674
+ diff.removed = [...remotePaths];
675
+ return diff;
676
+ }
677
+ async function loadManifest(path) {
678
+ try {
679
+ const buf = await readFile8(path);
680
+ return JSON.parse(buf.toString("utf-8"));
681
+ } catch (e) {
682
+ if (e.code === "ENOENT") return null;
683
+ throw e;
684
+ }
685
+ }
686
+ async function saveManifest(path, manifest) {
687
+ await mkdir6(dirname4(path), { recursive: true });
688
+ await writeFile7(path, JSON.stringify(manifest, null, 2));
689
+ }
690
+
691
+ // src/lib/path-encoder.ts
692
+ function encodeProjectPath(absolutePath) {
693
+ return absolutePath.replace(/[^a-zA-Z0-9-]/g, "-");
694
+ }
695
+
696
+ // src/lib/path-mappings.ts
697
+ import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
698
+ import { existsSync as existsSync4 } from "fs";
699
+ import { dirname as dirname5 } from "path";
700
+ import { join as join9 } from "path";
701
+ var MAPPINGS_PATH = join9(CORTEX_DIR, "path-mappings.json");
702
+ async function loadMappings() {
703
+ if (!existsSync4(MAPPINGS_PATH)) return {};
704
+ try {
705
+ return JSON.parse(await readFile9(MAPPINGS_PATH, "utf-8"));
706
+ } catch {
707
+ return {};
708
+ }
709
+ }
710
+ async function saveMappings(mappings) {
711
+ await mkdir7(dirname5(MAPPINGS_PATH), { recursive: true });
712
+ await writeFile8(MAPPINGS_PATH, JSON.stringify(mappings, null, 2), "utf-8");
713
+ }
714
+
715
+ // src/lib/jsonl-remapper.ts
716
+ function remapPath(value, oldPrefix, newPrefix) {
717
+ if (typeof value === "string" && value.startsWith(oldPrefix)) {
718
+ return newPrefix + value.slice(oldPrefix.length);
719
+ }
720
+ return value;
721
+ }
722
+ function remapLine(parsed, oldPath, newPath) {
723
+ if ("cwd" in parsed) {
724
+ parsed["cwd"] = remapPath(parsed["cwd"], oldPath, newPath);
725
+ }
726
+ const tur = parsed["toolUseResult"];
727
+ if (tur !== null && typeof tur === "object") {
728
+ const r = tur;
729
+ if ("filePath" in r) r["filePath"] = remapPath(r["filePath"], oldPath, newPath);
730
+ const f = r["file"];
731
+ if (f !== null && typeof f === "object") {
732
+ const ff = f;
733
+ if ("filePath" in ff) ff["filePath"] = remapPath(ff["filePath"], oldPath, newPath);
734
+ }
735
+ }
736
+ const msg = parsed["message"];
737
+ if (msg !== null && typeof msg === "object") {
738
+ const content = msg["content"];
739
+ if (Array.isArray(content)) {
740
+ for (const item of content) {
741
+ const input3 = item["input"];
742
+ if (input3 !== null && typeof input3 === "object") {
743
+ const inp = input3;
744
+ if ("file_path" in inp) {
745
+ inp["file_path"] = remapPath(inp["file_path"], oldPath, newPath);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
751
+ }
752
+ function remapJsonlBuffer(input3, oldPath, newPath) {
753
+ if (oldPath === newPath) return input3;
754
+ const parts = [];
755
+ let lineStart = 0;
756
+ for (let i = 0; i <= input3.length; i++) {
757
+ const atEnd = i === input3.length;
758
+ const byte = atEnd ? void 0 : input3[i];
759
+ const isLF = byte === 10;
760
+ if (!isLF && !atEnd) continue;
761
+ const lineEnd = i;
762
+ let contentEnd = lineEnd;
763
+ if (contentEnd > lineStart && input3[contentEnd - 1] === 13) {
764
+ contentEnd--;
765
+ }
766
+ const lineBytes = input3.slice(lineStart, contentEnd);
767
+ const eolBytes = input3.slice(contentEnd, isLF ? lineEnd + 1 : lineEnd);
768
+ const lineStr = lineBytes.toString("utf-8").trim();
769
+ if (lineStr.length > 0) {
770
+ try {
771
+ const parsed = JSON.parse(lineStr);
772
+ remapLine(parsed, oldPath, newPath);
773
+ parts.push(Buffer.from(JSON.stringify(parsed), "utf-8"));
774
+ } catch {
775
+ parts.push(lineBytes);
776
+ }
777
+ } else {
778
+ parts.push(lineBytes);
779
+ }
780
+ parts.push(eolBytes);
781
+ lineStart = i + 1;
782
+ }
783
+ return Buffer.concat(parts);
784
+ }
785
+ function extractCwdFromJsonl(input3) {
786
+ const text = input3.toString("utf-8");
787
+ for (const line of text.split("\n")) {
788
+ const trimmed = line.trim();
789
+ if (!trimmed) continue;
790
+ try {
791
+ const parsed = JSON.parse(trimmed);
792
+ if (typeof parsed["cwd"] === "string" && parsed["cwd"].length > 0) {
793
+ return parsed["cwd"];
794
+ }
795
+ } catch {
796
+ continue;
797
+ }
798
+ }
799
+ return null;
800
+ }
801
+
802
+ // src/commands/pull.ts
803
+ async function resolveLocalPath(projectId, originalPath, mappings) {
804
+ const key = projectId ?? originalPath;
805
+ if (mappings[key]) return mappings[key];
806
+ if (projectId && mappings[originalPath]) return mappings[originalPath];
807
+ if (existsSync5(originalPath)) return originalPath;
808
+ console.log(`
809
+ Project not found on this machine:`);
810
+ console.log(` Original path: ${originalPath}`);
811
+ if (projectId) console.log(` Project ID: ${projectId}`);
812
+ const answer = await input2({
813
+ message: "Local path (leave empty to skip this project):"
814
+ });
815
+ if (!answer.trim()) return null;
816
+ return answer.trim();
817
+ }
818
+ async function pullCommand(opts = {}) {
819
+ const config = await loadConfig();
820
+ const passphrase = await readPassphrase();
821
+ const derived = deriveKey(passphrase, config.email);
822
+ const adapter = new ClaudeCodeAdapter();
823
+ const backend = resolveBackend(config, { target: opts.target });
824
+ console.log(`Pull source: ${backend.name}${opts.target ? ` (${opts.target})` : ""}
825
+ `);
826
+ if (!await backend.has("manifest.json.enc")) {
827
+ throw new Error('Remote has no manifest. Run "cortex sync" on another machine first.');
828
+ }
829
+ const enc = await backend.read("manifest.json.enc");
830
+ const remote = JSON.parse(decrypt(enc, derived).toString("utf-8"));
831
+ const local = await loadManifest(MANIFEST_PATH) ?? emptyManifest("claude-code");
832
+ const diff = diffManifests(remote, local);
833
+ const toPull = [...diff.added, ...diff.modified];
834
+ console.log(
835
+ `Diff \u2014 to download: ${toPull.length} (new: ${diff.added.length}, changed: ${diff.modified.length}), to remove locally: ${diff.removed.length}`
836
+ );
837
+ const mappings = await loadMappings();
838
+ const dirRemap = /* @__PURE__ */ new Map();
839
+ if (remote.projects) {
840
+ let mappingsDirty = false;
841
+ for (const [encodedDir, meta] of Object.entries(remote.projects)) {
842
+ if (!isAbsolute(meta.originalPath)) {
843
+ console.warn(`Skipping project "${encodedDir}": originalPath is not absolute.`);
844
+ dirRemap.set(encodedDir, null);
845
+ continue;
846
+ }
847
+ const localPath = await resolveLocalPath(meta.projectId, meta.originalPath, mappings);
848
+ if (localPath === null) {
849
+ dirRemap.set(encodedDir, null);
850
+ continue;
851
+ }
852
+ const key = meta.projectId ?? meta.originalPath;
853
+ if (mappings[key] !== localPath) {
854
+ mappings[key] = localPath;
855
+ mappingsDirty = true;
856
+ }
857
+ const newEncoded = encodeProjectPath(localPath);
858
+ dirRemap.set(encodedDir, newEncoded);
859
+ }
860
+ if (mappingsDirty) await saveMappings(mappings);
861
+ }
862
+ async function* gen() {
863
+ for (const path of toPull) {
864
+ const blob = await backend.read("files/" + path);
865
+ const content = decrypt(blob, derived);
866
+ const expected = remote.files[path].checksum;
867
+ const actual = checksumSha256(content);
868
+ if (expected !== actual) {
869
+ throw new Error(`Checksum mismatch on ${path}: expected ${expected}, got ${actual}`);
870
+ }
871
+ const m = path.match(/^(projects\/([^/]+))\/(.*\.jsonl)$/);
872
+ if (m && dirRemap.has(m[2])) {
873
+ const oldEncodedDir = m[2];
874
+ const newEncodedDir = dirRemap.get(oldEncodedDir);
875
+ if (newEncodedDir === null) continue;
876
+ const originalPath = remote.projects?.[oldEncodedDir]?.originalPath;
877
+ let remappedContent = content;
878
+ let relativePath = path;
879
+ if (originalPath) {
880
+ const localPath = mappings[remote.projects[oldEncodedDir].projectId ?? originalPath] ?? originalPath;
881
+ remappedContent = remapJsonlBuffer(content, originalPath, localPath);
882
+ }
883
+ if (newEncodedDir !== oldEncodedDir) {
884
+ relativePath = `projects/${newEncodedDir}/${m[3]}`;
885
+ }
886
+ yield { relativePath, content: remappedContent };
887
+ } else {
888
+ yield { relativePath: path, content };
889
+ }
890
+ }
891
+ }
892
+ await adapter.putFiles(gen());
893
+ await saveManifest(MANIFEST_PATH, remote);
894
+ console.log(`
895
+ \u2713 Pull complete \u2014 ${toPull.length} files restored.`);
896
+ }
897
+
898
+ // src/commands/status.ts
899
+ async function statusCommand(opts = {}) {
900
+ const config = await loadConfig();
901
+ const passphrase = await readPassphrase();
902
+ const derived = deriveKey(passphrase, config.email);
903
+ const adapter = new ClaudeCodeAdapter();
904
+ const backend = resolveBackend(config, { target: opts.target });
905
+ console.log(`Backend: ${backend.name}
906
+ `);
907
+ const local = emptyManifest("claude-code");
908
+ for await (const f of adapter.getFiles()) {
909
+ local.files[f.relativePath] = {
910
+ checksum: checksumSha256(f.content),
911
+ size: f.content.length,
912
+ encryptedSize: 0
913
+ };
914
+ }
915
+ let remote = emptyManifest("claude-code");
916
+ if (await backend.has("manifest.json.enc")) {
917
+ const enc = await backend.read("manifest.json.enc");
918
+ remote = JSON.parse(decrypt(enc, derived).toString("utf-8"));
919
+ } else {
920
+ console.log("(remote has no manifest yet)");
921
+ }
922
+ const diff = diffManifests(local, remote);
923
+ console.log(` added locally: ${diff.added.length}`);
924
+ console.log(` modified locally: ${diff.modified.length}`);
925
+ console.log(` only on remote: ${diff.removed.length}`);
926
+ console.log(` unchanged: ${diff.unchanged.length}`);
927
+ if (remote.generatedAt) console.log(`
928
+ Last remote sync: ${remote.generatedAt}`);
929
+ }
930
+
931
+ // src/lib/secrets-detector.ts
932
+ var PATTERNS = [
933
+ { name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/g },
934
+ { name: "GitHub Token", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
935
+ { name: "OpenAI Key", regex: /sk-(proj-)?[A-Za-z0-9_-]{40,}/g },
936
+ { name: "Anthropic Key", regex: /sk-ant-[A-Za-z0-9_-]{40,}/g },
937
+ { name: "Stripe Live Key", regex: /sk_live_[A-Za-z0-9]{24,}/g },
938
+ { name: "Google API Key", regex: /AIza[A-Za-z0-9_-]{35}/g },
939
+ { name: "Slack Token", regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
940
+ { name: "Private Key", regex: /-----BEGIN[ A-Z]*PRIVATE KEY-----/g }
941
+ ];
942
+ function detectSecrets(content) {
943
+ const text = typeof content === "string" ? content : content.toString("utf-8");
944
+ const matches = [];
945
+ for (const p of PATTERNS) {
946
+ const regex = new RegExp(p.regex.source, p.regex.flags);
947
+ let m;
948
+ while ((m = regex.exec(text)) !== null) {
949
+ matches.push({ pattern: p.name, preview: m[0].slice(0, 8) + "\u2026" });
950
+ }
951
+ }
952
+ return matches;
953
+ }
954
+
955
+ // src/lib/project-identifier.ts
956
+ import { execSync } from "child_process";
957
+ import { existsSync as existsSync6, readFileSync } from "fs";
958
+ import { join as join10 } from "path";
959
+ function runGit(cwd, args) {
960
+ try {
961
+ return execSync(`git ${args}`, {
962
+ cwd,
963
+ stdio: ["ignore", "pipe", "ignore"],
964
+ encoding: "utf-8",
965
+ timeout: 5e3
966
+ // 5 s max — avoids hanging on unreachable remotes
967
+ }).trim();
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+ function identifyProject(dir) {
973
+ const override = join10(dir, "cortex.json");
974
+ if (existsSync6(override)) {
975
+ try {
976
+ const data = JSON.parse(readFileSync(override, "utf-8"));
977
+ if (typeof data.projectId === "string" && data.projectId.length > 0) {
978
+ return { projectId: data.projectId.trim(), method: "cortex-json" };
979
+ }
980
+ } catch {
981
+ }
982
+ }
983
+ const remoteUrl = runGit(dir, "config --get remote.origin.url");
984
+ if (remoteUrl) {
985
+ const normalized = remoteUrl.replace(/\.git$/i, "").toLowerCase().trim();
986
+ return { projectId: normalized, method: "git-remote" };
987
+ }
988
+ const firstCommit = runGit(dir, "rev-list --max-parents=0 HEAD");
989
+ if (firstCommit) {
990
+ return { projectId: `git:${firstCommit}`, method: "first-commit" };
991
+ }
992
+ return null;
993
+ }
994
+
995
+ // src/commands/sync.ts
996
+ async function syncCommand(opts = {}) {
997
+ const config = await loadConfig();
998
+ const passphrase = await readPassphrase();
999
+ const derived = deriveKey(passphrase, config.email);
1000
+ const adapter = new ClaudeCodeAdapter();
1001
+ const backend = resolveBackend(config, { target: opts.target });
1002
+ console.log(`Sync target: ${backend.name}${opts.target ? ` (${opts.target})` : ""}
1003
+ `);
1004
+ console.log("Reading local files\u2026");
1005
+ const local = emptyManifest("claude-code");
1006
+ const contents = /* @__PURE__ */ new Map();
1007
+ for await (const f of adapter.getFiles()) {
1008
+ contents.set(f.relativePath, f.content);
1009
+ local.files[f.relativePath] = {
1010
+ checksum: checksumSha256(f.content),
1011
+ size: f.content.length,
1012
+ encryptedSize: 0
1013
+ };
1014
+ }
1015
+ console.log(` ${contents.size} files`);
1016
+ local.projects = {};
1017
+ const seenDirs = /* @__PURE__ */ new Set();
1018
+ for (const [path, content] of contents) {
1019
+ const m = path.match(/^projects\/([^/]+)\/[^/]+\.jsonl$/);
1020
+ if (!m || seenDirs.has(m[1])) continue;
1021
+ const encodedDir = m[1];
1022
+ seenDirs.add(encodedDir);
1023
+ const cwd = extractCwdFromJsonl(content);
1024
+ if (!cwd) continue;
1025
+ const info = identifyProject(cwd);
1026
+ local.projects[encodedDir] = { projectId: info?.projectId ?? null, originalPath: cwd };
1027
+ }
1028
+ if (!opts.skipSecretsCheck) {
1029
+ const findings = [];
1030
+ for (const [path, content] of contents) {
1031
+ for (const s of detectSecrets(content)) findings.push({ file: path, ...s });
1032
+ }
1033
+ if (findings.length) {
1034
+ console.warn(`
1035
+ \u26A0 Found ${findings.length} potential secrets:`);
1036
+ for (const f of findings.slice(0, 10)) {
1037
+ console.warn(` - ${f.file}: ${f.pattern} (${f.preview})`);
1038
+ }
1039
+ if (findings.length > 10) console.warn(` \u2026and ${findings.length - 10} more`);
1040
+ console.warn("Files are encrypted before upload, but consider removing real secrets.");
1041
+ console.warn("Use --skip-secrets-check to bypass.\n");
1042
+ }
1043
+ }
1044
+ console.log("Loading remote manifest\u2026");
1045
+ let remote = emptyManifest("claude-code");
1046
+ if (await backend.has("manifest.json.enc")) {
1047
+ const enc = await backend.read("manifest.json.enc");
1048
+ remote = JSON.parse(decrypt(enc, derived).toString("utf-8"));
1049
+ }
1050
+ const diff = diffManifests(local, remote);
1051
+ console.log(
1052
+ `Diff \u2014 added: ${diff.added.length}, modified: ${diff.modified.length}, removed: ${diff.removed.length}, unchanged: ${diff.unchanged.length}`
1053
+ );
1054
+ for (const path of [...diff.added, ...diff.modified]) {
1055
+ const content = contents.get(path);
1056
+ const enc = encrypt(content, derived);
1057
+ local.files[path].encryptedSize = enc.length;
1058
+ await backend.write("files/" + path, enc);
1059
+ }
1060
+ for (const path of diff.removed) {
1061
+ await backend.remove("files/" + path);
1062
+ }
1063
+ for (const path of diff.unchanged) {
1064
+ local.files[path].encryptedSize = remote.files[path].encryptedSize;
1065
+ }
1066
+ const encryptedManifest = encrypt(Buffer.from(JSON.stringify(local), "utf-8"), derived);
1067
+ await backend.write("manifest.json.enc", encryptedManifest);
1068
+ await saveManifest(MANIFEST_PATH, local);
1069
+ console.log(
1070
+ `
1071
+ \u2713 Sync complete \u2014 ${diff.added.length + diff.modified.length} uploaded, ${diff.removed.length} deleted.`
1072
+ );
1073
+ }
1074
+
1075
+ // src/cli.ts
1076
+ var program = new Command();
1077
+ program.name("cortex").description("Sync Claude Code context between machines with path remapping").version("0.0.1");
1078
+ program.command("init").description("Configure Cortex: pick storage, set passphrase, detect tools").action(initCommand);
1079
+ 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);
1081
+ 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
+ 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
+ const validTargets = ["antigravity", "cursor", "all"];
1084
+ if (!validTargets.includes(opts.to)) {
1085
+ console.error(`Invalid target "${opts.to}". Use: antigravity, cursor, or all`);
1086
+ process.exit(1);
1087
+ }
1088
+ return convertCommand(skillFile, { to: opts.to, outputDir: opts.outputDir });
1089
+ });
1090
+ await program.parseAsync(process.argv);