@piut/cli 1.1.0 → 3.0.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 +192 -82
  2. package/dist/cli.js +834 -1072
  3. package/package.json +5 -6
package/dist/cli.js CHANGED
@@ -22,6 +22,43 @@ async function validateKey(key) {
22
22
  }
23
23
  return res.json();
24
24
  }
25
+ async function buildBrain(key, input) {
26
+ const res = await fetch(`${API_BASE}/api/cli/build-brain`, {
27
+ method: "POST",
28
+ headers: {
29
+ Authorization: `Bearer ${key}`,
30
+ "Content-Type": "application/json"
31
+ },
32
+ body: JSON.stringify(input)
33
+ });
34
+ if (!res.ok) {
35
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
36
+ if (res.status === 429) {
37
+ throw new Error(body.error || "Rate limit exceeded (3 builds per day)");
38
+ }
39
+ throw new Error(body.error || `Build failed (HTTP ${res.status})`);
40
+ }
41
+ const data = await res.json();
42
+ return data.sections;
43
+ }
44
+ async function publishServer(key) {
45
+ const res = await fetch(`${API_BASE}/api/mcp/publish`, {
46
+ method: "POST",
47
+ headers: {
48
+ Authorization: `Bearer ${key}`,
49
+ "Content-Type": "application/json"
50
+ },
51
+ body: JSON.stringify({ published: true })
52
+ });
53
+ if (!res.ok) {
54
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
55
+ if (res.status === 402) {
56
+ throw new Error("REQUIRES_SUBSCRIPTION");
57
+ }
58
+ throw new Error(body.error || `Publish failed (HTTP ${res.status})`);
59
+ }
60
+ return res.json();
61
+ }
25
62
 
26
63
  // src/lib/tools.ts
27
64
  import os from "os";
@@ -155,14 +192,12 @@ var TOOLS = [
155
192
  // src/lib/paths.ts
156
193
  import os2 from "os";
157
194
  import path2 from "path";
158
- var home = os2.homedir();
159
- var platform = process.platform;
160
195
  function expandPath(p) {
161
- return p.replace(/^~/, home);
196
+ return p.replace(/^~/, os2.homedir());
162
197
  }
163
198
  function resolveConfigPaths(configPaths) {
164
199
  const resolved = [];
165
- const platformKey = platform;
200
+ const platformKey = process.platform;
166
201
  const globalPaths = configPaths[platformKey] || [];
167
202
  for (const p of globalPaths) {
168
203
  resolved.push(expandPath(p));
@@ -427,16 +462,283 @@ function isCommandAvailable(cmd) {
427
462
  }
428
463
 
429
464
  // src/commands/status.ts
465
+ import fs5 from "fs";
466
+ import path7 from "path";
467
+
468
+ // src/lib/brain-scanner.ts
430
469
  import fs4 from "fs";
470
+ import path6 from "path";
471
+ import os3 from "os";
472
+ var home = os3.homedir();
473
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
474
+ "node_modules",
475
+ ".git",
476
+ "__pycache__",
477
+ ".venv",
478
+ "venv",
479
+ "dist",
480
+ "build",
481
+ ".next",
482
+ ".nuxt",
483
+ ".output",
484
+ ".Trash",
485
+ "Library",
486
+ ".cache",
487
+ ".npm",
488
+ ".yarn",
489
+ ".pnpm-store",
490
+ "Caches",
491
+ "Cache"
492
+ ]);
493
+ var FULL_READ_FILES = /* @__PURE__ */ new Set([
494
+ "README.md",
495
+ "CLAUDE.md",
496
+ ".cursorrules",
497
+ ".windsurfrules",
498
+ "AGENTS.md",
499
+ "CONVENTIONS.md",
500
+ "MEMORY.md",
501
+ "SOUL.md",
502
+ "IDENTITY.md"
503
+ ]);
504
+ var MAX_FILE_SIZE = 100 * 1024;
505
+ var RECENT_DAYS = 30;
506
+ function shouldSkipDir(name) {
507
+ if (name.startsWith(".") && name !== ".claude" && name !== ".cursor" && name !== ".windsurf" && name !== ".github" && name !== ".zed") {
508
+ return true;
509
+ }
510
+ return SKIP_DIRS.has(name);
511
+ }
512
+ function readFileSafe(filePath) {
513
+ try {
514
+ const stat = fs4.statSync(filePath);
515
+ if (stat.size > MAX_FILE_SIZE) return null;
516
+ if (!stat.isFile()) return null;
517
+ return fs4.readFileSync(filePath, "utf-8");
518
+ } catch {
519
+ return null;
520
+ }
521
+ }
522
+ function getFolderTree(dir, depth = 0, maxDepth = 3) {
523
+ if (depth >= maxDepth) return [];
524
+ const entries = [];
525
+ try {
526
+ const items = fs4.readdirSync(dir, { withFileTypes: true });
527
+ for (const item of items) {
528
+ if (!item.isDirectory()) continue;
529
+ if (shouldSkipDir(item.name)) continue;
530
+ const indent = " ".repeat(depth);
531
+ entries.push(`${indent}${item.name}/`);
532
+ entries.push(...getFolderTree(path6.join(dir, item.name), depth + 1, maxDepth));
533
+ }
534
+ } catch {
535
+ }
536
+ return entries;
537
+ }
538
+ function detectProjects(scanDirs) {
539
+ const projects = [];
540
+ for (const dir of scanDirs) {
541
+ try {
542
+ const items = fs4.readdirSync(dir, { withFileTypes: true });
543
+ for (const item of items) {
544
+ if (!item.isDirectory()) continue;
545
+ if (shouldSkipDir(item.name)) continue;
546
+ const projectPath = path6.join(dir, item.name);
547
+ const hasGit = fs4.existsSync(path6.join(projectPath, ".git"));
548
+ const hasPkgJson = fs4.existsSync(path6.join(projectPath, "package.json"));
549
+ const hasCargoToml = fs4.existsSync(path6.join(projectPath, "Cargo.toml"));
550
+ const hasPyproject = fs4.existsSync(path6.join(projectPath, "pyproject.toml"));
551
+ const hasGoMod = fs4.existsSync(path6.join(projectPath, "go.mod"));
552
+ if (!hasGit && !hasPkgJson && !hasCargoToml && !hasPyproject && !hasGoMod) continue;
553
+ let description = "";
554
+ if (hasPkgJson) {
555
+ try {
556
+ const pkg = JSON.parse(fs4.readFileSync(path6.join(projectPath, "package.json"), "utf-8"));
557
+ description = pkg.description || "";
558
+ } catch {
559
+ }
560
+ }
561
+ const readmePath = path6.join(projectPath, "README.md");
562
+ if (!description && fs4.existsSync(readmePath)) {
563
+ const content = readFileSafe(readmePath);
564
+ if (content) {
565
+ const lines = content.split("\n");
566
+ let foundHeading = false;
567
+ for (const line of lines) {
568
+ if (line.startsWith("#")) {
569
+ foundHeading = true;
570
+ continue;
571
+ }
572
+ if (foundHeading && line.trim()) {
573
+ description = line.trim().slice(0, 200);
574
+ break;
575
+ }
576
+ }
577
+ }
578
+ }
579
+ projects.push({
580
+ name: item.name,
581
+ path: projectPath,
582
+ description,
583
+ hasClaudeMd: fs4.existsSync(path6.join(projectPath, "CLAUDE.md")),
584
+ hasCursorRules: fs4.existsSync(path6.join(projectPath, ".cursorrules")) || fs4.existsSync(path6.join(projectPath, ".cursor", "rules")),
585
+ hasWindsurfRules: fs4.existsSync(path6.join(projectPath, ".windsurfrules")) || fs4.existsSync(path6.join(projectPath, ".windsurf", "rules")),
586
+ hasCopilotInstructions: fs4.existsSync(path6.join(projectPath, ".github", "copilot-instructions.md")),
587
+ hasConventionsMd: fs4.existsSync(path6.join(projectPath, "CONVENTIONS.md")),
588
+ hasZedRules: fs4.existsSync(path6.join(projectPath, ".zed", "rules.md"))
589
+ });
590
+ }
591
+ } catch {
592
+ }
593
+ }
594
+ return projects;
595
+ }
596
+ function collectConfigFiles(projects) {
597
+ const configs = [];
598
+ const globalPaths = [
599
+ path6.join(home, ".claude", "MEMORY.md"),
600
+ path6.join(home, ".claude", "CLAUDE.md"),
601
+ path6.join(home, ".openclaw", "workspace", "SOUL.md"),
602
+ path6.join(home, ".openclaw", "workspace", "MEMORY.md")
603
+ ];
604
+ for (const gp of globalPaths) {
605
+ const content = readFileSafe(gp);
606
+ if (content && content.trim()) {
607
+ configs.push({ name: `~/${path6.relative(home, gp)}`, content });
608
+ }
609
+ }
610
+ for (const project of projects) {
611
+ for (const fileName of FULL_READ_FILES) {
612
+ const filePath = path6.join(project.path, fileName);
613
+ const content = readFileSafe(filePath);
614
+ if (content && content.trim()) {
615
+ configs.push({ name: `${project.name}/${fileName}`, content });
616
+ }
617
+ }
618
+ const pkgPath = path6.join(project.path, "package.json");
619
+ const pkgContent = readFileSafe(pkgPath);
620
+ if (pkgContent) {
621
+ try {
622
+ const pkg = JSON.parse(pkgContent);
623
+ const summary = JSON.stringify({ name: pkg.name, description: pkg.description }, null, 2);
624
+ configs.push({ name: `${project.name}/package.json`, content: summary });
625
+ } catch {
626
+ }
627
+ }
628
+ }
629
+ return configs;
630
+ }
631
+ function collectRecentDocs(projects) {
632
+ const docs = [];
633
+ const cutoff = Date.now() - RECENT_DAYS * 24 * 60 * 60 * 1e3;
634
+ const seen = /* @__PURE__ */ new Set();
635
+ for (const project of projects) {
636
+ try {
637
+ const items = fs4.readdirSync(project.path, { withFileTypes: true });
638
+ for (const item of items) {
639
+ if (!item.isFile()) continue;
640
+ if (!item.name.endsWith(".md")) continue;
641
+ if (FULL_READ_FILES.has(item.name)) continue;
642
+ if (item.name.startsWith(".")) continue;
643
+ const filePath = path6.join(project.path, item.name);
644
+ if (seen.has(filePath)) continue;
645
+ seen.add(filePath);
646
+ try {
647
+ const stat = fs4.statSync(filePath);
648
+ if (stat.mtimeMs < cutoff) continue;
649
+ if (stat.size > MAX_FILE_SIZE) continue;
650
+ const content = fs4.readFileSync(filePath, "utf-8");
651
+ if (content.trim()) {
652
+ docs.push({ name: `${project.name}/${item.name}`, content });
653
+ }
654
+ } catch {
655
+ }
656
+ }
657
+ } catch {
658
+ }
659
+ }
660
+ return docs.slice(0, 20);
661
+ }
662
+ function getDefaultScanDirs() {
663
+ const dirs = [];
664
+ const candidates = [
665
+ path6.join(home, "Projects"),
666
+ path6.join(home, "projects"),
667
+ path6.join(home, "Developer"),
668
+ path6.join(home, "dev"),
669
+ path6.join(home, "Code"),
670
+ path6.join(home, "code"),
671
+ path6.join(home, "src"),
672
+ path6.join(home, "repos"),
673
+ path6.join(home, "workspace"),
674
+ path6.join(home, "Workspace"),
675
+ path6.join(home, "Documents"),
676
+ path6.join(home, "Desktop")
677
+ ];
678
+ for (const dir of candidates) {
679
+ if (fs4.existsSync(dir) && fs4.statSync(dir).isDirectory()) {
680
+ dirs.push(dir);
681
+ }
682
+ }
683
+ if (dirs.length === 0) {
684
+ dirs.push(home);
685
+ }
686
+ return dirs;
687
+ }
688
+ function scanForBrain(folders) {
689
+ const scanDirs = folders || getDefaultScanDirs();
690
+ const folderTree = [];
691
+ for (const dir of scanDirs) {
692
+ folderTree.push(`${path6.basename(dir)}/`);
693
+ folderTree.push(...getFolderTree(dir, 1));
694
+ }
695
+ const projects = detectProjects(scanDirs);
696
+ const configFiles = collectConfigFiles(projects);
697
+ const recentDocuments = collectRecentDocs(projects);
698
+ return {
699
+ summary: {
700
+ folders: folderTree,
701
+ projects: projects.map((p) => ({
702
+ name: p.name,
703
+ path: p.path.replace(home, "~"),
704
+ description: p.description
705
+ })),
706
+ configFiles,
707
+ recentDocuments
708
+ }
709
+ };
710
+ }
711
+ function scanForProjects(folders) {
712
+ const scanDirs = folders || getDefaultScanDirs();
713
+ return detectProjects(scanDirs);
714
+ }
715
+
716
+ // src/commands/status.ts
717
+ var PIUT_FILES = [
718
+ "CLAUDE.md",
719
+ ".cursor/rules/piut.mdc",
720
+ ".windsurf/rules/piut.md",
721
+ ".github/copilot-instructions.md",
722
+ "CONVENTIONS.md",
723
+ ".zed/rules.md"
724
+ ];
725
+ function hasPiutReference(filePath) {
726
+ try {
727
+ const content = fs5.readFileSync(filePath, "utf-8");
728
+ return content.includes("p\u0131ut Context") || content.includes("piut Context");
729
+ } catch {
730
+ return false;
731
+ }
732
+ }
431
733
  function statusCommand() {
432
734
  banner();
433
- console.log(" AI tool configuration status:");
735
+ console.log(" AI tool configuration:");
434
736
  console.log();
435
737
  let foundAny = false;
436
738
  for (const tool of TOOLS) {
437
739
  const paths = resolveConfigPaths(tool.configPaths);
438
740
  for (const configPath of paths) {
439
- if (!fs4.existsSync(configPath)) continue;
741
+ if (!fs5.existsSync(configPath)) continue;
440
742
  foundAny = true;
441
743
  const configured = isPiutConfigured(configPath, tool.configKey);
442
744
  if (configured) {
@@ -449,13 +751,38 @@ function statusCommand() {
449
751
  }
450
752
  if (!foundAny) {
451
753
  console.log(warning(" No supported AI tools detected."));
452
- console.log(dim(" Supported: Claude Code, Claude Desktop, Cursor, Windsurf, GitHub Copilot, Amazon Q, Zed"));
754
+ console.log(dim(" Run ") + brand("piut setup") + dim(" to configure your AI tools."));
755
+ }
756
+ console.log();
757
+ console.log(" Connected projects:");
758
+ console.log();
759
+ const projects = scanForProjects();
760
+ let connectedCount = 0;
761
+ for (const project of projects) {
762
+ const connectedFiles = [];
763
+ for (const file of PIUT_FILES) {
764
+ const absPath = path7.join(project.path, file);
765
+ if (fs5.existsSync(absPath) && hasPiutReference(absPath)) {
766
+ connectedFiles.push(file);
767
+ }
768
+ }
769
+ if (connectedFiles.length > 0) {
770
+ connectedCount++;
771
+ console.log(success(` \u2714 ${project.name}`) + dim(` (${connectedFiles.join(", ")})`));
772
+ }
773
+ }
774
+ if (connectedCount === 0) {
775
+ console.log(dim(" No projects connected."));
776
+ console.log(dim(" Run ") + brand("piut connect") + dim(" to add brain references to your projects."));
777
+ } else {
778
+ console.log();
779
+ console.log(dim(` ${connectedCount} project(s) connected to your brain.`));
453
780
  }
454
781
  console.log();
455
782
  }
456
783
 
457
784
  // src/commands/remove.ts
458
- import fs5 from "fs";
785
+ import fs6 from "fs";
459
786
  import { checkbox as checkbox2, confirm as confirm2 } from "@inquirer/prompts";
460
787
  async function removeCommand() {
461
788
  banner();
@@ -463,7 +790,7 @@ async function removeCommand() {
463
790
  for (const tool of TOOLS) {
464
791
  const paths = resolveConfigPaths(tool.configPaths);
465
792
  for (const configPath of paths) {
466
- if (fs5.existsSync(configPath) && isPiutConfigured(configPath, tool.configKey)) {
793
+ if (fs6.existsSync(configPath) && isPiutConfigured(configPath, tool.configKey)) {
467
794
  configured.push({ tool, configPath });
468
795
  break;
469
796
  }
@@ -505,1172 +832,607 @@ async function removeCommand() {
505
832
  console.log();
506
833
  }
507
834
 
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
- }
835
+ // src/commands/build.ts
836
+ import { select } from "@inquirer/prompts";
837
+ import chalk4 from "chalk";
656
838
 
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
- }
839
+ // src/lib/auth.ts
840
+ import { password as password2 } from "@inquirer/prompts";
841
+ import chalk3 from "chalk";
731
842
 
732
- // src/lib/sync-config.ts
843
+ // src/lib/store.ts
733
844
  import fs7 from "fs";
734
- import path7 from "path";
845
+ import path8 from "path";
735
846
  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
- };
847
+ var CONFIG_DIR = path8.join(os4.homedir(), ".piut");
848
+ var CONFIG_FILE = path8.join(CONFIG_DIR, "config.json");
849
+ function readStore() {
754
850
  try {
755
851
  const raw = fs7.readFileSync(CONFIG_FILE, "utf-8");
756
- const parsed = JSON.parse(raw);
757
- return { ...defaults, ...parsed };
852
+ return JSON.parse(raw);
758
853
  } catch {
759
- return defaults;
854
+ return {};
760
855
  }
761
856
  }
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();
857
+ function updateStore(updates) {
858
+ const config = readStore();
768
859
  const updated = { ...config, ...updates };
769
- writeSyncConfig(updated);
860
+ fs7.mkdirSync(CONFIG_DIR, { recursive: true });
861
+ fs7.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2) + "\n", "utf-8");
770
862
  return updated;
771
863
  }
772
- function getConfigFile() {
773
- return CONFIG_FILE;
774
- }
775
864
 
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
- }
865
+ // src/lib/auth.ts
866
+ async function resolveApiKey(keyOption) {
867
+ const config = readStore();
868
+ let apiKey = keyOption || config.apiKey;
869
+ if (!apiKey) {
870
+ apiKey = await password2({
871
+ message: "Enter your p\u0131ut API key:",
872
+ mask: "*",
873
+ validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
874
+ });
901
875
  }
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();
876
+ console.log(dim(" Validating key..."));
877
+ let result;
878
+ try {
879
+ result = await validateKey(apiKey);
880
+ } catch (err) {
881
+ console.log(chalk3.red(` \u2717 ${err.message}`));
882
+ console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
883
+ process.exit(1);
907
884
  }
908
- return safe;
885
+ console.log(success(` \u2713 Connected as ${result.displayName} (${result.slug})`));
886
+ updateStore({ apiKey });
887
+ return apiKey;
909
888
  }
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;
889
+ async function resolveApiKeyWithResult(keyOption) {
890
+ const config = readStore();
891
+ let apiKey = keyOption || config.apiKey;
916
892
  if (!apiKey) {
917
- console.log(dim(" Enter your API key, or press Enter to get one at piut.com/dashboard"));
918
- console.log();
919
893
  apiKey = await password2({
920
- message: "Enter your API key (or press Enter to get one)",
894
+ message: "Enter your p\u0131ut API key:",
921
895
  mask: "*",
922
- validate: (v) => {
923
- if (!v) return true;
924
- return v.startsWith("pb_") || "Key must start with pb_";
925
- }
896
+ validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
926
897
  });
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
898
  }
935
899
  console.log(dim(" Validating key..."));
936
- let validationResult;
900
+ let result;
937
901
  try {
938
- validationResult = await validateKey(apiKey);
902
+ result = await validateKey(apiKey);
939
903
  } catch (err) {
940
904
  console.log(chalk3.red(` \u2717 ${err.message}`));
941
- console.log(dim(" Get a key at https://piut.com/dashboard"));
905
+ console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
942
906
  process.exit(1);
943
907
  }
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"));
908
+ console.log(success(` \u2713 Connected as ${result.displayName} (${result.slug})`));
909
+ updateStore({ apiKey });
910
+ return { apiKey, ...result };
911
+ }
912
+
913
+ // src/commands/build.ts
914
+ async function buildCommand(options) {
915
+ banner();
916
+ const apiKey = await resolveApiKey(options.key);
917
+ let scanFolders;
918
+ if (options.folders) {
919
+ scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
920
+ }
921
+ const mode = scanFolders ? "auto" : await select({
922
+ message: "How do you want to build your brain?",
923
+ choices: [
924
+ { name: "Automatically (recommended)", value: "auto", description: "Scan your files and build automatically" },
925
+ { name: "Select folder(s)...", value: "folders", description: "Choose specific folders to scan" }
926
+ ]
927
+ });
928
+ if (mode === "folders" && !scanFolders) {
929
+ const defaults = getDefaultScanDirs();
952
930
  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;
931
+ console.log(dim(" Detected directories:"));
932
+ for (const d of defaults) {
933
+ console.log(dim(` ${d}`));
968
934
  }
935
+ console.log();
936
+ console.log(dim(" Tip: pass --folders ~/Projects,~/Documents to specify directly"));
937
+ scanFolders = defaults;
969
938
  }
970
939
  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.)"));
940
+ console.log(dim(" Building your brain..."));
973
941
  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."));
942
+ const input = scanForBrain(scanFolders);
943
+ const projCount = input.summary.projects.length;
944
+ const configCount = input.summary.configFiles.length;
945
+ const docCount = input.summary.recentDocuments.length;
946
+ console.log(dim(` Scanned: ${projCount} projects, ${configCount} config files, ${docCount} recent docs`));
947
+ console.log();
948
+ if (projCount === 0 && configCount === 0) {
949
+ console.log(chalk4.yellow(" No projects or config files found to build from."));
950
+ console.log(dim(" Try running from a directory with your projects, or use --folders."));
988
951
  console.log();
989
952
  return;
990
953
  }
991
- const safeFiles = guardAndFilter(scannedFiles, options);
992
- if (safeFiles.length === 0) {
993
- console.log(warning(" All files were blocked by the sensitive file guard."));
954
+ try {
955
+ const sections = await buildBrain(apiKey, input);
956
+ const sectionSummary = (content, label) => {
957
+ if (!content || !content.trim()) {
958
+ console.log(dim(` ${label} \u2014 (empty)`));
959
+ } else {
960
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"))?.trim() || "";
961
+ const preview = firstLine.length > 60 ? firstLine.slice(0, 60) + "..." : firstLine;
962
+ console.log(success(` ${label}`) + dim(` \u2014 ${preview}`));
963
+ }
964
+ };
965
+ console.log(success(" Brain built!"));
994
966
  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
- }
967
+ sectionSummary(sections.about, "About");
968
+ sectionSummary(sections.soul, "Soul");
969
+ sectionSummary(sections.areas, "Areas of Responsibility");
970
+ sectionSummary(sections.projects, "Projects");
971
+ sectionSummary(sections.memory, "Memory");
1012
972
  console.log();
973
+ console.log(dim(` Review and edit at ${brand("piut.com/dashboard")}`));
974
+ console.log();
975
+ } catch (err) {
976
+ console.log(chalk4.red(` \u2717 ${err.message}`));
977
+ process.exit(1);
1013
978
  }
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);
979
+ }
980
+
981
+ // src/commands/deploy.ts
982
+ import { confirm as confirm3 } from "@inquirer/prompts";
983
+ import chalk5 from "chalk";
984
+ async function deployCommand(options) {
985
+ banner();
986
+ const { apiKey, slug, serverUrl } = await resolveApiKeyWithResult(options.key);
987
+ console.log();
988
+ console.log(dim(" Your brain will be published as an MCP server at:"));
989
+ console.log(` ${brand(serverUrl)}`);
1028
990
  console.log();
1029
- console.log(` View your backups: ${brand("https://piut.com/dashboard/backups")}`);
991
+ console.log(dim(" Any AI tool with this URL can read your brain."));
1030
992
  console.log();
1031
993
  if (!options.yes) {
1032
- const autoBackup = await confirm3({
1033
- message: "Configure auto-backup?",
1034
- default: false
994
+ const proceed = await confirm3({
995
+ message: "Deploy?",
996
+ default: true
1035
997
  });
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 {
998
+ if (!proceed) {
999
+ console.log(dim(" Cancelled."));
1000
+ return;
1096
1001
  }
1097
1002
  }
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
1003
  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
- }
1004
+ await publishServer(apiKey);
1005
+ console.log();
1006
+ console.log(success(" \u2713 Brain deployed. MCP server live."));
1007
+ console.log(dim(` URL: ${serverUrl}`));
1008
+ console.log();
1009
+ console.log(dim(" Next: run ") + brand("piut connect") + dim(" to add brain references to your projects."));
1119
1010
  console.log();
1120
- console.log(success(` Pulled ${result.files.length} file(s)`));
1121
1011
  } catch (err) {
1122
- console.log(chalk3.red(` \u2717 Pull failed: ${err.message}`));
1123
- process.exit(1);
1012
+ const msg = err.message;
1013
+ if (msg === "REQUIRES_SUBSCRIPTION") {
1014
+ console.log();
1015
+ console.log(chalk5.yellow(" Deploy requires an active subscription ($10/mo)."));
1016
+ console.log();
1017
+ console.log(` Subscribe at: ${brand("https://piut.com/dashboard/billing")}`);
1018
+ console.log(dim(" 14-day free trial included."));
1019
+ console.log();
1020
+ } else {
1021
+ console.log(chalk5.red(` \u2717 ${msg}`));
1022
+ process.exit(1);
1023
+ }
1124
1024
  }
1125
- console.log();
1126
1025
  }
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);
1026
+
1027
+ // src/commands/connect.ts
1028
+ import fs8 from "fs";
1029
+ import path9 from "path";
1030
+ import { checkbox as checkbox3 } from "@inquirer/prompts";
1031
+ var RULE_FILES = [
1032
+ {
1033
+ tool: "Claude Code",
1034
+ filePath: "CLAUDE.md",
1035
+ strategy: "append",
1036
+ detect: (p) => p.hasClaudeMd || fs8.existsSync(path9.join(p.path, ".claude"))
1037
+ },
1038
+ {
1039
+ tool: "Cursor",
1040
+ filePath: ".cursor/rules/piut.mdc",
1041
+ strategy: "create",
1042
+ detect: (p) => p.hasCursorRules || fs8.existsSync(path9.join(p.path, ".cursor"))
1043
+ },
1044
+ {
1045
+ tool: "Windsurf",
1046
+ filePath: ".windsurf/rules/piut.md",
1047
+ strategy: "create",
1048
+ detect: (p) => p.hasWindsurfRules || fs8.existsSync(path9.join(p.path, ".windsurf"))
1049
+ },
1050
+ {
1051
+ tool: "GitHub Copilot",
1052
+ filePath: ".github/copilot-instructions.md",
1053
+ strategy: "append",
1054
+ detect: (p) => p.hasCopilotInstructions || fs8.existsSync(path9.join(p.path, ".github"))
1055
+ },
1056
+ {
1057
+ tool: "Amazon Q",
1058
+ filePath: "CONVENTIONS.md",
1059
+ strategy: "append",
1060
+ detect: (p) => p.hasConventionsMd
1061
+ },
1062
+ {
1063
+ tool: "Zed",
1064
+ filePath: ".zed/rules.md",
1065
+ strategy: "create",
1066
+ detect: (p) => p.hasZedRules || fs8.existsSync(path9.join(p.path, ".zed"))
1140
1067
  }
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}`);
1068
+ ];
1069
+ var DEDICATED_FILE_CONTENT = `## p\u0131ut Context
1070
+ This project uses p\u0131ut for persistent personal context.
1071
+ Skill reference: https://raw.githubusercontent.com/M-Flat-Inc/piut/main/skill.md
1072
+
1073
+ Always call \`get_context\` at the start of every conversation.
1074
+ Read the \`soul\` section first \u2014 it contains behavioral instructions.
1075
+ Use \`update_brain\` for substantial new info, \`append_brain\` for quick notes.
1076
+ `;
1077
+ var APPEND_SECTION = `
1078
+
1079
+ ## p\u0131ut Context
1080
+ Skill reference: https://raw.githubusercontent.com/M-Flat-Inc/piut/main/skill.md
1081
+ Always call \`get_context\` at the start of every conversation to load personal context.
1082
+ `;
1083
+ function hasPiutReference2(filePath) {
1084
+ try {
1085
+ const content = fs8.readFileSync(filePath, "utf-8");
1086
+ return content.includes("p\u0131ut Context") || content.includes("piut Context");
1087
+ } catch {
1088
+ return false;
1151
1089
  }
1152
- console.log();
1153
1090
  }
1154
- async function diffFlow(filePathOrId) {
1091
+ async function connectCommand(options) {
1155
1092
  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);
1093
+ await resolveApiKeyWithResult(options.key);
1094
+ let scanFolders;
1095
+ if (options.folders) {
1096
+ scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
1162
1097
  }
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"));
1098
+ console.log();
1099
+ console.log(dim(" Scanning for projects..."));
1100
+ const projects = scanForProjects(scanFolders);
1101
+ if (projects.length === 0) {
1102
+ console.log(warning(" No projects found."));
1103
+ console.log(dim(" Try running from a directory with your projects, or use --folders."));
1182
1104
  console.log();
1183
1105
  return;
1184
1106
  }
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}`));
1107
+ const actions = [];
1108
+ for (const project of projects) {
1109
+ for (const rule of RULE_FILES) {
1110
+ if (!rule.detect(project)) continue;
1111
+ const absPath = path9.join(project.path, rule.filePath);
1112
+ if (fs8.existsSync(absPath) && hasPiutReference2(absPath)) continue;
1113
+ actions.push({
1114
+ project,
1115
+ tool: rule.tool,
1116
+ filePath: rule.filePath,
1117
+ absPath,
1118
+ action: rule.strategy === "create" || !fs8.existsSync(absPath) ? "create" : "append"
1119
+ });
1120
+ }
1121
+ const hasAnyAction = actions.some((a) => a.project === project);
1122
+ if (!hasAnyAction) {
1123
+ const claudeMdPath = path9.join(project.path, "CLAUDE.md");
1124
+ if (!fs8.existsSync(claudeMdPath)) {
1125
+ actions.push({
1126
+ project,
1127
+ tool: "Claude Code",
1128
+ filePath: "CLAUDE.md",
1129
+ absPath: claudeMdPath,
1130
+ action: "create"
1131
+ });
1132
+ } else if (!hasPiutReference2(claudeMdPath)) {
1133
+ actions.push({
1134
+ project,
1135
+ tool: "Claude Code",
1136
+ filePath: "CLAUDE.md",
1137
+ absPath: claudeMdPath,
1138
+ action: "append"
1139
+ });
1208
1140
  }
1209
1141
  }
1210
1142
  }
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."));
1143
+ if (actions.length === 0) {
1144
+ console.log(dim(" All projects are already connected."));
1234
1145
  console.log();
1235
1146
  return;
1236
1147
  }
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
- }
1148
+ const byProject = /* @__PURE__ */ new Map();
1149
+ for (const action of actions) {
1150
+ const key = action.project.path;
1151
+ if (!byProject.has(key)) byProject.set(key, []);
1152
+ byProject.get(key).push(action);
1268
1153
  }
1269
1154
  console.log();
1270
- }
1271
- async function watchFlow() {
1272
- banner();
1273
- console.log(brand.bold(" Live Sync (Watch Mode)"));
1155
+ console.log(` Found ${brand.bold(String(byProject.size))} project(s) to connect:`);
1274
1156
  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}`));
1157
+ const projectChoices = [];
1158
+ for (const [projectPath, projectActions] of byProject) {
1159
+ const projectName = path9.basename(projectPath);
1160
+ const desc = projectActions.map((a) => {
1161
+ const verb = a.action === "create" ? "will create" : "will append to";
1162
+ return `${verb} ${a.filePath}`;
1163
+ }).join(", ");
1164
+ projectChoices.push({
1165
+ name: `${projectName} ${dim(`(${desc})`)}`,
1166
+ value: projectPath,
1167
+ checked: true
1168
+ });
1299
1169
  }
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();
1170
+ let selectedPaths;
1171
+ if (options.yes) {
1172
+ selectedPaths = Array.from(byProject.keys());
1354
1173
  } 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();
1174
+ selectedPaths = await checkbox3({
1175
+ message: "Select projects to connect:",
1176
+ choices: projectChoices
1177
+ });
1178
+ if (selectedPaths.length === 0) {
1179
+ console.log(dim(" No projects selected."));
1180
+ return;
1181
+ }
1361
1182
  }
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
1183
  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 {
1184
+ let connected = 0;
1185
+ for (const projectPath of selectedPaths) {
1186
+ const projectActions = byProject.get(projectPath) || [];
1187
+ const projectName = path9.basename(projectPath);
1188
+ for (const action of projectActions) {
1189
+ if (action.action === "create") {
1190
+ const isAppendType = RULE_FILES.find((r) => r.filePath === action.filePath)?.strategy === "append";
1191
+ const content = isAppendType ? SKILL_SNIPPET + "\n" : DEDICATED_FILE_CONTENT;
1192
+ fs8.mkdirSync(path9.dirname(action.absPath), { recursive: true });
1193
+ fs8.writeFileSync(action.absPath, content, "utf-8");
1194
+ console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 created"));
1195
+ } else {
1196
+ fs8.appendFileSync(action.absPath, APPEND_SECTION);
1197
+ console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 appended"));
1198
+ }
1199
+ connected++;
1422
1200
  }
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
1201
  }
1429
1202
  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."));
1203
+ console.log(success(` Done. ${connected} file(s) updated across ${selectedPaths.length} project(s).`));
1440
1204
  console.log();
1441
1205
  }
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
- }
1206
+
1207
+ // src/commands/disconnect.ts
1208
+ import fs9 from "fs";
1209
+ import path10 from "path";
1210
+ import { checkbox as checkbox4, confirm as confirm5 } from "@inquirer/prompts";
1211
+ var DEDICATED_FILES = /* @__PURE__ */ new Set([
1212
+ ".cursor/rules/piut.mdc",
1213
+ ".windsurf/rules/piut.md",
1214
+ ".zed/rules.md"
1215
+ ]);
1216
+ var APPEND_FILES = [
1217
+ "CLAUDE.md",
1218
+ ".github/copilot-instructions.md",
1219
+ "CONVENTIONS.md"
1220
+ ];
1221
+ function hasPiutReference3(filePath) {
1454
1222
  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}`));
1223
+ const content = fs9.readFileSync(filePath, "utf-8");
1224
+ return content.includes("p\u0131ut Context") || content.includes("piut Context");
1225
+ } catch {
1226
+ return false;
1475
1227
  }
1476
- console.log();
1477
1228
  }
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
- }));
1229
+ function removePiutSection(filePath) {
1493
1230
  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}`));
1231
+ let content = fs9.readFileSync(filePath, "utf-8");
1232
+ const patterns = [
1233
+ /\n*## p[ıi]ut Context[\s\S]*?(?=\n## |\n---\n|$)/g
1234
+ ];
1235
+ let changed = false;
1236
+ for (const pattern of patterns) {
1237
+ const newContent = content.replace(pattern, "");
1238
+ if (newContent !== content) {
1239
+ content = newContent;
1240
+ changed = true;
1506
1241
  }
1507
1242
  }
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`));
1243
+ if (changed) {
1244
+ content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1245
+ fs9.writeFileSync(filePath, content, "utf-8");
1514
1246
  }
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();
1247
+ return changed;
1548
1248
  } 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];
1249
+ return false;
1565
1250
  }
1566
- return sorted;
1567
1251
  }
1568
-
1569
- // src/commands/sync-config.ts
1570
- import chalk4 from "chalk";
1571
- async function syncConfigCommand(options) {
1252
+ async function disconnectCommand(options) {
1572
1253
  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);
1254
+ let scanFolders;
1255
+ if (options.folders) {
1256
+ scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
1257
+ }
1258
+ console.log(dim(" Scanning for connected projects..."));
1259
+ const projects = scanForProjects(scanFolders);
1260
+ const actions = [];
1261
+ for (const project of projects) {
1262
+ const projectName = path10.basename(project.path);
1263
+ for (const dedicatedFile of DEDICATED_FILES) {
1264
+ const absPath = path10.join(project.path, dedicatedFile);
1265
+ if (fs9.existsSync(absPath) && hasPiutReference3(absPath)) {
1266
+ actions.push({
1267
+ projectPath: project.path,
1268
+ projectName,
1269
+ filePath: dedicatedFile,
1270
+ absPath,
1271
+ action: "delete"
1272
+ });
1273
+ }
1578
1274
  }
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);
1275
+ for (const appendFile of APPEND_FILES) {
1276
+ const absPath = path10.join(project.path, appendFile);
1277
+ if (fs9.existsSync(absPath) && hasPiutReference3(absPath)) {
1278
+ actions.push({
1279
+ projectPath: project.path,
1280
+ projectName,
1281
+ filePath: appendFile,
1282
+ absPath,
1283
+ action: "remove-section"
1284
+ });
1285
+ }
1589
1286
  }
1590
- updateSyncConfig({ keepBrainUpdated: value });
1591
- console.log(success(` \u2713 Keep brain updated: ${value ? "ON" : "OFF"}`));
1592
- console.log();
1593
- return;
1594
1287
  }
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"}`));
1288
+ if (actions.length === 0) {
1289
+ console.log(dim(" No connected projects found."));
1603
1290
  console.log();
1604
1291
  return;
1605
1292
  }
1606
- if (options.show || options.files) {
1607
- showConfig();
1608
- return;
1293
+ const byProject = /* @__PURE__ */ new Map();
1294
+ for (const action of actions) {
1295
+ if (!byProject.has(action.projectPath)) byProject.set(action.projectPath, []);
1296
+ byProject.get(action.projectPath).push(action);
1609
1297
  }
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
1298
  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
- }
1299
+ const projectChoices = Array.from(byProject.entries()).map(([projectPath, projectActions]) => {
1300
+ const name = path10.basename(projectPath);
1301
+ const files = projectActions.map((a) => a.filePath).join(", ");
1302
+ return {
1303
+ name: `${name} ${dim(`(${files})`)}`,
1304
+ value: projectPath,
1305
+ checked: true
1306
+ };
1307
+ });
1308
+ let selectedPaths;
1309
+ if (options.yes) {
1310
+ selectedPaths = Array.from(byProject.keys());
1631
1311
  } else {
1632
- console.log(dim(" No files backed up yet."));
1312
+ selectedPaths = await checkbox4({
1313
+ message: "Select projects to disconnect:",
1314
+ choices: projectChoices
1315
+ });
1316
+ if (selectedPaths.length === 0) {
1317
+ console.log(dim(" No projects selected."));
1318
+ return;
1319
+ }
1320
+ const proceed = await confirm5({
1321
+ message: `Disconnect ${selectedPaths.length} project(s)?`,
1322
+ default: false
1323
+ });
1324
+ if (!proceed) return;
1633
1325
  }
1634
1326
  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]`));
1327
+ let disconnected = 0;
1328
+ for (const projectPath of selectedPaths) {
1329
+ const projectActions = byProject.get(projectPath) || [];
1330
+ const projectName = path10.basename(projectPath);
1331
+ for (const action of projectActions) {
1332
+ if (action.action === "delete") {
1333
+ try {
1334
+ fs9.unlinkSync(action.absPath);
1335
+ console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 deleted"));
1336
+ disconnected++;
1337
+ } catch {
1338
+ console.log(warning(` \u2717 ${projectName}/${action.filePath}`) + dim(" \u2014 could not delete"));
1339
+ }
1340
+ } else {
1341
+ const removed = removePiutSection(action.absPath);
1342
+ if (removed) {
1343
+ console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 section removed"));
1344
+ disconnected++;
1345
+ } else {
1346
+ console.log(warning(` \u2717 ${projectName}/${action.filePath}`) + dim(" \u2014 section not found"));
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1652
1351
  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]`));
1352
+ console.log(success(` Done. ${disconnected} file(s) updated.`));
1656
1353
  console.log();
1657
- console.log(` 5. ${chalk4.white("View current configuration")}`);
1658
- console.log(dim(` Command: piut sync config --show`));
1354
+ }
1355
+
1356
+ // src/commands/interactive.ts
1357
+ import { select as select2 } from "@inquirer/prompts";
1358
+ import chalk6 from "chalk";
1359
+ import { password as password3 } from "@inquirer/prompts";
1360
+ async function authenticate() {
1361
+ const config = readStore();
1362
+ let apiKey = config.apiKey;
1363
+ if (apiKey) {
1364
+ try {
1365
+ const result2 = await validateKey(apiKey);
1366
+ console.log(success(` Connected as ${result2.displayName} (${result2.slug})`));
1367
+ return apiKey;
1368
+ } catch {
1369
+ console.log(dim(" Saved key expired. Please re-authenticate."));
1370
+ apiKey = void 0;
1371
+ }
1372
+ }
1373
+ console.log(dim(" Connect to p\u0131ut:"));
1374
+ console.log(dim(" > Log in at piut.com"));
1375
+ console.log(dim(" > Enter p\u0131ut API key"));
1659
1376
  console.log();
1377
+ apiKey = await password3({
1378
+ message: "Enter your p\u0131ut API key:",
1379
+ mask: "*",
1380
+ validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
1381
+ });
1382
+ console.log(dim(" Validating key..."));
1383
+ let result;
1384
+ try {
1385
+ result = await validateKey(apiKey);
1386
+ } catch (err) {
1387
+ console.log(chalk6.red(` \u2717 ${err.message}`));
1388
+ console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
1389
+ process.exit(1);
1390
+ }
1391
+ console.log(success(` \u2713 Connected as ${result.displayName} (${result.slug})`));
1392
+ updateStore({ apiKey });
1393
+ return apiKey;
1660
1394
  }
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;
1395
+ async function interactiveMenu() {
1396
+ banner();
1397
+ const apiKey = await authenticate();
1398
+ console.log();
1399
+ const action = await select2({
1400
+ message: "What would you like to do?",
1401
+ choices: [
1402
+ { name: "Build Brain", value: "build", description: "Build or rebuild your brain from your files" },
1403
+ { name: "Deploy Brain", value: "deploy", description: "Publish your MCP server (requires paid account)" },
1404
+ { name: "Connect Projects", value: "connect", description: "Add brain references to project config files" },
1405
+ { name: "Disconnect Projects", value: "disconnect", description: "Remove brain references from project configs" },
1406
+ { name: "Status", value: "status", description: "Show brain, deployment, and connected projects" }
1407
+ ]
1408
+ });
1409
+ switch (action) {
1410
+ case "build":
1411
+ await buildCommand({ key: apiKey });
1412
+ break;
1413
+ case "deploy":
1414
+ await deployCommand({ key: apiKey });
1415
+ break;
1416
+ case "connect":
1417
+ await connectCommand({ key: apiKey });
1418
+ break;
1419
+ case "disconnect":
1420
+ await disconnectCommand({});
1421
+ break;
1422
+ case "status":
1423
+ statusCommand();
1424
+ break;
1425
+ }
1666
1426
  }
1667
1427
 
1668
1428
  // src/cli.ts
1669
1429
  var program = new Command();
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");
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);
1672
- program.command("status").description("Show which AI tools are configured with p\u0131ut").action(statusCommand);
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);
1430
+ program.name("piut").description("Build your AI brain instantly. Deploy it as an MCP server. Connect it to every project.").version("3.0.0").action(interactiveMenu);
1431
+ program.command("build").description("Build or rebuild your brain from your files").option("-k, --key <key>", "API key").option("--folders <paths>", "Comma-separated folder paths to scan").action(buildCommand);
1432
+ program.command("deploy").description("Publish your MCP server (requires paid account)").option("-k, --key <key>", "API key").option("-y, --yes", "Skip confirmation prompts").action(deployCommand);
1433
+ program.command("connect").description("Add brain references to project config files").option("-k, --key <key>", "API key").option("-y, --yes", "Skip interactive prompts").option("--folders <paths>", "Comma-separated folder paths to scan").action(connectCommand);
1434
+ program.command("disconnect").description("Remove brain references from project config files").option("-y, --yes", "Skip interactive prompts").option("--folders <paths>", "Comma-separated folder paths to scan").action(disconnectCommand);
1435
+ program.command("setup").description("Auto-detect and configure AI tools (MCP config)").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);
1436
+ program.command("status").description("Show brain, deployment, and connected projects").action(statusCommand);
1437
+ program.command("remove").description("Remove all p\u0131ut configurations").action(removeCommand);
1676
1438
  program.parse();