@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.
- package/README.md +192 -82
- package/dist/cli.js +834 -1072
- 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(/^~/,
|
|
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
|
|
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 (!
|
|
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("
|
|
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
|
|
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 (
|
|
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/
|
|
509
|
-
import
|
|
510
|
-
import
|
|
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/
|
|
658
|
-
|
|
659
|
-
|
|
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/
|
|
843
|
+
// src/lib/store.ts
|
|
733
844
|
import fs7 from "fs";
|
|
734
|
-
import
|
|
845
|
+
import path8 from "path";
|
|
735
846
|
import os4 from "os";
|
|
736
|
-
|
|
737
|
-
var
|
|
738
|
-
|
|
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
|
-
|
|
757
|
-
return { ...defaults, ...parsed };
|
|
852
|
+
return JSON.parse(raw);
|
|
758
853
|
} catch {
|
|
759
|
-
return
|
|
854
|
+
return {};
|
|
760
855
|
}
|
|
761
856
|
}
|
|
762
|
-
function
|
|
763
|
-
|
|
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
|
-
|
|
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/
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
885
|
+
console.log(success(` \u2713 Connected as ${result.displayName} (${result.slug})`));
|
|
886
|
+
updateStore({ apiKey });
|
|
887
|
+
return apiKey;
|
|
909
888
|
}
|
|
910
|
-
async function
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
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
|
|
900
|
+
let result;
|
|
937
901
|
try {
|
|
938
|
-
|
|
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
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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("
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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(
|
|
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
|
|
1033
|
-
message: "
|
|
1034
|
-
default:
|
|
994
|
+
const proceed = await confirm3({
|
|
995
|
+
message: "Deploy?",
|
|
996
|
+
default: true
|
|
1035
997
|
});
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
1091
|
+
async function connectCommand(options) {
|
|
1155
1092
|
banner();
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
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
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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(
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
|
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
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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 (
|
|
1596
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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(`
|
|
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
|
-
|
|
1658
|
-
|
|
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
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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("
|
|
1671
|
-
program.command("
|
|
1672
|
-
program.command("
|
|
1673
|
-
program.command("
|
|
1674
|
-
|
|
1675
|
-
|
|
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();
|