@piut/cli 1.0.1 → 1.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.
Files changed (3) hide show
  1. package/README.md +65 -32
  2. package/dist/cli.js +1163 -1
  3. package/package.json +8 -3
package/README.md CHANGED
@@ -1,16 +1,23 @@
1
- # pıut
1
+ # pıut
2
2
 
3
- **Give every AI tool persistent memory about you.**
3
+ **Your AI context, everywhere. Your config files, backed up.**
4
4
 
5
- pıut is a personal context service that works via [MCP (Model Context Protocol)](https://modelcontextprotocol.io). Connect once, and every AI tool you use — Claude, ChatGPT, Cursor, Copilot, and more — knows who you are, what you're working on, and how you like to work.
5
+ pıut does three things that build on each other:
6
+
7
+ 1. **Host your brain** — Centralized personal context as an MCP server. Connect once, and every AI tool you use knows who you are, how you work, and what matters to you.
8
+ 2. **Keep it updated** — 6 MCP tools let your AI read, write, search, and organize your context automatically. Add a skill reference and it happens without you lifting a finger.
9
+ 3. **Back up your files** — Cloud backup of all your agent config files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) with version history, restore, and cross-machine sync.
6
10
 
7
11
  ## Quick Start
8
12
 
9
13
  ```bash
10
- npx piut
14
+ npx @piut/cli
11
15
  ```
12
16
 
13
- That's it. The CLI auto-detects your AI tools and configures them.
17
+ The CLI does everything in one interactive flow:
18
+ - Configures your AI tools with your MCP server
19
+ - Adds skill.md references to your rules files
20
+ - Scans and backs up your agent config files to the cloud
14
21
 
15
22
  Or set up manually:
16
23
 
@@ -39,28 +46,37 @@ See [piut.com/docs](https://piut.com/docs#add-to-ai) for setup guides for 14+ AI
39
46
  Install globally or run with `npx`:
40
47
 
41
48
  ```bash
42
- npx piut # Auto-detect and configure AI tools
43
- npx piut status # Show which tools are connected
44
- npx piut remove # Remove pıut from selected tools
49
+ # Setup & Configuration
50
+ npx @piut/cli # Interactive setup: MCP + skill.md + cloud backup
51
+ npx @piut/cli status # Show which tools are connected
52
+ npx @piut/cli remove # Remove pıut from selected tools
53
+
54
+ # Cloud Backup
55
+ npx @piut/cli sync # Show backup status for current workspace
56
+ npx @piut/cli sync --install # Scan workspace, detect files, upload to cloud
57
+ npx @piut/cli sync --push # Push local changes to cloud
58
+ npx @piut/cli sync --pull # Pull cloud changes to local files
59
+ npx @piut/cli sync --history # Show version history for a file
60
+ npx @piut/cli sync --diff # Show diff between local and cloud
61
+ npx @piut/cli sync --restore # Restore files from cloud backup
45
62
  ```
46
63
 
47
64
  **Options:**
48
65
 
49
66
  ```bash
50
- npx piut --key pb_... # Pass API key non-interactively
51
- npx piut --tool cursor # Configure a single tool
52
- npx piut --skip-skill # Skip skill.md file placement
67
+ npx @piut/cli --key pb_... # Pass API key non-interactively
68
+ npx @piut/cli --tool cursor # Configure a single tool
69
+ npx @piut/cli --skip-skill # Skip skill.md file placement
70
+ npx @piut/cli sync --install --yes # Non-interactive backup setup
53
71
  ```
54
72
 
55
73
  **Supported tools:** Claude Code, Claude Desktop, Cursor, Windsurf, GitHub Copilot, Amazon Q, Zed
56
74
 
57
- ## How It Works
75
+ ## The Three Features
58
76
 
59
- 1. **Build your context** Answer 5 questions or import existing files from your AI tools
60
- 2. **Connect your tools** — Run `npx piut` or add one config, and every connected AI knows your context
61
- 3. **Stay in sync** — Update your context once, and it's reflected everywhere
77
+ ### 1. Host Your Brain (MCP Server)
62
78
 
63
- Your context is organized into 5 sections:
79
+ Your brain is organized into 5 sections, accessible from every AI tool via MCP:
64
80
 
65
81
  | Section | What it stores |
66
82
  |---------|---------------|
@@ -70,32 +86,49 @@ Your context is organized into 5 sections:
70
86
  | **Projects** | Active, time-bound work with goals and deadlines |
71
87
  | **Memory** | Bookmarks, links, ideas, notes, reference material |
72
88
 
73
- ## Documentation
74
-
75
- | Document | Description |
76
- |----------|-------------|
77
- | [**skill.md**](skill.md) | AI skill file — MCP tools, rate limits, error codes |
78
- | [**Add to your AI**](https://piut.com/docs#add-to-ai) | Setup guides for Claude, ChatGPT, Cursor, Copilot, and more |
79
- | [**API Reference**](https://piut.com/docs#api-examples) | Code examples in cURL, Python, Node.js, Go, and Ruby |
80
- | [**Rate Limits**](https://piut.com/docs#limits) | Limits by plan, error codes, and response headers |
81
- | [**Context Files**](https://piut.com/docs#context-files) | Where to find your existing context in 14 AI platforms |
82
-
83
- All documentation is maintained at [piut.com/docs](https://piut.com/docs) — the interactive version with credential auto-fill and setup guides.
89
+ ### 2. Keep It Updated (MCP Tools + Skill)
84
90
 
85
- ## MCP Tools
86
-
87
- pıut provides 6 tools via MCP:
91
+ 6 tools let your AI read and write your brain:
88
92
 
89
93
  | Tool | Purpose |
90
94
  |------|---------|
91
95
  | `get_context` | Read all 5 context sections |
92
96
  | `get_section` | Read a specific section |
93
97
  | `search_brain` | Search across all sections |
94
- | `add_memory` | Append content to a section |
98
+ | `append_brain` | Append content to a section |
95
99
  | `update_brain` | AI-powered smart update across sections |
96
100
  | `prompt_brain` | Natural language command (edit, delete, reorganize) |
97
101
 
98
- Full tool documentation: [skill.md](skill.md)
102
+ Add the [skill reference](skill.md) to your rules file and your AI uses these tools automatically.
103
+
104
+ ### 3. Back Up Your Files (Cloud Backup)
105
+
106
+ The CLI scans your workspace for agent config files and backs them up to the cloud:
107
+
108
+ | File | Tool |
109
+ |------|------|
110
+ | `CLAUDE.md` | Claude Code |
111
+ | `AGENTS.md` | Multi-agent |
112
+ | `.cursorrules` | Cursor |
113
+ | `.windsurfrules` | Windsurf |
114
+ | `copilot-instructions.md` | GitHub Copilot |
115
+ | `MEMORY.md` | Claude Code |
116
+ | `SOUL.md` | OpenClaw |
117
+ | `rules/*.md` | Various |
118
+
119
+ Files are encrypted at rest (AES-256-GCM), versioned, and syncable across machines.
120
+
121
+ ## Documentation
122
+
123
+ | Document | Description |
124
+ |----------|-------------|
125
+ | [**skill.md**](skill.md) | AI skill file — MCP tools, rate limits, error codes |
126
+ | [**Add to your AI**](https://piut.com/docs#add-to-ai) | Setup guides for Claude, ChatGPT, Cursor, Copilot, and more |
127
+ | [**API Reference**](https://piut.com/docs#api-examples) | Code examples in cURL, Python, Node.js, Go, and Ruby |
128
+ | [**Cloud Backup**](https://piut.com/docs#cloud-backup) | Cloud backup setup, commands, and sync workflow |
129
+ | [**Rate Limits**](https://piut.com/docs#limits) | Limits by plan, error codes, and response headers |
130
+
131
+ All documentation is maintained at [piut.com/docs](https://piut.com/docs) — the interactive version with credential auto-fill and setup guides.
99
132
 
100
133
  ## Links
101
134
 
package/dist/cli.js CHANGED
@@ -505,10 +505,1172 @@ async function removeCommand() {
505
505
  console.log();
506
506
  }
507
507
 
508
+ // src/commands/sync.ts
509
+ import fs8 from "fs";
510
+ import crypto2 from "crypto";
511
+ import { password as password2, confirm as confirm3, checkbox as checkbox3, select } from "@inquirer/prompts";
512
+ import chalk3 from "chalk";
513
+
514
+ // src/lib/scanner.ts
515
+ import fs6 from "fs";
516
+ import path6 from "path";
517
+ import os3 from "os";
518
+ var KNOWN_FILES = [
519
+ "CLAUDE.md",
520
+ ".claude/MEMORY.md",
521
+ ".claude/settings.json",
522
+ "AGENTS.md",
523
+ ".cursorrules",
524
+ ".windsurfrules",
525
+ ".github/copilot-instructions.md",
526
+ ".cursor/rules/*.md",
527
+ ".cursor/rules/*.mdc",
528
+ ".windsurf/rules/*.md",
529
+ ".claude/rules/*.md",
530
+ "CONVENTIONS.md",
531
+ ".zed/rules.md"
532
+ ];
533
+ var GLOBAL_FILES = [
534
+ "~/.claude/MEMORY.md",
535
+ "~/.claude/settings.local.json",
536
+ "~/.openclaw/workspace/SOUL.md",
537
+ "~/.openclaw/workspace/MEMORY.md",
538
+ "~/.openclaw/workspace/IDENTITY.md",
539
+ "~/.config/agent/IDENTITY.md"
540
+ ];
541
+ function detectInstalledTools() {
542
+ const installed = [];
543
+ for (const tool of TOOLS) {
544
+ const paths = resolveConfigPaths(tool.configPaths);
545
+ for (const configPath of paths) {
546
+ if (fs6.existsSync(configPath) || fs6.existsSync(path6.dirname(configPath))) {
547
+ installed.push({ name: tool.name, id: tool.id });
548
+ break;
549
+ }
550
+ }
551
+ }
552
+ return installed;
553
+ }
554
+ function categorizeFile(filePath) {
555
+ const lower = filePath.toLowerCase();
556
+ if (lower.includes(".claude/") || lower.includes("claude.md")) return "Claude Code";
557
+ if (lower.includes(".cursor/") || lower.includes(".cursorrules")) return "Cursor Rules";
558
+ if (lower.includes(".windsurf/") || lower.includes(".windsurfrules")) return "Windsurf Rules";
559
+ if (lower.includes(".github/copilot")) return "VS Code / Copilot";
560
+ if (lower.includes(".aws/amazonq")) return "Amazon Q";
561
+ if (lower.includes(".zed/")) return "Zed";
562
+ if (lower.includes(".openclaw/")) return "OpenClaw";
563
+ return "Custom";
564
+ }
565
+ function matchesGlob(filename, pattern) {
566
+ if (!pattern.includes("*")) return filename === pattern;
567
+ const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
568
+ return regex.test(filename);
569
+ }
570
+ function scanDirectory(dir, patterns) {
571
+ const found = [];
572
+ for (const pattern of patterns) {
573
+ const parts = pattern.split("/");
574
+ if (parts.length === 1) {
575
+ const filePath = path6.join(dir, pattern);
576
+ if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
577
+ found.push(filePath);
578
+ }
579
+ } else {
580
+ const lastPart = parts[parts.length - 1];
581
+ const dirPart = parts.slice(0, -1).join("/");
582
+ const fullDir = path6.join(dir, dirPart);
583
+ if (lastPart.includes("*")) {
584
+ if (fs6.existsSync(fullDir) && fs6.statSync(fullDir).isDirectory()) {
585
+ try {
586
+ const entries = fs6.readdirSync(fullDir);
587
+ for (const entry of entries) {
588
+ if (matchesGlob(entry, lastPart)) {
589
+ const fullPath = path6.join(fullDir, entry);
590
+ if (fs6.statSync(fullPath).isFile()) {
591
+ found.push(fullPath);
592
+ }
593
+ }
594
+ }
595
+ } catch {
596
+ }
597
+ }
598
+ } else {
599
+ const filePath = path6.join(dir, pattern);
600
+ if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
601
+ found.push(filePath);
602
+ }
603
+ }
604
+ }
605
+ }
606
+ return found;
607
+ }
608
+ function scanForFiles(workspaceDirs) {
609
+ const home2 = os3.homedir();
610
+ const files = [];
611
+ const seen = /* @__PURE__ */ new Set();
612
+ for (const globalPath of GLOBAL_FILES) {
613
+ const absPath = expandPath(globalPath);
614
+ if (fs6.existsSync(absPath) && fs6.statSync(absPath).isFile()) {
615
+ if (seen.has(absPath)) continue;
616
+ seen.add(absPath);
617
+ files.push({
618
+ absolutePath: absPath,
619
+ displayPath: globalPath,
620
+ sizeBytes: fs6.statSync(absPath).size,
621
+ category: "Global",
622
+ type: "global",
623
+ projectName: "Global"
624
+ });
625
+ }
626
+ }
627
+ const dirs = workspaceDirs || [process.cwd()];
628
+ for (const dir of dirs) {
629
+ const absDir = path6.resolve(dir);
630
+ if (!fs6.existsSync(absDir)) continue;
631
+ const foundPaths = scanDirectory(absDir, KNOWN_FILES);
632
+ for (const filePath of foundPaths) {
633
+ if (seen.has(filePath)) continue;
634
+ seen.add(filePath);
635
+ const relativePath = path6.relative(absDir, filePath);
636
+ const projectName = path6.basename(absDir);
637
+ files.push({
638
+ absolutePath: filePath,
639
+ displayPath: path6.relative(home2, filePath).startsWith("..") ? filePath : "~/" + path6.relative(home2, filePath),
640
+ sizeBytes: fs6.statSync(filePath).size,
641
+ category: categorizeFile(filePath),
642
+ type: "project",
643
+ projectName
644
+ });
645
+ }
646
+ }
647
+ return files;
648
+ }
649
+ function formatSize(bytes) {
650
+ if (bytes < 1024) return `${bytes} B`;
651
+ const kb = bytes / 1024;
652
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
653
+ const mb = kb / 1024;
654
+ return `${mb.toFixed(1)} MB`;
655
+ }
656
+
657
+ // src/lib/sync-api.ts
658
+ var API_BASE2 = process.env.PIUT_API_BASE || "https://piut.com";
659
+ function authHeaders(apiKey) {
660
+ return {
661
+ Authorization: `Bearer ${apiKey}`,
662
+ "Content-Type": "application/json"
663
+ };
664
+ }
665
+ async function uploadFiles(apiKey, files) {
666
+ const res = await fetch(`${API_BASE2}/api/sync/upload`, {
667
+ method: "POST",
668
+ headers: authHeaders(apiKey),
669
+ body: JSON.stringify({ files })
670
+ });
671
+ if (!res.ok) {
672
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
673
+ throw new Error(body.error || `Upload failed (HTTP ${res.status})`);
674
+ }
675
+ return res.json();
676
+ }
677
+ async function listFiles(apiKey) {
678
+ const res = await fetch(`${API_BASE2}/api/sync/files`, {
679
+ headers: authHeaders(apiKey)
680
+ });
681
+ if (!res.ok) {
682
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
683
+ throw new Error(body.error || `List failed (HTTP ${res.status})`);
684
+ }
685
+ return res.json();
686
+ }
687
+ async function pullFiles(apiKey, fileIds, deviceId) {
688
+ const res = await fetch(`${API_BASE2}/api/sync/pull`, {
689
+ method: "POST",
690
+ headers: authHeaders(apiKey),
691
+ body: JSON.stringify({ fileIds, deviceId })
692
+ });
693
+ if (!res.ok) {
694
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
695
+ throw new Error(body.error || `Pull failed (HTTP ${res.status})`);
696
+ }
697
+ return res.json();
698
+ }
699
+ async function listFileVersions(apiKey, fileId) {
700
+ const res = await fetch(`${API_BASE2}/api/sync/files/${fileId}/versions`, {
701
+ headers: authHeaders(apiKey)
702
+ });
703
+ if (!res.ok) {
704
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
705
+ throw new Error(body.error || `Version list failed (HTTP ${res.status})`);
706
+ }
707
+ return res.json();
708
+ }
709
+ async function getFileVersion(apiKey, fileId, version) {
710
+ const res = await fetch(`${API_BASE2}/api/sync/files/${fileId}/versions/${version}`, {
711
+ headers: authHeaders(apiKey)
712
+ });
713
+ if (!res.ok) {
714
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
715
+ throw new Error(body.error || `Version fetch failed (HTTP ${res.status})`);
716
+ }
717
+ return res.json();
718
+ }
719
+ async function resolveConflict(apiKey, fileId, resolution, localContent, deviceId, deviceName) {
720
+ const res = await fetch(`${API_BASE2}/api/sync/resolve`, {
721
+ method: "POST",
722
+ headers: authHeaders(apiKey),
723
+ body: JSON.stringify({ fileId, resolution, localContent, deviceId, deviceName })
724
+ });
725
+ if (!res.ok) {
726
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
727
+ throw new Error(body.error || `Resolve failed (HTTP ${res.status})`);
728
+ }
729
+ return res.json();
730
+ }
731
+
732
+ // src/lib/sync-config.ts
733
+ import fs7 from "fs";
734
+ import path7 from "path";
735
+ import os4 from "os";
736
+ import crypto from "crypto";
737
+ var CONFIG_DIR = path7.join(os4.homedir(), ".piut");
738
+ var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
739
+ function defaultDeviceName() {
740
+ return os4.hostname() || "unknown";
741
+ }
742
+ function generateDeviceId() {
743
+ return `dev_${crypto.randomBytes(8).toString("hex")}`;
744
+ }
745
+ function readSyncConfig() {
746
+ const defaults = {
747
+ deviceId: generateDeviceId(),
748
+ deviceName: defaultDeviceName(),
749
+ autoDiscover: false,
750
+ keepBrainUpdated: false,
751
+ useBrain: false,
752
+ backedUpFiles: []
753
+ };
754
+ try {
755
+ const raw = fs7.readFileSync(CONFIG_FILE, "utf-8");
756
+ const parsed = JSON.parse(raw);
757
+ return { ...defaults, ...parsed };
758
+ } catch {
759
+ return defaults;
760
+ }
761
+ }
762
+ function writeSyncConfig(config) {
763
+ fs7.mkdirSync(CONFIG_DIR, { recursive: true });
764
+ fs7.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
765
+ }
766
+ function updateSyncConfig(updates) {
767
+ const config = readSyncConfig();
768
+ const updated = { ...config, ...updates };
769
+ writeSyncConfig(updated);
770
+ return updated;
771
+ }
772
+ function getConfigFile() {
773
+ return CONFIG_FILE;
774
+ }
775
+
776
+ // src/lib/sensitive-guard.ts
777
+ var SECRET_PATTERNS = [
778
+ // API keys and tokens
779
+ /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|bearer)\s*[:=]\s*['"]?[A-Za-z0-9_\-/.]{20,}/i,
780
+ // AWS credentials
781
+ /AKIA[0-9A-Z]{16}/,
782
+ // Private keys
783
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
784
+ // Supabase service role JWTs (eyJ...)
785
+ /SUPABASE_SERVICE_ROLE_KEY\s*[:=]\s*['"]?eyJ/i,
786
+ // Generic JWT tokens assigned to variables
787
+ /(?:JWT|TOKEN|SECRET|PASSWORD|CREDENTIAL)\s*[:=]\s*['"]?eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/i,
788
+ // Connection strings
789
+ /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+:[^\s'"]+@/i,
790
+ // Stripe keys
791
+ /sk_(?:live|test)_[A-Za-z0-9]{20,}/,
792
+ // Anthropic keys
793
+ /sk-ant-[A-Za-z0-9_-]{20,}/,
794
+ // OpenAI keys
795
+ /sk-[A-Za-z0-9]{40,}/,
796
+ // npm tokens
797
+ /npm_[A-Za-z0-9]{36}/,
798
+ // GitHub tokens
799
+ /gh[pousr]_[A-Za-z0-9]{36,}/,
800
+ // Generic password assignments
801
+ /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i
802
+ ];
803
+ var BLOCKED_FILENAMES = [
804
+ ".env",
805
+ ".env.local",
806
+ ".env.production",
807
+ ".env.development",
808
+ ".env.staging",
809
+ ".env.test",
810
+ "credentials.json",
811
+ "service-account.json",
812
+ "secrets.json",
813
+ "secrets.yaml",
814
+ "secrets.yml",
815
+ ".netrc",
816
+ ".npmrc",
817
+ "id_rsa",
818
+ "id_ed25519",
819
+ "id_ecdsa"
820
+ ];
821
+ function isBlockedFilename(filePath) {
822
+ const basename = filePath.split("/").pop() || "";
823
+ return BLOCKED_FILENAMES.some(
824
+ (blocked) => basename === blocked || basename.startsWith(".env.")
825
+ );
826
+ }
827
+ function scanForSecrets(content) {
828
+ const matches = [];
829
+ const lines = content.split("\n");
830
+ for (let i = 0; i < lines.length; i++) {
831
+ const line = lines[i];
832
+ for (const pattern of SECRET_PATTERNS) {
833
+ if (pattern.test(line)) {
834
+ const preview = line.length > 80 ? line.slice(0, 77) + "..." : line;
835
+ matches.push({
836
+ line: i + 1,
837
+ preview: preview.replace(/(['"])[A-Za-z0-9_\-/.+=]{20,}(['"])/g, "$1[REDACTED]$2"),
838
+ pattern: pattern.source.slice(0, 40)
839
+ });
840
+ break;
841
+ }
842
+ }
843
+ }
844
+ return matches;
845
+ }
846
+ function guardFile(filePath, content) {
847
+ if (isBlockedFilename(filePath)) {
848
+ return { blocked: true, reason: "filename", matches: [] };
849
+ }
850
+ const matches = scanForSecrets(content);
851
+ if (matches.length > 0) {
852
+ return { blocked: true, reason: "content", matches };
853
+ }
854
+ return { blocked: false, reason: null, matches: [] };
855
+ }
856
+
857
+ // src/commands/sync.ts
858
+ async function syncCommand(options) {
859
+ if (options.install) {
860
+ await installFlow(options);
861
+ } else if (options.push) {
862
+ await pushFlow(options);
863
+ } else if (options.pull) {
864
+ await pullFlow(options);
865
+ } else if (options.watch) {
866
+ await watchFlow();
867
+ } else if (options.history) {
868
+ await historyFlow(options.history);
869
+ } else if (options.diff) {
870
+ await diffFlow(options.diff);
871
+ } else if (options.restore) {
872
+ await restoreFlow(options.restore);
873
+ } else if (options.installDaemon) {
874
+ await installDaemonFlow();
875
+ } else {
876
+ await statusFlow();
877
+ }
878
+ }
879
+ function guardAndFilter(files, options) {
880
+ const safe = [];
881
+ let blocked = 0;
882
+ for (const file of files) {
883
+ const content = fs8.readFileSync(file.absolutePath, "utf-8");
884
+ const result = guardFile(file.displayPath, content);
885
+ if (result.blocked) {
886
+ blocked++;
887
+ if (result.reason === "filename") {
888
+ console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (sensitive filename)"));
889
+ } else {
890
+ console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (contains secrets)"));
891
+ for (const match of result.matches.slice(0, 3)) {
892
+ console.log(dim(` line ${match.line}: ${match.preview}`));
893
+ }
894
+ if (result.matches.length > 3) {
895
+ console.log(dim(` ... and ${result.matches.length - 3} more`));
896
+ }
897
+ }
898
+ } else {
899
+ safe.push(file);
900
+ }
901
+ }
902
+ if (blocked > 0) {
903
+ console.log();
904
+ console.log(warning(` ${blocked} file(s) blocked by sensitive file guard`));
905
+ console.log(dim(" These files will not be uploaded to protect your secrets."));
906
+ console.log();
907
+ }
908
+ return safe;
909
+ }
910
+ async function installFlow(options) {
911
+ banner();
912
+ console.log(brand.bold(" Cloud Backup Setup"));
913
+ console.log();
914
+ const config = readSyncConfig();
915
+ let apiKey = options.key || config.apiKey;
916
+ if (!apiKey) {
917
+ console.log(dim(" Enter your API key, or press Enter to get one at piut.com/dashboard"));
918
+ console.log();
919
+ apiKey = await password2({
920
+ message: "Enter your API key (or press Enter to get one)",
921
+ mask: "*",
922
+ validate: (v) => {
923
+ if (!v) return true;
924
+ return v.startsWith("pb_") || "Key must start with pb_";
925
+ }
926
+ });
927
+ if (!apiKey) {
928
+ console.log();
929
+ console.log(` Get an API key at: ${brand("https://piut.com/dashboard")}`);
930
+ console.log(dim(" Then run: npx @piut/cli sync --install"));
931
+ console.log();
932
+ return;
933
+ }
934
+ }
935
+ console.log(dim(" Validating key..."));
936
+ let validationResult;
937
+ try {
938
+ validationResult = await validateKey(apiKey);
939
+ } catch (err) {
940
+ console.log(chalk3.red(` \u2717 ${err.message}`));
941
+ console.log(dim(" Get a key at https://piut.com/dashboard"));
942
+ process.exit(1);
943
+ }
944
+ console.log(success(` \u2713 Authenticated as ${validationResult.displayName}`));
945
+ console.log();
946
+ updateSyncConfig({ apiKey });
947
+ console.log(dim(" Scanning your AI tool environments..."));
948
+ const tools = detectInstalledTools();
949
+ if (tools.length === 0) {
950
+ console.log(warning(" No AI tools detected."));
951
+ console.log(dim(" Supported: Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, Amazon Q, Zed"));
952
+ console.log();
953
+ return;
954
+ }
955
+ console.log(success(` \u2713 Found ${tools.length} product${tools.length === 1 ? "" : "s"} installed:`));
956
+ for (const tool of tools) {
957
+ console.log(` - ${tool.name}`);
958
+ }
959
+ console.log();
960
+ if (!options.yes) {
961
+ const proceed = await confirm3({
962
+ message: `Continue with all ${tools.length}?`,
963
+ default: true
964
+ });
965
+ if (!proceed) {
966
+ console.log(dim(" Setup cancelled."));
967
+ return;
968
+ }
969
+ }
970
+ console.log();
971
+ console.log(dim(" Scanning for brain files..."));
972
+ console.log(dim(" (looking for: CLAUDE.md, MEMORY.md, SOUL.md, IDENTITY.md, .cursorrules, etc.)"));
973
+ console.log();
974
+ if (!options.yes) {
975
+ const scanPermission = await confirm3({
976
+ message: "Permission to scan your workspace?",
977
+ default: true
978
+ });
979
+ if (!scanPermission) {
980
+ console.log(dim(" Scan cancelled."));
981
+ return;
982
+ }
983
+ }
984
+ const scannedFiles = scanForFiles();
985
+ if (scannedFiles.length === 0) {
986
+ console.log(warning(" No agent config files found in the current workspace."));
987
+ console.log(dim(" Try running from a project directory with CLAUDE.md, .cursorrules, etc."));
988
+ console.log();
989
+ return;
990
+ }
991
+ const safeFiles = guardAndFilter(scannedFiles, options);
992
+ if (safeFiles.length === 0) {
993
+ console.log(warning(" All files were blocked by the sensitive file guard."));
994
+ console.log();
995
+ return;
996
+ }
997
+ console.log();
998
+ console.log(` Found ${brand.bold(String(safeFiles.length))} safe files across your workspace:`);
999
+ console.log();
1000
+ const grouped = groupByCategory(safeFiles);
1001
+ const choices = [];
1002
+ for (const [category, files] of Object.entries(grouped)) {
1003
+ console.log(dim(` \u{1F4C1} ${category}`));
1004
+ for (const file of files) {
1005
+ console.log(` \u2611 ${file.displayPath}`);
1006
+ choices.push({
1007
+ name: `${file.displayPath} ${dim(`(${formatSize(file.sizeBytes)})`)}`,
1008
+ value: file,
1009
+ checked: true
1010
+ });
1011
+ }
1012
+ console.log();
1013
+ }
1014
+ let selectedFiles;
1015
+ if (options.yes) {
1016
+ selectedFiles = safeFiles;
1017
+ } else {
1018
+ selectedFiles = await checkbox3({
1019
+ message: `Back up all ${safeFiles.length} files?`,
1020
+ choices
1021
+ });
1022
+ if (selectedFiles.length === 0) {
1023
+ console.log(dim(" No files selected."));
1024
+ return;
1025
+ }
1026
+ }
1027
+ await uploadScannedFiles(apiKey, selectedFiles);
1028
+ console.log();
1029
+ console.log(` View your backups: ${brand("https://piut.com/dashboard/backups")}`);
1030
+ console.log();
1031
+ if (!options.yes) {
1032
+ const autoBackup = await confirm3({
1033
+ message: "Configure auto-backup?",
1034
+ default: false
1035
+ });
1036
+ if (autoBackup) {
1037
+ updateSyncConfig({ autoDiscover: true });
1038
+ console.log(success(" \u2713 Auto-backup enabled."));
1039
+ console.log(dim(" New files in the same environments will be backed up automatically."));
1040
+ console.log(dim(" Configure: piut sync config"));
1041
+ }
1042
+ }
1043
+ console.log();
1044
+ }
1045
+ async function pushFlow(options) {
1046
+ banner();
1047
+ const config = readSyncConfig();
1048
+ const apiKey = options.key || config.apiKey;
1049
+ if (!apiKey) {
1050
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1051
+ process.exit(1);
1052
+ }
1053
+ console.log(dim(" Scanning for changes..."));
1054
+ const files = scanForFiles();
1055
+ if (files.length === 0) {
1056
+ console.log(dim(" No files found to push."));
1057
+ return;
1058
+ }
1059
+ const safeFiles = guardAndFilter(files, options);
1060
+ if (safeFiles.length === 0) {
1061
+ console.log(dim(" No safe files to push."));
1062
+ return;
1063
+ }
1064
+ if (!options.preferLocal && !options.preferCloud) {
1065
+ try {
1066
+ const cloudFiles = await listFiles(apiKey);
1067
+ for (const localFile of safeFiles) {
1068
+ const localContent = fs8.readFileSync(localFile.absolutePath, "utf-8");
1069
+ const localHash = hashContent(localContent);
1070
+ const cloudFile = cloudFiles.files.find(
1071
+ (cf) => cf.file_path === localFile.displayPath && cf.project_name === localFile.projectName
1072
+ );
1073
+ if (cloudFile && cloudFile.content_hash !== localHash) {
1074
+ console.log(warning(` Conflict: ${localFile.displayPath}`));
1075
+ console.log(dim(` local hash: ${localHash.slice(0, 12)}...`));
1076
+ console.log(dim(` cloud hash: ${cloudFile.content_hash.slice(0, 12)}...`));
1077
+ if (!options.yes) {
1078
+ const resolution = await select({
1079
+ message: `How to resolve ${localFile.displayPath}?`,
1080
+ choices: [
1081
+ { name: "Keep local (push local to cloud)", value: "keep-local" },
1082
+ { name: "Keep cloud (skip this file)", value: "keep-cloud" }
1083
+ ]
1084
+ });
1085
+ if (resolution === "keep-cloud") {
1086
+ await resolveConflict(apiKey, cloudFile.id, "keep-cloud", void 0, config.deviceId, config.deviceName);
1087
+ console.log(success(` \u2713 Kept cloud version of ${localFile.displayPath}`));
1088
+ const idx = safeFiles.indexOf(localFile);
1089
+ if (idx >= 0) safeFiles.splice(idx, 1);
1090
+ continue;
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ await uploadScannedFiles(apiKey, safeFiles);
1099
+ console.log();
1100
+ }
1101
+ async function pullFlow(options) {
1102
+ banner();
1103
+ const config = readSyncConfig();
1104
+ const apiKey = options.key || config.apiKey;
1105
+ if (!apiKey) {
1106
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1107
+ process.exit(1);
1108
+ }
1109
+ console.log(dim(" Pulling latest versions from cloud..."));
1110
+ try {
1111
+ const result = await pullFiles(apiKey, void 0, config.deviceId);
1112
+ if (result.files.length === 0) {
1113
+ console.log(dim(" No files to pull. Everything is up to date."));
1114
+ return;
1115
+ }
1116
+ for (const file of result.files) {
1117
+ console.log(success(` \u2713 ${file.file_path}`) + dim(` (v${file.current_version})`));
1118
+ }
1119
+ console.log();
1120
+ console.log(success(` Pulled ${result.files.length} file(s)`));
1121
+ } catch (err) {
1122
+ console.log(chalk3.red(` \u2717 Pull failed: ${err.message}`));
1123
+ process.exit(1);
1124
+ }
1125
+ console.log();
1126
+ }
1127
+ async function historyFlow(filePathOrId) {
1128
+ banner();
1129
+ console.log(brand.bold(" Version History"));
1130
+ console.log();
1131
+ const config = readSyncConfig();
1132
+ if (!config.apiKey) {
1133
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1134
+ process.exit(1);
1135
+ }
1136
+ const fileId = await resolveFileId(config.apiKey, filePathOrId);
1137
+ if (!fileId) {
1138
+ console.log(chalk3.red(` \u2717 File not found: ${filePathOrId}`));
1139
+ process.exit(1);
1140
+ }
1141
+ const result = await listFileVersions(config.apiKey, fileId);
1142
+ console.log(` ${brand.bold(result.filePath)} (${result.projectName})`);
1143
+ console.log(` Current version: v${result.currentVersion}`);
1144
+ console.log();
1145
+ for (const v of result.versions) {
1146
+ const date = new Date(v.createdAt).toLocaleString();
1147
+ const size = formatSize(v.contentSize);
1148
+ const summary = v.changeSummary ? ` \u2014 ${v.changeSummary}` : "";
1149
+ const marker = v.version === result.currentVersion ? chalk3.green(" (current)") : "";
1150
+ console.log(` v${v.version} ${dim(date)} ${dim(size)}${summary}${marker}`);
1151
+ }
1152
+ console.log();
1153
+ }
1154
+ async function diffFlow(filePathOrId) {
1155
+ banner();
1156
+ console.log(brand.bold(" Local vs Cloud Diff"));
1157
+ console.log();
1158
+ const config = readSyncConfig();
1159
+ if (!config.apiKey) {
1160
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1161
+ process.exit(1);
1162
+ }
1163
+ const fileId = await resolveFileId(config.apiKey, filePathOrId);
1164
+ if (!fileId) {
1165
+ console.log(chalk3.red(` \u2717 File not found in cloud: ${filePathOrId}`));
1166
+ process.exit(1);
1167
+ }
1168
+ const versions = await listFileVersions(config.apiKey, fileId);
1169
+ const cloudVersion = await getFileVersion(config.apiKey, fileId, versions.currentVersion);
1170
+ const cloudContent = cloudVersion.content;
1171
+ const localPath = resolveLocalPath(filePathOrId, versions.filePath);
1172
+ if (!localPath || !fs8.existsSync(localPath)) {
1173
+ console.log(warning(` Local file not found: ${filePathOrId}`));
1174
+ console.log(dim(" Showing cloud content only:"));
1175
+ console.log();
1176
+ console.log(cloudContent);
1177
+ return;
1178
+ }
1179
+ const localContent = fs8.readFileSync(localPath, "utf-8");
1180
+ if (localContent === cloudContent) {
1181
+ console.log(success(" \u2713 Local and cloud are identical"));
1182
+ console.log();
1183
+ return;
1184
+ }
1185
+ const localLines = localContent.split("\n");
1186
+ const cloudLines = cloudContent.split("\n");
1187
+ const maxLen = Math.max(localLines.length, cloudLines.length);
1188
+ console.log(` ${versions.filePath}`);
1189
+ console.log(dim(` local: ${localLines.length} lines | cloud v${versions.currentVersion}: ${cloudLines.length} lines`));
1190
+ console.log();
1191
+ let diffCount = 0;
1192
+ for (let i = 0; i < maxLen; i++) {
1193
+ const local = localLines[i];
1194
+ const cloud = cloudLines[i];
1195
+ if (local !== cloud) {
1196
+ diffCount++;
1197
+ if (diffCount > 50) {
1198
+ console.log(dim(` ... and more differences (${maxLen - i} lines remaining)`));
1199
+ break;
1200
+ }
1201
+ if (cloud !== void 0 && local !== void 0) {
1202
+ console.log(chalk3.red(` - ${i + 1}: ${cloud}`));
1203
+ console.log(chalk3.green(` + ${i + 1}: ${local}`));
1204
+ } else if (cloud === void 0) {
1205
+ console.log(chalk3.green(` + ${i + 1}: ${local}`));
1206
+ } else {
1207
+ console.log(chalk3.red(` - ${i + 1}: ${cloud}`));
1208
+ }
1209
+ }
1210
+ }
1211
+ console.log();
1212
+ console.log(dim(` ${diffCount} line(s) differ`));
1213
+ console.log();
1214
+ }
1215
+ async function restoreFlow(filePathOrId) {
1216
+ banner();
1217
+ console.log(brand.bold(" Restore from Cloud"));
1218
+ console.log();
1219
+ const config = readSyncConfig();
1220
+ if (!config.apiKey) {
1221
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1222
+ process.exit(1);
1223
+ }
1224
+ const fileId = await resolveFileId(config.apiKey, filePathOrId);
1225
+ if (!fileId) {
1226
+ console.log(chalk3.red(` \u2717 File not found in cloud: ${filePathOrId}`));
1227
+ process.exit(1);
1228
+ }
1229
+ const versionsResult = await listFileVersions(config.apiKey, fileId);
1230
+ console.log(` ${brand.bold(versionsResult.filePath)} (${versionsResult.projectName})`);
1231
+ console.log();
1232
+ if (versionsResult.versions.length <= 1) {
1233
+ console.log(dim(" Only one version available. Nothing to restore."));
1234
+ console.log();
1235
+ return;
1236
+ }
1237
+ const versionChoice = await select({
1238
+ message: "Which version to restore?",
1239
+ choices: versionsResult.versions.map((v) => ({
1240
+ name: `v${v.version} \u2014 ${new Date(v.createdAt).toLocaleString()} (${formatSize(v.contentSize)})${v.changeSummary ? ` \u2014 ${v.changeSummary}` : ""}`,
1241
+ value: v.version
1242
+ }))
1243
+ });
1244
+ const versionData = await getFileVersion(config.apiKey, fileId, versionChoice);
1245
+ const localPath = resolveLocalPath(filePathOrId, versionsResult.filePath);
1246
+ console.log();
1247
+ console.log(dim(` Restoring v${versionChoice} of ${versionsResult.filePath}...`));
1248
+ const result = await uploadFiles(config.apiKey, [{
1249
+ projectName: versionsResult.projectName,
1250
+ filePath: versionsResult.filePath,
1251
+ content: versionData.content,
1252
+ category: "project",
1253
+ deviceId: config.deviceId,
1254
+ deviceName: config.deviceName
1255
+ }]);
1256
+ if (result.uploaded > 0) {
1257
+ console.log(success(` \u2713 Cloud restored to v${versionChoice} content (saved as new version)`));
1258
+ }
1259
+ if (localPath) {
1260
+ const writeLocal = await confirm3({
1261
+ message: `Also write to local file ${localPath}?`,
1262
+ default: true
1263
+ });
1264
+ if (writeLocal) {
1265
+ fs8.writeFileSync(localPath, versionData.content, "utf-8");
1266
+ console.log(success(` \u2713 Local file updated: ${localPath}`));
1267
+ }
1268
+ }
1269
+ console.log();
1270
+ }
1271
+ async function watchFlow() {
1272
+ banner();
1273
+ console.log(brand.bold(" Live Sync (Watch Mode)"));
1274
+ console.log();
1275
+ const config = readSyncConfig();
1276
+ if (!config.apiKey) {
1277
+ console.log(chalk3.red(" \u2717 Not configured. Run: npx @piut/cli sync --install"));
1278
+ process.exit(1);
1279
+ }
1280
+ let chokidar;
1281
+ try {
1282
+ chokidar = await import("chokidar");
1283
+ } catch {
1284
+ console.log(chalk3.red(" \u2717 chokidar is required for watch mode."));
1285
+ console.log(dim(" Install it: npm install -g chokidar"));
1286
+ console.log(dim(" Or use cron-based sync: piut sync --install-daemon"));
1287
+ process.exit(1);
1288
+ }
1289
+ const files = scanForFiles();
1290
+ const safeFiles = guardAndFilter(files, { yes: true });
1291
+ if (safeFiles.length === 0) {
1292
+ console.log(dim(" No files to watch."));
1293
+ return;
1294
+ }
1295
+ const watchPaths = safeFiles.map((f) => f.absolutePath);
1296
+ console.log(dim(` Watching ${watchPaths.length} file(s) for changes...`));
1297
+ for (const f of safeFiles) {
1298
+ console.log(dim(` ${f.displayPath}`));
1299
+ }
1300
+ console.log();
1301
+ console.log(dim(" Press Ctrl+C to stop."));
1302
+ console.log();
1303
+ const debounceMap = /* @__PURE__ */ new Map();
1304
+ const DEBOUNCE_MS = 2e3;
1305
+ const watcher = chokidar.watch(watchPaths, {
1306
+ persistent: true,
1307
+ ignoreInitial: true,
1308
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
1309
+ });
1310
+ watcher.on("change", (changedPath) => {
1311
+ const existing = debounceMap.get(changedPath);
1312
+ if (existing) clearTimeout(existing);
1313
+ debounceMap.set(changedPath, setTimeout(async () => {
1314
+ debounceMap.delete(changedPath);
1315
+ const file = safeFiles.find((f) => f.absolutePath === changedPath);
1316
+ if (!file) return;
1317
+ const content = fs8.readFileSync(changedPath, "utf-8");
1318
+ const guardResult = guardFile(file.displayPath, content);
1319
+ if (guardResult.blocked) {
1320
+ console.log(chalk3.red(` BLOCKED ${file.displayPath}`) + dim(" (sensitive content detected)"));
1321
+ return;
1322
+ }
1323
+ try {
1324
+ const result = await uploadFiles(config.apiKey, [{
1325
+ projectName: file.projectName,
1326
+ filePath: file.displayPath,
1327
+ content,
1328
+ category: file.type,
1329
+ deviceId: config.deviceId,
1330
+ deviceName: config.deviceName
1331
+ }]);
1332
+ const uploaded = result.files.find((f) => f.status === "ok");
1333
+ if (uploaded) {
1334
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1335
+ console.log(success(` \u2713 ${file.displayPath}`) + dim(` v${uploaded.version} (${time})`));
1336
+ }
1337
+ } catch (err) {
1338
+ console.log(chalk3.red(` \u2717 ${file.displayPath}: ${err.message}`));
1339
+ }
1340
+ }, DEBOUNCE_MS));
1341
+ });
1342
+ await new Promise(() => {
1343
+ });
1344
+ }
1345
+ async function installDaemonFlow() {
1346
+ banner();
1347
+ console.log(brand.bold(" Auto-Sync Daemon Setup"));
1348
+ console.log();
1349
+ const platform2 = process.platform;
1350
+ if (platform2 === "darwin") {
1351
+ await installMacDaemon();
1352
+ } else if (platform2 === "linux") {
1353
+ installLinuxCron();
1354
+ } else {
1355
+ console.log(dim(" Auto-sync daemon setup is available for macOS and Linux."));
1356
+ console.log();
1357
+ console.log(dim(" Manual alternative: add this to your crontab (crontab -e):"));
1358
+ console.log();
1359
+ console.log(` */30 * * * * cd ~ && npx @piut/cli sync --push --yes 2>&1 >> ~/.piut/sync.log`);
1360
+ console.log();
1361
+ }
1362
+ }
1363
+ async function installMacDaemon() {
1364
+ const plistName = "com.piut.auto-sync";
1365
+ const plistDir = `${process.env.HOME}/Library/LaunchAgents`;
1366
+ const plistPath = `${plistDir}/${plistName}.plist`;
1367
+ const logDir = `${process.env.HOME}/.piut/logs`;
1368
+ const npxPath = await resolveNpxPath();
1369
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
1370
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1371
+ <plist version="1.0">
1372
+ <dict>
1373
+ <key>Label</key>
1374
+ <string>${plistName}</string>
1375
+ <key>ProgramArguments</key>
1376
+ <array>
1377
+ <string>${npxPath}</string>
1378
+ <string>@piut/cli</string>
1379
+ <string>sync</string>
1380
+ <string>--push</string>
1381
+ <string>--yes</string>
1382
+ </array>
1383
+ <key>StartInterval</key>
1384
+ <integer>1800</integer>
1385
+ <key>RunAtLoad</key>
1386
+ <true/>
1387
+ <key>WorkingDirectory</key>
1388
+ <string>${process.env.HOME}</string>
1389
+ <key>StandardOutPath</key>
1390
+ <string>${logDir}/sync.out.log</string>
1391
+ <key>StandardErrorPath</key>
1392
+ <string>${logDir}/sync.err.log</string>
1393
+ <key>EnvironmentVariables</key>
1394
+ <dict>
1395
+ <key>PATH</key>
1396
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.HOME}/.local/bin:${process.env.HOME}/.npm-global/bin</string>
1397
+ </dict>
1398
+ </dict>
1399
+ </plist>`;
1400
+ console.log(dim(" This will create a macOS LaunchAgent that runs every 30 minutes."));
1401
+ console.log();
1402
+ console.log(dim(` Plist: ${plistPath}`));
1403
+ console.log(dim(` Logs: ${logDir}/sync.{out,err}.log`));
1404
+ console.log();
1405
+ const proceed = await confirm3({
1406
+ message: "Install the auto-sync LaunchAgent?",
1407
+ default: true
1408
+ });
1409
+ if (!proceed) {
1410
+ console.log(dim(" Cancelled."));
1411
+ return;
1412
+ }
1413
+ fs8.mkdirSync(logDir, { recursive: true });
1414
+ fs8.mkdirSync(plistDir, { recursive: true });
1415
+ fs8.writeFileSync(plistPath, plistContent, "utf-8");
1416
+ console.log(success(` \u2713 Plist written: ${plistPath}`));
1417
+ const { execSync: execSync2 } = await import("child_process");
1418
+ try {
1419
+ try {
1420
+ execSync2(`launchctl bootout gui/$(id -u) ${plistPath} 2>/dev/null`, { stdio: "ignore" });
1421
+ } catch {
1422
+ }
1423
+ execSync2(`launchctl bootstrap gui/$(id -u) ${plistPath}`);
1424
+ console.log(success(" \u2713 LaunchAgent loaded \u2014 auto-sync active!"));
1425
+ } catch {
1426
+ console.log(warning(" LaunchAgent written but could not be loaded automatically."));
1427
+ console.log(dim(` Load manually: launchctl bootstrap gui/$(id -u) ${plistPath}`));
1428
+ }
1429
+ console.log();
1430
+ console.log(dim(" To stop: launchctl bootout gui/$(id -u) com.piut.auto-sync"));
1431
+ console.log(dim(" To check: launchctl print gui/$(id -u)/com.piut.auto-sync"));
1432
+ console.log();
1433
+ }
1434
+ function installLinuxCron() {
1435
+ console.log(dim(" Add this line to your crontab (run: crontab -e):"));
1436
+ console.log();
1437
+ console.log(` */30 * * * * cd ~ && npx @piut/cli sync --push --yes 2>&1 >> ~/.piut/sync.log`);
1438
+ console.log();
1439
+ console.log(dim(" This will push changes every 30 minutes."));
1440
+ console.log();
1441
+ }
1442
+ async function statusFlow() {
1443
+ banner();
1444
+ console.log(brand.bold(" Cloud Backup Status"));
1445
+ console.log();
1446
+ const config = readSyncConfig();
1447
+ if (!config.apiKey) {
1448
+ console.log(dim(" Not configured."));
1449
+ console.log();
1450
+ console.log(` Get started: ${brand("npx @piut/cli sync --install")}`);
1451
+ console.log();
1452
+ return;
1453
+ }
1454
+ try {
1455
+ const result = await listFiles(config.apiKey);
1456
+ console.log(` Files: ${brand.bold(String(result.fileCount))} / ${result.fileLimit}`);
1457
+ console.log(` Storage: ${brand.bold(formatSize(result.storageUsed))} / ${formatSize(result.storageLimit)}`);
1458
+ console.log(` Devices: ${result.devices.length}`);
1459
+ console.log();
1460
+ if (result.files.length > 0) {
1461
+ console.log(dim(" Backed-up files:"));
1462
+ for (const file of result.files) {
1463
+ console.log(` ${file.file_path} ${dim(`v${file.current_version}`)}`);
1464
+ }
1465
+ }
1466
+ console.log();
1467
+ console.log(dim(" Configuration:"));
1468
+ console.log(` Auto-discover: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
1469
+ console.log(` Brain sync: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
1470
+ console.log(` Use brain: ${config.useBrain ? success("ON") : dim("OFF")}`);
1471
+ console.log();
1472
+ console.log(` Dashboard: ${brand("https://piut.com/dashboard/backups")}`);
1473
+ } catch (err) {
1474
+ console.log(chalk3.red(` \u2717 ${err.message}`));
1475
+ }
1476
+ console.log();
1477
+ }
1478
+ function hashContent(content) {
1479
+ return crypto2.createHash("sha256").update(content, "utf8").digest("hex");
1480
+ }
1481
+ async function uploadScannedFiles(apiKey, files) {
1482
+ console.log();
1483
+ console.log(dim(" Backing up files..."));
1484
+ const syncConfig = readSyncConfig();
1485
+ const payloads = files.map((file) => ({
1486
+ projectName: file.projectName,
1487
+ filePath: file.displayPath,
1488
+ content: fs8.readFileSync(file.absolutePath, "utf-8"),
1489
+ category: file.type,
1490
+ deviceId: syncConfig.deviceId,
1491
+ deviceName: syncConfig.deviceName
1492
+ }));
1493
+ try {
1494
+ const result = await uploadFiles(apiKey, payloads);
1495
+ let totalSize = 0;
1496
+ for (const file of result.files) {
1497
+ const scanned = files.find(
1498
+ (s) => s.displayPath === file.filePath && s.projectName === file.projectName
1499
+ );
1500
+ const size = scanned ? scanned.sizeBytes : 0;
1501
+ totalSize += size;
1502
+ if (file.status === "ok") {
1503
+ console.log(success(` \u2713 ${file.filePath}`) + dim(` (${formatSize(size)})`));
1504
+ } else {
1505
+ console.log(chalk3.red(` \u2717 ${file.filePath}: ${file.status}`));
1506
+ }
1507
+ }
1508
+ console.log();
1509
+ if (result.uploaded > 0) {
1510
+ console.log(success(` \u2713 All files backed up successfully!`));
1511
+ console.log();
1512
+ console.log(dim(" \u{1F4CA} Backup complete:"));
1513
+ console.log(dim(` ${result.uploaded} files | ${formatSize(totalSize)} total`));
1514
+ }
1515
+ if (result.errors > 0) {
1516
+ console.log(warning(` ${result.errors} file(s) failed to upload.`));
1517
+ }
1518
+ const backedUpPaths = result.files.filter((f) => f.status === "ok").map((f) => f.filePath);
1519
+ updateSyncConfig({ backedUpFiles: backedUpPaths });
1520
+ } catch (err) {
1521
+ console.log(chalk3.red(` \u2717 Upload failed: ${err.message}`));
1522
+ process.exit(1);
1523
+ }
1524
+ }
1525
+ async function resolveFileId(apiKey, pathOrId) {
1526
+ if (/^[0-9a-f]{8}-/.test(pathOrId)) return pathOrId;
1527
+ const result = await listFiles(apiKey);
1528
+ const match = result.files.find(
1529
+ (f) => f.file_path === pathOrId || f.file_path.endsWith(pathOrId) || f.file_path.includes(pathOrId)
1530
+ );
1531
+ return match?.id || null;
1532
+ }
1533
+ function resolveLocalPath(input, cloudPath) {
1534
+ if (fs8.existsSync(input)) return input;
1535
+ const home2 = process.env.HOME || "";
1536
+ if (cloudPath.startsWith("~/")) {
1537
+ const expanded = cloudPath.replace("~", home2);
1538
+ if (fs8.existsSync(expanded)) return expanded;
1539
+ }
1540
+ const cwdPath = `${process.cwd()}/${cloudPath}`;
1541
+ if (fs8.existsSync(cwdPath)) return cwdPath;
1542
+ return null;
1543
+ }
1544
+ async function resolveNpxPath() {
1545
+ const { execSync: execSync2 } = await import("child_process");
1546
+ try {
1547
+ return execSync2("which npx", { encoding: "utf-8" }).trim();
1548
+ } catch {
1549
+ return "/usr/local/bin/npx";
1550
+ }
1551
+ }
1552
+ function groupByCategory(files) {
1553
+ const groups = {};
1554
+ for (const file of files) {
1555
+ if (!groups[file.category]) groups[file.category] = [];
1556
+ groups[file.category].push(file);
1557
+ }
1558
+ const sorted = {};
1559
+ if (groups["Global"]) {
1560
+ sorted["Global"] = groups["Global"];
1561
+ delete groups["Global"];
1562
+ }
1563
+ for (const key of Object.keys(groups).sort()) {
1564
+ sorted[key] = groups[key];
1565
+ }
1566
+ return sorted;
1567
+ }
1568
+
1569
+ // src/commands/sync-config.ts
1570
+ import chalk4 from "chalk";
1571
+ async function syncConfigCommand(options) {
1572
+ banner();
1573
+ if (options.autoDiscover !== void 0) {
1574
+ const value = parseBool(options.autoDiscover);
1575
+ if (value === null) {
1576
+ console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
1577
+ process.exit(1);
1578
+ }
1579
+ updateSyncConfig({ autoDiscover: value });
1580
+ console.log(success(` \u2713 Auto-discover: ${value ? "ON" : "OFF"}`));
1581
+ console.log();
1582
+ return;
1583
+ }
1584
+ if (options.keepBrainUpdated !== void 0) {
1585
+ const value = parseBool(options.keepBrainUpdated);
1586
+ if (value === null) {
1587
+ console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
1588
+ process.exit(1);
1589
+ }
1590
+ updateSyncConfig({ keepBrainUpdated: value });
1591
+ console.log(success(` \u2713 Keep brain updated: ${value ? "ON" : "OFF"}`));
1592
+ console.log();
1593
+ return;
1594
+ }
1595
+ if (options.useBrain !== void 0) {
1596
+ const value = parseBool(options.useBrain);
1597
+ if (value === null) {
1598
+ console.log(chalk4.red(" \u2717 Invalid value. Use: on, off, true, false"));
1599
+ process.exit(1);
1600
+ }
1601
+ updateSyncConfig({ useBrain: value });
1602
+ console.log(success(` \u2713 Use brain: ${value ? "ON" : "OFF"}`));
1603
+ console.log();
1604
+ return;
1605
+ }
1606
+ if (options.show || options.files) {
1607
+ showConfig();
1608
+ return;
1609
+ }
1610
+ showConfigMenu();
1611
+ }
1612
+ function showConfig() {
1613
+ const config = readSyncConfig();
1614
+ console.log(brand.bold(" Current Configuration"));
1615
+ console.log();
1616
+ console.log(` Config file: ${dim(getConfigFile())}`);
1617
+ console.log(` Device ID: ${dim(config.deviceId)}`);
1618
+ console.log(` Device name: ${dim(config.deviceName)}`);
1619
+ console.log(` API key: ${config.apiKey ? success("configured") : dim("not set")}`);
1620
+ console.log();
1621
+ console.log(dim(" Features:"));
1622
+ console.log(` Auto-discover: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
1623
+ console.log(` Keep brain updated: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
1624
+ console.log(` Use brain: ${config.useBrain ? success("ON") : dim("OFF")}`);
1625
+ console.log();
1626
+ if (config.backedUpFiles.length > 0) {
1627
+ console.log(dim(" Backed-up files:"));
1628
+ for (const f of config.backedUpFiles) {
1629
+ console.log(` ${f}`);
1630
+ }
1631
+ } else {
1632
+ console.log(dim(" No files backed up yet."));
1633
+ }
1634
+ console.log();
1635
+ }
1636
+ function showConfigMenu() {
1637
+ const config = readSyncConfig();
1638
+ console.log(brand.bold(" Configuration Options"));
1639
+ console.log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1640
+ console.log();
1641
+ console.log(` 1. ${chalk4.white("Change which files are backed up")}`);
1642
+ console.log(` Current: ${config.backedUpFiles.length} files selected`);
1643
+ console.log(dim(` Command: piut sync config --files`));
1644
+ console.log();
1645
+ console.log(` 2. ${chalk4.white("Auto-backup new files in same environments")}`);
1646
+ console.log(` Current: ${config.autoDiscover ? success("ON") : dim("OFF")}`);
1647
+ console.log(dim(` Command: piut sync config --auto-discover [on|off]`));
1648
+ console.log();
1649
+ console.log(` 3. ${chalk4.white("Use skill to keep brain up to date")}`);
1650
+ console.log(` Current: ${config.keepBrainUpdated ? success("ON") : dim("OFF")}`);
1651
+ console.log(dim(` Command: piut sync config --keep-brain-updated [on|off]`));
1652
+ console.log();
1653
+ console.log(` 4. ${chalk4.white("Reference centralized brain")}`);
1654
+ console.log(` Current: ${config.useBrain ? success("ON") : dim("OFF")}`);
1655
+ console.log(dim(` Command: piut sync config --use-brain [on|off]`));
1656
+ console.log();
1657
+ console.log(` 5. ${chalk4.white("View current configuration")}`);
1658
+ console.log(dim(` Command: piut sync config --show`));
1659
+ console.log();
1660
+ }
1661
+ function parseBool(value) {
1662
+ const lower = value.toLowerCase();
1663
+ if (["on", "true", "1", "yes"].includes(lower)) return true;
1664
+ if (["off", "false", "0", "no"].includes(lower)) return false;
1665
+ return null;
1666
+ }
1667
+
508
1668
  // src/cli.ts
509
1669
  var program = new Command();
510
- program.name("piut").description("Configure your AI tools to use p\u0131ut personal context").version("1.0.0");
1670
+ program.name("piut").description("Automatic backup + hosting of all your agent configs across every machine. Version history. Restore any time. One command to set up.").version("1.1.0");
511
1671
  program.command("setup", { isDefault: true }).description("Auto-detect and configure AI tools").option("-k, --key <key>", "API key (prompts interactively if not provided)").option("-t, --tool <id>", "Configure a single tool (claude-code, cursor, windsurf, etc.)").option("-y, --yes", "Skip interactive prompts (auto-select all detected tools)").option("--project", "Prefer project-local config files").option("--skip-skill", "Skip skill.md file placement").action(setupCommand);
512
1672
  program.command("status").description("Show which AI tools are configured with p\u0131ut").action(statusCommand);
513
1673
  program.command("remove").description("Remove p\u0131ut configuration from AI tools").action(removeCommand);
1674
+ var sync = program.command("sync").description("Back up and sync your agent config files to the cloud").option("--install", "Run the guided backup setup flow").option("--push", "Push local changes to cloud").option("--pull", "Pull latest versions from cloud").option("--watch", "Watch files for changes and auto-push (live sync)").option("--history <file>", "Show version history for a file").option("--diff <file>", "Show diff between local and cloud version").option("--restore <file>", "Restore a file from a previous version").option("--prefer-local", "Resolve conflicts by keeping local version").option("--prefer-cloud", "Resolve conflicts by keeping cloud version").option("--install-daemon", "Set up auto-sync via cron/launchd").option("-k, --key <key>", "API key").option("-y, --yes", "Skip interactive prompts").action(syncCommand);
1675
+ sync.command("config").description("Configure cloud backup settings").option("--files", "Change which files are backed up").option("--auto-discover <value>", "Auto-backup new files (on/off)").option("--keep-brain-updated <value>", "Keep brain updated from backups (on/off)").option("--use-brain <value>", "Reference centralized brain (on/off)").option("--show", "View current configuration").action(syncConfigCommand);
514
1676
  program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@piut/cli",
3
- "version": "1.0.1",
4
- "description": "Configure your AI tools to use pıut personal context",
3
+ "version": "1.1.0",
4
+ "description": "Automatic backup + hosting of all your agent configs across every machine. Version history. Restore any time. One command to set up.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "piut": "dist/cli.js"
@@ -25,7 +25,11 @@
25
25
  "claude",
26
26
  "cursor",
27
27
  "copilot",
28
- "windsurf"
28
+ "windsurf",
29
+ "backup",
30
+ "sync",
31
+ "cloud",
32
+ "agent-config"
29
33
  ],
30
34
  "author": "M-Flat Inc",
31
35
  "license": "MIT",
@@ -37,6 +41,7 @@
37
41
  "dependencies": {
38
42
  "@inquirer/prompts": "^7.0.0",
39
43
  "chalk": "^5.4.0",
44
+ "chokidar": "^4.0.3",
40
45
  "commander": "^13.0.0"
41
46
  },
42
47
  "devDependencies": {