@piut/cli 3.5.1 → 3.7.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 (2) hide show
  1. package/dist/cli.js +1610 -482
  2. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -24,7 +24,19 @@ async function validateKey(key) {
24
24
  }
25
25
  return res.json();
26
26
  }
27
- async function* buildBrainStreaming(key, input2) {
27
+ async function loginWithEmail(email, password3) {
28
+ const res = await fetch(`${API_BASE}/api/cli/login`, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({ email, password: password3 })
32
+ });
33
+ if (!res.ok) {
34
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
35
+ throw new Error(body.error || `Login failed (HTTP ${res.status})`);
36
+ }
37
+ return res.json();
38
+ }
39
+ async function* buildBrainStreaming(key, input3) {
28
40
  const res = await fetch(`${API_BASE}/api/cli/build-brain`, {
29
41
  method: "POST",
30
42
  headers: {
@@ -32,7 +44,7 @@ async function* buildBrainStreaming(key, input2) {
32
44
  "Content-Type": "application/json",
33
45
  Accept: "text/event-stream"
34
46
  },
35
- body: JSON.stringify(input2)
47
+ body: JSON.stringify(input3)
36
48
  });
37
49
  if (!res.ok) {
38
50
  const body = await res.json().catch(() => ({ error: "Unknown error" }));
@@ -110,7 +122,10 @@ async function pingMcp(serverUrl, key, toolName) {
110
122
  headers: {
111
123
  Authorization: `Bearer ${key}`,
112
124
  "Content-Type": "application/json",
113
- "User-Agent": `piut-cli (configured: ${toolName})`
125
+ "User-Agent": `piut-cli (configured: ${toolName})`,
126
+ "X-Piut-Hostname": os.hostname(),
127
+ "X-Piut-Machine-Id": getMachineId(),
128
+ "X-Piut-Tool": toolName
114
129
  },
115
130
  body: JSON.stringify({
116
131
  jsonrpc: "2.0",
@@ -155,6 +170,9 @@ function getMachineId() {
155
170
  const hostname = os.hostname();
156
171
  return crypto.createHash("sha256").update(hostname).digest("hex").slice(0, 16);
157
172
  }
173
+ function getHostname() {
174
+ return os.hostname();
175
+ }
158
176
  async function registerProject(key, project) {
159
177
  const res = await fetch(`${API_BASE}/api/cli/projects`, {
160
178
  method: "POST",
@@ -184,6 +202,19 @@ async function unregisterProject(key, projectPath, machineId) {
184
202
  throw new Error(body.error || `Unregister project failed (HTTP ${res.status})`);
185
203
  }
186
204
  }
205
+ async function deleteConnections(key, toolNames) {
206
+ try {
207
+ await fetch(`${API_BASE}/api/mcp/connections`, {
208
+ method: "DELETE",
209
+ headers: {
210
+ Authorization: `Bearer ${key}`,
211
+ "Content-Type": "application/json"
212
+ },
213
+ body: JSON.stringify({ toolNames })
214
+ });
215
+ } catch {
216
+ }
217
+ }
187
218
  async function unpublishServer(key) {
188
219
  const res = await fetch(`${API_BASE}/api/mcp/publish`, {
189
220
  method: "POST",
@@ -199,12 +230,73 @@ async function unpublishServer(key) {
199
230
  }
200
231
  return res.json();
201
232
  }
233
+ async function listVaultFiles(key) {
234
+ const res = await fetch(`${API_BASE}/api/cli/vault`, {
235
+ headers: { Authorization: `Bearer ${key}` }
236
+ });
237
+ if (!res.ok) {
238
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
239
+ throw new Error(body.error || `Failed to list vault files (HTTP ${res.status})`);
240
+ }
241
+ return res.json();
242
+ }
243
+ async function uploadVaultFile(key, filename, content) {
244
+ const res = await fetch(`${API_BASE}/api/cli/vault/upload`, {
245
+ method: "POST",
246
+ headers: {
247
+ Authorization: `Bearer ${key}`,
248
+ "Content-Type": "application/json"
249
+ },
250
+ body: JSON.stringify({ filename, content })
251
+ });
252
+ if (!res.ok) {
253
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
254
+ throw new Error(body.error || `Upload failed (HTTP ${res.status})`);
255
+ }
256
+ return res.json();
257
+ }
258
+ async function readVaultFile(key, filename) {
259
+ const encoded = encodeURIComponent(filename);
260
+ const res = await fetch(`${API_BASE}/api/cli/vault/${encoded}`, {
261
+ headers: { Authorization: `Bearer ${key}` }
262
+ });
263
+ if (!res.ok) {
264
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
265
+ throw new Error(body.error || `Failed to read vault file (HTTP ${res.status})`);
266
+ }
267
+ return res.json();
268
+ }
269
+ async function deleteVaultFile(key, filename) {
270
+ const res = await fetch(`${API_BASE}/api/cli/vault`, {
271
+ method: "DELETE",
272
+ headers: {
273
+ Authorization: `Bearer ${key}`,
274
+ "Content-Type": "application/json"
275
+ },
276
+ body: JSON.stringify({ filename })
277
+ });
278
+ if (!res.ok) {
279
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
280
+ throw new Error(body.error || `Delete failed (HTTP ${res.status})`);
281
+ }
282
+ }
202
283
 
203
284
  // src/lib/tools.ts
204
285
  import os2 from "os";
205
286
  import path from "path";
287
+ import crypto2 from "crypto";
206
288
  var MCP_URL = (slug) => `https://piut.com/api/mcp/${slug}`;
207
289
  var AUTH_HEADER = (key) => ({ Authorization: `Bearer ${key}` });
290
+ function getMachineId2() {
291
+ return crypto2.createHash("sha256").update(os2.hostname()).digest("hex").slice(0, 16);
292
+ }
293
+ function machineHeaders(toolName) {
294
+ return {
295
+ "X-Piut-Hostname": os2.hostname(),
296
+ "X-Piut-Machine-Id": getMachineId2(),
297
+ "X-Piut-Tool": toolName
298
+ };
299
+ }
208
300
  function appData() {
209
301
  return process.env.APPDATA || path.join(os2.homedir(), "AppData", "Roaming");
210
302
  }
@@ -216,18 +308,19 @@ var TOOLS = [
216
308
  configPaths: {
217
309
  darwin: ["~/.claude.json"],
218
310
  win32: ["~/.claude.json"],
219
- linux: ["~/.claude.json"]
311
+ linux: ["~/.claude.json"],
312
+ project: [".mcp.json"]
220
313
  },
221
314
  skillFilePath: "CLAUDE.md",
222
315
  quickCommand: (slug, key) => `claude mcp add-json piut-context '${JSON.stringify({
223
316
  type: "http",
224
317
  url: MCP_URL(slug),
225
- headers: AUTH_HEADER(key)
318
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Claude Code") }
226
319
  })}'`,
227
320
  generateConfig: (slug, key) => ({
228
321
  type: "http",
229
322
  url: MCP_URL(slug),
230
- headers: AUTH_HEADER(key)
323
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Claude Code") }
231
324
  })
232
325
  },
233
326
  {
@@ -263,7 +356,7 @@ var TOOLS = [
263
356
  skillFilePath: ".cursor/rules/piut.mdc",
264
357
  generateConfig: (slug, key) => ({
265
358
  url: MCP_URL(slug),
266
- headers: AUTH_HEADER(key)
359
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Cursor") }
267
360
  })
268
361
  },
269
362
  {
@@ -278,7 +371,7 @@ var TOOLS = [
278
371
  skillFilePath: ".windsurf/rules/piut.md",
279
372
  generateConfig: (slug, key) => ({
280
373
  serverUrl: MCP_URL(slug),
281
- headers: AUTH_HEADER(key)
374
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Windsurf") }
282
375
  })
283
376
  },
284
377
  {
@@ -292,7 +385,7 @@ var TOOLS = [
292
385
  generateConfig: (slug, key) => ({
293
386
  type: "http",
294
387
  url: MCP_URL(slug),
295
- headers: AUTH_HEADER(key)
388
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("GitHub Copilot") }
296
389
  })
297
390
  },
298
391
  {
@@ -308,7 +401,7 @@ var TOOLS = [
308
401
  generateConfig: (slug, key) => ({
309
402
  type: "http",
310
403
  url: MCP_URL(slug),
311
- headers: AUTH_HEADER(key)
404
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Amazon Q") }
312
405
  })
313
406
  },
314
407
  {
@@ -323,9 +416,49 @@ var TOOLS = [
323
416
  generateConfig: (slug, key) => ({
324
417
  settings: {
325
418
  url: MCP_URL(slug),
326
- headers: AUTH_HEADER(key)
419
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Zed") }
327
420
  }
328
421
  })
422
+ },
423
+ {
424
+ id: "gemini-cli",
425
+ name: "Gemini CLI",
426
+ configKey: "mcpServers",
427
+ configPaths: {
428
+ darwin: ["~/.gemini/settings.json"],
429
+ win32: ["~/.gemini/settings.json"],
430
+ linux: ["~/.gemini/settings.json"],
431
+ project: [".gemini/settings.json"]
432
+ },
433
+ generateConfig: (slug, key) => ({
434
+ httpUrl: MCP_URL(slug),
435
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("Gemini CLI") }
436
+ })
437
+ },
438
+ {
439
+ id: "openclaw",
440
+ name: "OpenClaw",
441
+ configKey: "mcpServers",
442
+ configPaths: {
443
+ darwin: ["~/.mcporter/mcporter.json", "~/.openclaw/workspace/config/mcporter.json"],
444
+ win32: ["~/.mcporter/mcporter.json", "~/.openclaw/workspace/config/mcporter.json"],
445
+ linux: ["~/.mcporter/mcporter.json", "~/.openclaw/workspace/config/mcporter.json"]
446
+ },
447
+ quickCommand: (slug, key) => `npx mcporter config add piut-context ${MCP_URL(slug)} --header "Authorization=Bearer ${key}"`,
448
+ generateConfig: (slug, key) => ({
449
+ url: MCP_URL(slug),
450
+ headers: { ...AUTH_HEADER(key), ...machineHeaders("OpenClaw") }
451
+ })
452
+ },
453
+ {
454
+ id: "paperclip",
455
+ name: "Paperclip",
456
+ skillOnly: true,
457
+ configPaths: {
458
+ darwin: ["~/.paperclip/config.json"],
459
+ win32: ["~/.paperclip/config.json"],
460
+ linux: ["~/.paperclip/config.json"]
461
+ }
329
462
  }
330
463
  ];
331
464
 
@@ -418,6 +551,28 @@ function extractKeyFromConfig(piutConfig) {
418
551
  }
419
552
  return null;
420
553
  }
554
+ function extractSlugFromConfig(piutConfig) {
555
+ const slugFromUrl = (u) => {
556
+ if (typeof u !== "string") return null;
557
+ const m = u.match(/\/api\/mcp\/([^/?#]+)/);
558
+ return m ? m[1] : null;
559
+ };
560
+ const fromUrl = slugFromUrl(piutConfig.url) || slugFromUrl(piutConfig.serverUrl) || slugFromUrl(piutConfig.httpUrl);
561
+ if (fromUrl) return fromUrl;
562
+ const settings = piutConfig.settings;
563
+ if (settings) {
564
+ const fromSettings = slugFromUrl(settings.url);
565
+ if (fromSettings) return fromSettings;
566
+ }
567
+ const args2 = piutConfig.args;
568
+ if (Array.isArray(args2)) {
569
+ for (const arg of args2) {
570
+ const fromArg = slugFromUrl(arg);
571
+ if (fromArg) return fromArg;
572
+ }
573
+ }
574
+ return null;
575
+ }
421
576
  function removeFromConfig(filePath, configKey) {
422
577
  const config = readConfig(filePath);
423
578
  if (!config) return false;
@@ -728,20 +883,25 @@ async function setupCommand(options) {
728
883
  const toolFilter = options.tool;
729
884
  for (const tool of TOOLS) {
730
885
  if (toolFilter && tool.id !== toolFilter) continue;
886
+ if (tool.skillOnly) continue;
731
887
  const paths = resolveConfigPaths(tool.configPaths);
732
888
  for (const configPath of paths) {
733
889
  const exists = fs4.existsSync(configPath);
734
890
  const parentExists = fs4.existsSync(path6.dirname(configPath));
735
891
  if (exists || parentExists) {
736
- const configured2 = exists && isPiutConfigured(configPath, tool.configKey);
892
+ const configured2 = exists && !!tool.configKey && isPiutConfigured(configPath, tool.configKey);
737
893
  let staleKey = false;
738
- if (configured2) {
894
+ if (configured2 && tool.configKey) {
739
895
  const piutConfig = getPiutConfig(configPath, tool.configKey);
740
896
  if (piutConfig) {
741
897
  const existingKey = extractKeyFromConfig(piutConfig);
742
898
  if (existingKey && existingKey !== apiKey) {
743
899
  staleKey = true;
744
900
  }
901
+ const existingSlug = extractSlugFromConfig(piutConfig);
902
+ if (existingSlug && existingSlug !== slug) {
903
+ staleKey = true;
904
+ }
745
905
  }
746
906
  }
747
907
  detected.push({
@@ -757,7 +917,6 @@ async function setupCommand(options) {
757
917
  }
758
918
  if (detected.length === 0) {
759
919
  console.log(warning(" No supported AI tools detected."));
760
- console.log(dim(" Supported: Claude Code, Claude Desktop, Cursor, Windsurf, GitHub Copilot, Amazon Q, Zed"));
761
920
  console.log(dim(" See https://piut.com/docs for manual setup."));
762
921
  console.log();
763
922
  return;
@@ -809,7 +968,7 @@ async function setupCommand(options) {
809
968
  try {
810
969
  execSync(tool.quickCommand(slug, apiKey), { stdio: "pipe" });
811
970
  const claudeJson = expandPath("~/.claude.json");
812
- const written = getPiutConfig(claudeJson, tool.configKey);
971
+ const written = tool.configKey ? getPiutConfig(claudeJson, tool.configKey) : null;
813
972
  if (written) {
814
973
  quickSuccess = true;
815
974
  configured.push(tool.name);
@@ -818,7 +977,7 @@ async function setupCommand(options) {
818
977
  }
819
978
  try {
820
979
  execSync(tool.quickCommand(slug, apiKey) + " --scope user", { stdio: "pipe" });
821
- const retryCheck = getPiutConfig(claudeJson, tool.configKey);
980
+ const retryCheck = tool.configKey ? getPiutConfig(claudeJson, tool.configKey) : null;
822
981
  if (retryCheck) {
823
982
  quickSuccess = true;
824
983
  configured.push(tool.name);
@@ -837,10 +996,12 @@ async function setupCommand(options) {
837
996
  }
838
997
  if (quickSuccess) continue;
839
998
  }
840
- const serverConfig = tool.generateConfig(slug, apiKey);
841
- mergeConfig(configPath, tool.configKey, serverConfig);
842
- configured.push(tool.name);
843
- toolLine(tool.name, success("configured"), "\u2714");
999
+ if (tool.generateConfig && tool.configKey) {
1000
+ const serverConfig = tool.generateConfig(slug, apiKey);
1001
+ mergeConfig(configPath, tool.configKey, serverConfig);
1002
+ configured.push(tool.name);
1003
+ toolLine(tool.name, success("configured"), "\u2714");
1004
+ }
844
1005
  }
845
1006
  if (!options.skipSkill && configured.length > 0) {
846
1007
  const addSkill = options.yes ? true : await confirm({
@@ -881,6 +1042,7 @@ async function setupCommand(options) {
881
1042
  console.log(dim(" Verifying..."));
882
1043
  for (const det of selected) {
883
1044
  if (!configured.includes(det.tool.name)) continue;
1045
+ if (!det.tool.configKey) continue;
884
1046
  const piutConfig = getPiutConfig(det.configPath, det.tool.configKey);
885
1047
  if (piutConfig) {
886
1048
  toolLine(det.tool.name, success("config verified"), "\u2714");
@@ -922,15 +1084,463 @@ function isCommandAvailable(cmd) {
922
1084
  }
923
1085
 
924
1086
  // src/commands/status.ts
925
- import fs7 from "fs";
926
- import path9 from "path";
1087
+ import fs9 from "fs";
1088
+ import os7 from "os";
1089
+ import path12 from "path";
927
1090
  import chalk3 from "chalk";
928
1091
 
929
1092
  // src/lib/brain-scanner.ts
930
- import fs5 from "fs";
1093
+ import fs7 from "fs";
1094
+ import path10 from "path";
1095
+ import os5 from "os";
1096
+
1097
+ // src/lib/file-types.ts
931
1098
  import path7 from "path";
1099
+ var DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([
1100
+ ".pdf",
1101
+ ".docx",
1102
+ ".doc",
1103
+ ".pptx",
1104
+ ".pages",
1105
+ ".key",
1106
+ ".rtf",
1107
+ ".odt",
1108
+ ".odp",
1109
+ ".eml",
1110
+ ".mbox"
1111
+ ]);
1112
+ var PLAIN_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
1113
+ ".md",
1114
+ ".markdown",
1115
+ ".txt",
1116
+ ".csv",
1117
+ ".xml",
1118
+ ".html",
1119
+ ".htm",
1120
+ ".json",
1121
+ ".yaml",
1122
+ ".yml",
1123
+ ".toml",
1124
+ ".rst",
1125
+ ".adoc",
1126
+ ".tex",
1127
+ ".ini",
1128
+ ".cfg",
1129
+ ".conf",
1130
+ ".log"
1131
+ ]);
1132
+ var AI_CONFIG_FILENAMES = /* @__PURE__ */ new Set([
1133
+ "CLAUDE.md",
1134
+ ".cursorrules",
1135
+ ".windsurfrules",
1136
+ ".rules",
1137
+ ".clinerules",
1138
+ "AGENTS.md",
1139
+ "CONVENTIONS.md",
1140
+ "MEMORY.md",
1141
+ "SOUL.md",
1142
+ "IDENTITY.md",
1143
+ "copilot-instructions.md"
1144
+ ]);
1145
+ var PARSEABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1146
+ ...DOCUMENT_EXTENSIONS,
1147
+ ...PLAIN_TEXT_EXTENSIONS
1148
+ ]);
1149
+ var EXCLUDED_EXTENSIONS = /* @__PURE__ */ new Set([
1150
+ ".js",
1151
+ ".ts",
1152
+ ".jsx",
1153
+ ".tsx",
1154
+ ".mjs",
1155
+ ".cjs",
1156
+ ".py",
1157
+ ".pyw",
1158
+ ".pyi",
1159
+ ".rs",
1160
+ ".go",
1161
+ ".java",
1162
+ ".kt",
1163
+ ".scala",
1164
+ ".clj",
1165
+ ".c",
1166
+ ".cpp",
1167
+ ".cc",
1168
+ ".h",
1169
+ ".hpp",
1170
+ ".cs",
1171
+ ".rb",
1172
+ ".php",
1173
+ ".swift",
1174
+ ".m",
1175
+ ".mm",
1176
+ ".lua",
1177
+ ".r",
1178
+ ".jl",
1179
+ ".zig",
1180
+ ".nim",
1181
+ ".v",
1182
+ ".sh",
1183
+ ".bash",
1184
+ ".zsh",
1185
+ ".fish",
1186
+ ".ps1",
1187
+ ".bat",
1188
+ ".cmd",
1189
+ ".sql",
1190
+ ".graphql",
1191
+ ".gql",
1192
+ ".proto",
1193
+ ".css",
1194
+ ".scss",
1195
+ ".sass",
1196
+ ".less",
1197
+ ".styl",
1198
+ ".xls",
1199
+ ".xlsx",
1200
+ ".numbers",
1201
+ ".sqlite",
1202
+ ".db",
1203
+ ".dat",
1204
+ ".parquet",
1205
+ ".avro",
1206
+ ".png",
1207
+ ".jpg",
1208
+ ".jpeg",
1209
+ ".gif",
1210
+ ".svg",
1211
+ ".ico",
1212
+ ".webp",
1213
+ ".bmp",
1214
+ ".tiff",
1215
+ ".heic",
1216
+ ".mp3",
1217
+ ".mp4",
1218
+ ".wav",
1219
+ ".aac",
1220
+ ".flac",
1221
+ ".ogg",
1222
+ ".avi",
1223
+ ".mov",
1224
+ ".mkv",
1225
+ ".wmv",
1226
+ ".webm",
1227
+ ".zip",
1228
+ ".tar",
1229
+ ".gz",
1230
+ ".bz2",
1231
+ ".xz",
1232
+ ".7z",
1233
+ ".rar",
1234
+ ".dmg",
1235
+ ".iso",
1236
+ ".exe",
1237
+ ".dll",
1238
+ ".so",
1239
+ ".dylib",
1240
+ ".o",
1241
+ ".a",
1242
+ ".wasm",
1243
+ ".class",
1244
+ ".jar",
1245
+ ".pyc",
1246
+ ".pyo",
1247
+ ".ttf",
1248
+ ".otf",
1249
+ ".woff",
1250
+ ".woff2",
1251
+ ".eot",
1252
+ ".lock",
1253
+ ".map",
1254
+ ".min.js",
1255
+ ".min.css"
1256
+ ]);
1257
+ function canParse(filename) {
1258
+ return PARSEABLE_EXTENSIONS.has(path7.extname(filename).toLowerCase());
1259
+ }
1260
+ function isAiConfigFile(filename) {
1261
+ return AI_CONFIG_FILENAMES.has(path7.basename(filename));
1262
+ }
1263
+ function getFileCategory(filename) {
1264
+ const base = path7.basename(filename);
1265
+ if (AI_CONFIG_FILENAMES.has(base)) return "config";
1266
+ const ext = path7.extname(filename).toLowerCase();
1267
+ if (DOCUMENT_EXTENSIONS.has(ext)) return "document";
1268
+ if (PLAIN_TEXT_EXTENSIONS.has(ext)) return "text";
1269
+ if (EXCLUDED_EXTENSIONS.has(ext)) return "excluded";
1270
+ return "unknown";
1271
+ }
1272
+
1273
+ // src/lib/file-parsers.ts
1274
+ import fs5 from "fs";
1275
+ import path8 from "path";
1276
+ var MAX_RAW_SIZE = 500 * 1024;
1277
+ var MAX_EXTRACTED_TEXT = 100 * 1024;
1278
+ function truncate(text) {
1279
+ if (text.length <= MAX_EXTRACTED_TEXT) return text;
1280
+ return text.slice(0, MAX_EXTRACTED_TEXT);
1281
+ }
1282
+ function stripHtml(html) {
1283
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\n{3,}/g, "\n\n").trim();
1284
+ }
1285
+ async function parsePdf(buffer) {
1286
+ const { PDFParse } = await import("pdf-parse");
1287
+ const parser = new PDFParse({ data: new Uint8Array(buffer) });
1288
+ const result = await parser.getText();
1289
+ return result.text;
1290
+ }
1291
+ async function parseDocx(buffer) {
1292
+ const { extractRawText } = await import("mammoth");
1293
+ const result = await extractRawText({ buffer });
1294
+ return result.value;
1295
+ }
1296
+ async function parsePptx(buffer) {
1297
+ const JSZip = (await import("jszip")).default;
1298
+ const zip = await JSZip.loadAsync(buffer);
1299
+ const texts = [];
1300
+ const slideFiles = Object.keys(zip.files).filter((name) => /^ppt\/slides\/slide\d+\.xml$/i.test(name)).sort((a, b) => {
1301
+ const numA = parseInt(a.match(/slide(\d+)/)?.[1] || "0");
1302
+ const numB = parseInt(b.match(/slide(\d+)/)?.[1] || "0");
1303
+ return numA - numB;
1304
+ });
1305
+ for (const slidePath of slideFiles) {
1306
+ const xml = await zip.files[slidePath].async("string");
1307
+ const matches = xml.match(/<a:t[^>]*>([^<]*)<\/a:t>/g);
1308
+ if (matches) {
1309
+ texts.push(matches.map((m) => m.replace(/<[^>]+>/g, "")).join(" "));
1310
+ }
1311
+ }
1312
+ return texts.join("\n\n");
1313
+ }
1314
+ async function parseAppleDoc(buffer) {
1315
+ const JSZip = (await import("jszip")).default;
1316
+ const zip = await JSZip.loadAsync(buffer);
1317
+ if (zip.files["preview.pdf"]) {
1318
+ const pdfBuf = await zip.files["preview.pdf"].async("nodebuffer");
1319
+ try {
1320
+ return await parsePdf(pdfBuf);
1321
+ } catch {
1322
+ }
1323
+ }
1324
+ const texts = [];
1325
+ for (const [name, file] of Object.entries(zip.files)) {
1326
+ if (name.endsWith(".iwa") && !file.dir) {
1327
+ try {
1328
+ const buf = await file.async("nodebuffer");
1329
+ const str = buf.toString("utf-8");
1330
+ const readable = str.match(/[\x20-\x7E\xC0-\xFF]{4,}/g);
1331
+ if (readable) {
1332
+ texts.push(...readable.filter((s) => s.length > 10));
1333
+ }
1334
+ } catch {
1335
+ }
1336
+ }
1337
+ }
1338
+ return texts.join("\n\n");
1339
+ }
1340
+ async function parseRtf(buffer) {
1341
+ const { fromString } = await import("@iarna/rtf-to-html");
1342
+ return new Promise((resolve, reject) => {
1343
+ fromString(buffer.toString("utf-8"), (err, html) => {
1344
+ if (err) {
1345
+ reject(err);
1346
+ return;
1347
+ }
1348
+ resolve(stripHtml(html));
1349
+ });
1350
+ });
1351
+ }
1352
+ async function parseOpenDocument(buffer) {
1353
+ const JSZip = (await import("jszip")).default;
1354
+ const zip = await JSZip.loadAsync(buffer);
1355
+ const contentXml = zip.files["content.xml"];
1356
+ if (!contentXml) return "";
1357
+ return stripHtml(await contentXml.async("string"));
1358
+ }
1359
+ function parseEml(buffer) {
1360
+ const content = buffer.toString("utf-8");
1361
+ const boundaryMatch = content.match(/boundary="?([^\s"]+)"?/i);
1362
+ if (boundaryMatch) {
1363
+ const parts = content.split(`--${boundaryMatch[1]}`);
1364
+ for (const part of parts) {
1365
+ if (/content-type:\s*text\/plain/i.test(part)) {
1366
+ const bodyStart = part.indexOf("\n\n");
1367
+ if (bodyStart !== -1) return part.slice(bodyStart + 2).trim();
1368
+ }
1369
+ }
1370
+ }
1371
+ const headerEnd = content.indexOf("\n\n");
1372
+ if (headerEnd !== -1) {
1373
+ const body = content.slice(headerEnd + 2);
1374
+ if (!/<html/i.test(body.slice(0, 200))) return body.trim();
1375
+ return stripHtml(body);
1376
+ }
1377
+ return content.trim();
1378
+ }
1379
+ function parseMbox(buffer) {
1380
+ const content = buffer.toString("utf-8");
1381
+ const messages = content.split(/^From /m).filter(Boolean);
1382
+ const texts = [];
1383
+ for (const msg of messages.slice(0, 50)) {
1384
+ const headerEnd = msg.indexOf("\n\n");
1385
+ if (headerEnd !== -1) {
1386
+ const body = msg.slice(headerEnd + 2);
1387
+ texts.push(!/<html/i.test(body.slice(0, 200)) ? body.trim() : stripHtml(body));
1388
+ }
1389
+ }
1390
+ return texts.join("\n\n---\n\n");
1391
+ }
1392
+ async function extractTextFromFile(filePath) {
1393
+ const ext = path8.extname(filePath).toLowerCase();
1394
+ try {
1395
+ const stat = fs5.statSync(filePath);
1396
+ if (!stat.isFile()) return null;
1397
+ if (stat.size > MAX_RAW_SIZE) return null;
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ if (PLAIN_TEXT_EXTENSIONS.has(ext)) {
1402
+ try {
1403
+ return truncate(fs5.readFileSync(filePath, "utf-8"));
1404
+ } catch {
1405
+ return null;
1406
+ }
1407
+ }
1408
+ if (!DOCUMENT_EXTENSIONS.has(ext)) return null;
1409
+ try {
1410
+ const buffer = fs5.readFileSync(filePath);
1411
+ let text;
1412
+ switch (ext) {
1413
+ case ".pdf":
1414
+ text = await parsePdf(buffer);
1415
+ break;
1416
+ case ".docx":
1417
+ case ".doc":
1418
+ text = await parseDocx(buffer);
1419
+ break;
1420
+ case ".pptx":
1421
+ text = await parsePptx(buffer);
1422
+ break;
1423
+ case ".pages":
1424
+ case ".key":
1425
+ text = await parseAppleDoc(buffer);
1426
+ break;
1427
+ case ".rtf":
1428
+ text = await parseRtf(buffer);
1429
+ break;
1430
+ case ".odt":
1431
+ case ".odp":
1432
+ text = await parseOpenDocument(buffer);
1433
+ break;
1434
+ case ".eml":
1435
+ text = parseEml(buffer);
1436
+ break;
1437
+ case ".mbox":
1438
+ text = parseMbox(buffer);
1439
+ break;
1440
+ default:
1441
+ return null;
1442
+ }
1443
+ return truncate(text);
1444
+ } catch {
1445
+ return null;
1446
+ }
1447
+ }
1448
+ function formatSize(bytes) {
1449
+ if (bytes < 1024) return `${bytes} B`;
1450
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1451
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1452
+ }
1453
+
1454
+ // src/lib/folder-tree.ts
1455
+ import fs6 from "fs";
1456
+ import path9 from "path";
932
1457
  import os4 from "os";
933
1458
  var home = os4.homedir();
1459
+ function displayPath(p) {
1460
+ return p.startsWith(home) ? "~" + p.slice(home.length) : p;
1461
+ }
1462
+ function groupFilesByFolder(files) {
1463
+ const map = /* @__PURE__ */ new Map();
1464
+ for (const file of files) {
1465
+ const folder = file.folder;
1466
+ if (!map.has(folder)) map.set(folder, []);
1467
+ map.get(folder).push(file);
1468
+ }
1469
+ const results = [];
1470
+ for (const [folderPath, folderFiles] of map) {
1471
+ results.push({
1472
+ path: folderPath,
1473
+ displayPath: displayPath(folderPath),
1474
+ files: folderFiles,
1475
+ fileCount: folderFiles.length,
1476
+ totalBytes: folderFiles.reduce((sum, f) => sum + f.sizeBytes, 0)
1477
+ });
1478
+ }
1479
+ results.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
1480
+ return results;
1481
+ }
1482
+ function formatFolderChoice(folder) {
1483
+ return `${folder.displayPath} \u2014 ${folder.fileCount} file${folder.fileCount === 1 ? "" : "s"} (${formatSize(folder.totalBytes)})`;
1484
+ }
1485
+ function formatSelectionSummary(folders) {
1486
+ const totalFiles = folders.reduce((sum, f) => sum + f.fileCount, 0);
1487
+ const totalBytes = folders.reduce((sum, f) => sum + f.totalBytes, 0);
1488
+ return `${folders.length} folder${folders.length === 1 ? "" : "s"} selected (${totalFiles} file${totalFiles === 1 ? "" : "s"}, ${formatSize(totalBytes)})`;
1489
+ }
1490
+ var SKIP_HOME_DIRS = /* @__PURE__ */ new Set([
1491
+ "Library",
1492
+ "Applications",
1493
+ "Public",
1494
+ "Movies",
1495
+ "Music",
1496
+ "Pictures",
1497
+ "Templates",
1498
+ ".Trash"
1499
+ ]);
1500
+ var INCLUDE_DOT_DIRS = /* @__PURE__ */ new Set([
1501
+ ".claude",
1502
+ ".cursor",
1503
+ ".windsurf",
1504
+ ".openclaw",
1505
+ ".zed",
1506
+ ".github",
1507
+ ".amazonq",
1508
+ ".gemini",
1509
+ ".mcporter",
1510
+ ".paperclip"
1511
+ ]);
1512
+ function getDefaultScanDirs() {
1513
+ const dirs = [];
1514
+ try {
1515
+ const entries = fs6.readdirSync(home, { withFileTypes: true });
1516
+ for (const entry of entries) {
1517
+ if (!entry.isDirectory()) continue;
1518
+ if (entry.name.startsWith(".") && !INCLUDE_DOT_DIRS.has(entry.name)) continue;
1519
+ if (SKIP_HOME_DIRS.has(entry.name)) continue;
1520
+ dirs.push(path9.join(home, entry.name));
1521
+ }
1522
+ } catch {
1523
+ }
1524
+ const cloudStorage = path9.join(home, "Library", "CloudStorage");
1525
+ try {
1526
+ if (fs6.existsSync(cloudStorage) && fs6.statSync(cloudStorage).isDirectory()) {
1527
+ const entries = fs6.readdirSync(cloudStorage, { withFileTypes: true });
1528
+ for (const entry of entries) {
1529
+ if (!entry.isDirectory()) continue;
1530
+ const fullPath = path9.join(cloudStorage, entry.name);
1531
+ if (!dirs.includes(fullPath)) {
1532
+ dirs.push(fullPath);
1533
+ }
1534
+ }
1535
+ }
1536
+ } catch {
1537
+ }
1538
+ if (dirs.length === 0) dirs.push(home);
1539
+ return dirs;
1540
+ }
1541
+
1542
+ // src/lib/brain-scanner.ts
1543
+ var home2 = os5.homedir();
934
1544
  var SKIP_DIRS = /* @__PURE__ */ new Set([
935
1545
  "node_modules",
936
1546
  ".git",
@@ -952,21 +1562,6 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
952
1562
  "Cache",
953
1563
  ".piut"
954
1564
  ]);
955
- var FULL_READ_FILES = /* @__PURE__ */ new Set([
956
- "README.md",
957
- "CLAUDE.md",
958
- ".cursorrules",
959
- ".windsurfrules",
960
- ".rules",
961
- ".clinerules",
962
- "AGENTS.md",
963
- "CONVENTIONS.md",
964
- "MEMORY.md",
965
- "SOUL.md",
966
- "IDENTITY.md"
967
- ]);
968
- var MAX_FILE_SIZE = 100 * 1024;
969
- var RECENT_DAYS = 30;
970
1565
  var SCAN_DOT_DIRS = /* @__PURE__ */ new Set([
971
1566
  ".claude",
972
1567
  ".cursor",
@@ -974,57 +1569,33 @@ var SCAN_DOT_DIRS = /* @__PURE__ */ new Set([
974
1569
  ".github",
975
1570
  ".zed",
976
1571
  ".amazonq",
977
- ".vscode"
1572
+ ".vscode",
1573
+ ".gemini",
1574
+ ".openclaw",
1575
+ ".mcporter",
1576
+ ".paperclip"
978
1577
  ]);
979
1578
  function shouldSkipDir(name) {
980
- if (name.startsWith(".") && !SCAN_DOT_DIRS.has(name)) {
981
- return true;
982
- }
1579
+ if (name.startsWith(".") && !SCAN_DOT_DIRS.has(name)) return true;
983
1580
  return SKIP_DIRS.has(name);
984
1581
  }
985
- function readFileSafe(filePath) {
986
- try {
987
- const stat = fs5.statSync(filePath);
988
- if (stat.size > MAX_FILE_SIZE) return null;
989
- if (!stat.isFile()) return null;
990
- return fs5.readFileSync(filePath, "utf-8");
991
- } catch {
992
- return null;
993
- }
994
- }
995
- function getFolderTree(dir, depth = 0, maxDepth = 3) {
996
- if (depth >= maxDepth) return [];
997
- const entries = [];
998
- try {
999
- const items = fs5.readdirSync(dir, { withFileTypes: true });
1000
- for (const item of items) {
1001
- if (!item.isDirectory()) continue;
1002
- if (shouldSkipDir(item.name)) continue;
1003
- const indent = " ".repeat(depth);
1004
- entries.push(`${indent}${item.name}/`);
1005
- entries.push(...getFolderTree(path7.join(dir, item.name), depth + 1, maxDepth));
1006
- }
1007
- } catch {
1008
- }
1009
- return entries;
1010
- }
1011
1582
  function isProject(dirPath) {
1012
- return fs5.existsSync(path7.join(dirPath, ".git")) || fs5.existsSync(path7.join(dirPath, "package.json")) || fs5.existsSync(path7.join(dirPath, "Cargo.toml")) || fs5.existsSync(path7.join(dirPath, "pyproject.toml")) || fs5.existsSync(path7.join(dirPath, "go.mod"));
1583
+ return fs7.existsSync(path10.join(dirPath, ".git")) || fs7.existsSync(path10.join(dirPath, "package.json")) || fs7.existsSync(path10.join(dirPath, "Cargo.toml")) || fs7.existsSync(path10.join(dirPath, "pyproject.toml")) || fs7.existsSync(path10.join(dirPath, "go.mod"));
1013
1584
  }
1014
1585
  function buildProjectInfo(projectPath) {
1015
- const hasPkgJson = fs5.existsSync(path7.join(projectPath, "package.json"));
1586
+ const hasPkgJson = fs7.existsSync(path10.join(projectPath, "package.json"));
1016
1587
  let description = "";
1017
1588
  if (hasPkgJson) {
1018
1589
  try {
1019
- const pkg = JSON.parse(fs5.readFileSync(path7.join(projectPath, "package.json"), "utf-8"));
1590
+ const pkg = JSON.parse(fs7.readFileSync(path10.join(projectPath, "package.json"), "utf-8"));
1020
1591
  description = pkg.description || "";
1021
1592
  } catch {
1022
1593
  }
1023
1594
  }
1024
- const readmePath = path7.join(projectPath, "README.md");
1025
- if (!description && fs5.existsSync(readmePath)) {
1026
- const content = readFileSafe(readmePath);
1027
- if (content) {
1595
+ const readmePath = path10.join(projectPath, "README.md");
1596
+ if (!description && fs7.existsSync(readmePath)) {
1597
+ try {
1598
+ const content = fs7.readFileSync(readmePath, "utf-8");
1028
1599
  const lines = content.split("\n");
1029
1600
  let foundHeading = false;
1030
1601
  for (const line of lines) {
@@ -1037,18 +1608,19 @@ function buildProjectInfo(projectPath) {
1037
1608
  break;
1038
1609
  }
1039
1610
  }
1611
+ } catch {
1040
1612
  }
1041
1613
  }
1042
1614
  return {
1043
- name: path7.basename(projectPath),
1615
+ name: path10.basename(projectPath),
1044
1616
  path: projectPath,
1045
1617
  description,
1046
- hasClaudeMd: fs5.existsSync(path7.join(projectPath, "CLAUDE.md")) || fs5.existsSync(path7.join(projectPath, ".claude", "rules")),
1047
- hasCursorRules: fs5.existsSync(path7.join(projectPath, ".cursorrules")) || fs5.existsSync(path7.join(projectPath, ".cursor", "rules")),
1048
- hasWindsurfRules: fs5.existsSync(path7.join(projectPath, ".windsurfrules")) || fs5.existsSync(path7.join(projectPath, ".windsurf", "rules")),
1049
- hasCopilotInstructions: fs5.existsSync(path7.join(projectPath, ".github", "copilot-instructions.md")) || fs5.existsSync(path7.join(projectPath, ".github", "instructions")),
1050
- hasConventionsMd: fs5.existsSync(path7.join(projectPath, "CONVENTIONS.md")) || fs5.existsSync(path7.join(projectPath, ".amazonq", "rules")),
1051
- hasZedRules: fs5.existsSync(path7.join(projectPath, ".rules"))
1618
+ hasClaudeMd: fs7.existsSync(path10.join(projectPath, "CLAUDE.md")) || fs7.existsSync(path10.join(projectPath, ".claude", "rules")),
1619
+ hasCursorRules: fs7.existsSync(path10.join(projectPath, ".cursorrules")) || fs7.existsSync(path10.join(projectPath, ".cursor", "rules")),
1620
+ hasWindsurfRules: fs7.existsSync(path10.join(projectPath, ".windsurfrules")) || fs7.existsSync(path10.join(projectPath, ".windsurf", "rules")),
1621
+ hasCopilotInstructions: fs7.existsSync(path10.join(projectPath, ".github", "copilot-instructions.md")) || fs7.existsSync(path10.join(projectPath, ".github", "instructions")),
1622
+ hasConventionsMd: fs7.existsSync(path10.join(projectPath, "CONVENTIONS.md")) || fs7.existsSync(path10.join(projectPath, ".amazonq", "rules")),
1623
+ hasZedRules: fs7.existsSync(path10.join(projectPath, ".rules"))
1052
1624
  };
1053
1625
  }
1054
1626
  var MAX_PROJECT_DEPTH = 4;
@@ -1058,17 +1630,17 @@ function detectProjects(scanDirs, onProgress) {
1058
1630
  function walk(dir, depth) {
1059
1631
  if (depth > MAX_PROJECT_DEPTH) return;
1060
1632
  try {
1061
- const items = fs5.readdirSync(dir, { withFileTypes: true });
1633
+ const items = fs7.readdirSync(dir, { withFileTypes: true });
1062
1634
  for (const item of items) {
1063
1635
  if (!item.isDirectory()) continue;
1064
1636
  if (shouldSkipDir(item.name)) continue;
1065
- const fullPath = path7.join(dir, item.name);
1637
+ const fullPath = path10.join(dir, item.name);
1066
1638
  if (seen.has(fullPath)) continue;
1067
1639
  seen.add(fullPath);
1068
1640
  if (isProject(fullPath)) {
1069
1641
  const info = buildProjectInfo(fullPath);
1070
1642
  projects.push(info);
1071
- onProgress?.({ phase: "projects", message: `${info.name} (${fullPath.replace(home, "~")})` });
1643
+ onProgress?.({ phase: "projects", message: `${info.name} (${fullPath.replace(home2, "~")})` });
1072
1644
  } else {
1073
1645
  walk(fullPath, depth + 1);
1074
1646
  }
@@ -1081,150 +1653,150 @@ function detectProjects(scanDirs, onProgress) {
1081
1653
  }
1082
1654
  return projects;
1083
1655
  }
1656
+ var MAX_CONFIG_SIZE = 100 * 1024;
1084
1657
  function collectConfigFiles(projects, onProgress) {
1085
1658
  const configs = [];
1086
1659
  const globalPaths = [
1087
- path7.join(home, ".claude", "MEMORY.md"),
1088
- path7.join(home, ".claude", "CLAUDE.md"),
1089
- path7.join(home, ".openclaw", "workspace", "SOUL.md"),
1090
- path7.join(home, ".openclaw", "workspace", "MEMORY.md")
1660
+ path10.join(home2, ".claude", "MEMORY.md"),
1661
+ path10.join(home2, ".claude", "CLAUDE.md"),
1662
+ path10.join(home2, ".openclaw", "workspace", "SOUL.md"),
1663
+ path10.join(home2, ".openclaw", "workspace", "MEMORY.md"),
1664
+ path10.join(home2, ".gemini", "MEMORY.md"),
1665
+ path10.join(home2, ".paperclip", "IDENTITY.md")
1091
1666
  ];
1092
1667
  for (const gp of globalPaths) {
1093
- const content = readFileSafe(gp);
1094
- if (content && content.trim()) {
1095
- const name = `~/${path7.relative(home, gp)}`;
1096
- configs.push({ name, content });
1097
- onProgress?.({ phase: "configs", message: name });
1098
- }
1099
- }
1100
- for (const project of projects) {
1101
- for (const fileName of FULL_READ_FILES) {
1102
- const filePath = path7.join(project.path, fileName);
1103
- const content = readFileSafe(filePath);
1104
- if (content && content.trim()) {
1105
- const name = `${project.name}/${fileName}`;
1668
+ try {
1669
+ const stat = fs7.statSync(gp);
1670
+ if (!stat.isFile() || stat.size > MAX_CONFIG_SIZE) continue;
1671
+ const content = fs7.readFileSync(gp, "utf-8");
1672
+ if (content.trim()) {
1673
+ const name = `~/${path10.relative(home2, gp)}`;
1106
1674
  configs.push({ name, content });
1107
1675
  onProgress?.({ phase: "configs", message: name });
1108
1676
  }
1677
+ } catch {
1109
1678
  }
1110
- const pkgPath = path7.join(project.path, "package.json");
1111
- const pkgContent = readFileSafe(pkgPath);
1112
- if (pkgContent) {
1679
+ }
1680
+ for (const project of projects) {
1681
+ for (const fileName of AI_CONFIG_FILENAMES) {
1682
+ const filePath = path10.join(project.path, fileName);
1113
1683
  try {
1114
- const pkg = JSON.parse(pkgContent);
1115
- const summary = JSON.stringify({ name: pkg.name, description: pkg.description }, null, 2);
1116
- const name = `${project.name}/package.json`;
1117
- configs.push({ name, content: summary });
1118
- onProgress?.({ phase: "configs", message: name });
1684
+ const stat = fs7.statSync(filePath);
1685
+ if (!stat.isFile() || stat.size > MAX_CONFIG_SIZE) continue;
1686
+ const content = fs7.readFileSync(filePath, "utf-8");
1687
+ if (content.trim()) {
1688
+ const name = `${project.name}/${fileName}`;
1689
+ configs.push({ name, content });
1690
+ onProgress?.({ phase: "configs", message: name });
1691
+ }
1119
1692
  } catch {
1120
1693
  }
1121
1694
  }
1695
+ const pkgPath = path10.join(project.path, "package.json");
1696
+ try {
1697
+ const stat = fs7.statSync(pkgPath);
1698
+ if (stat.isFile() && stat.size <= MAX_CONFIG_SIZE) {
1699
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1700
+ const summary = JSON.stringify({ name: pkg.name, description: pkg.description }, null, 2);
1701
+ configs.push({ name: `${project.name}/package.json`, content: summary });
1702
+ onProgress?.({ phase: "configs", message: `${project.name}/package.json` });
1703
+ }
1704
+ } catch {
1705
+ }
1122
1706
  }
1123
1707
  return configs;
1124
1708
  }
1125
- function collectRecentDocs(projects, onProgress) {
1126
- const docs = [];
1127
- const cutoff = Date.now() - RECENT_DAYS * 24 * 60 * 60 * 1e3;
1709
+ var MAX_SCAN_DEPTH = 6;
1710
+ var MAX_FILES = 500;
1711
+ async function scanFilesInDirs(dirs, onProgress) {
1712
+ const files = [];
1128
1713
  const seen = /* @__PURE__ */ new Set();
1129
- for (const project of projects) {
1714
+ function walk(dir, depth) {
1715
+ if (depth > MAX_SCAN_DEPTH) return [];
1716
+ const found = [];
1130
1717
  try {
1131
- const items = fs5.readdirSync(project.path, { withFileTypes: true });
1718
+ const items = fs7.readdirSync(dir, { withFileTypes: true });
1132
1719
  for (const item of items) {
1133
- if (!item.isFile()) continue;
1134
- if (!item.name.endsWith(".md")) continue;
1135
- if (FULL_READ_FILES.has(item.name)) continue;
1136
- if (item.name.startsWith(".")) continue;
1137
- const filePath = path7.join(project.path, item.name);
1138
- if (seen.has(filePath)) continue;
1139
- seen.add(filePath);
1140
- try {
1141
- const stat = fs5.statSync(filePath);
1142
- if (stat.mtimeMs < cutoff) continue;
1143
- if (stat.size > MAX_FILE_SIZE) continue;
1144
- const content = fs5.readFileSync(filePath, "utf-8");
1145
- if (content.trim()) {
1146
- const name = `${project.name}/${item.name}`;
1147
- docs.push({ name, content });
1148
- onProgress?.({ phase: "docs", message: name });
1720
+ if (item.isDirectory()) {
1721
+ if (!shouldSkipDir(item.name)) {
1722
+ found.push(...walk(path10.join(dir, item.name), depth + 1));
1723
+ }
1724
+ } else if (item.isFile()) {
1725
+ if (canParse(item.name) && !isAiConfigFile(item.name)) {
1726
+ const fullPath = path10.join(dir, item.name);
1727
+ if (!seen.has(fullPath)) {
1728
+ seen.add(fullPath);
1729
+ found.push(fullPath);
1730
+ }
1149
1731
  }
1150
- } catch {
1151
1732
  }
1152
1733
  }
1153
1734
  } catch {
1154
1735
  }
1736
+ return found;
1155
1737
  }
1156
- return docs.slice(0, 20);
1157
- }
1158
- var SKIP_HOME_DIRS = /* @__PURE__ */ new Set([
1159
- "Library",
1160
- "Applications",
1161
- "Public",
1162
- "Movies",
1163
- "Music",
1164
- "Pictures",
1165
- "Templates",
1166
- ".Trash"
1167
- ]);
1168
- var INCLUDE_DOT_DIRS = /* @__PURE__ */ new Set([
1169
- ".claude",
1170
- ".cursor",
1171
- ".windsurf",
1172
- ".openclaw",
1173
- ".zed",
1174
- ".github",
1175
- ".amazonq"
1176
- ]);
1177
- function getDefaultScanDirs() {
1178
- const dirs = [];
1179
- try {
1180
- const entries = fs5.readdirSync(home, { withFileTypes: true });
1181
- for (const entry of entries) {
1182
- if (!entry.isDirectory()) continue;
1183
- if (entry.name.startsWith(".") && !INCLUDE_DOT_DIRS.has(entry.name)) continue;
1184
- if (SKIP_HOME_DIRS.has(entry.name)) continue;
1185
- dirs.push(path7.join(home, entry.name));
1186
- }
1187
- } catch {
1738
+ const allPaths = [];
1739
+ for (const dir of dirs) {
1740
+ onProgress?.({ phase: "scanning", message: displayPath(dir) });
1741
+ allPaths.push(...walk(dir, 0));
1742
+ if (allPaths.length > MAX_FILES) break;
1188
1743
  }
1189
- const cloudStorage = path7.join(home, "Library", "CloudStorage");
1190
- try {
1191
- if (fs5.existsSync(cloudStorage) && fs5.statSync(cloudStorage).isDirectory()) {
1192
- const entries = fs5.readdirSync(cloudStorage, { withFileTypes: true });
1193
- for (const entry of entries) {
1194
- if (!entry.isDirectory()) continue;
1195
- const fullPath = path7.join(cloudStorage, entry.name);
1196
- if (!dirs.includes(fullPath)) {
1197
- dirs.push(fullPath);
1198
- }
1744
+ const pathsToProcess = allPaths.slice(0, MAX_FILES);
1745
+ for (const filePath of pathsToProcess) {
1746
+ try {
1747
+ const stat = fs7.statSync(filePath);
1748
+ onProgress?.({ phase: "parsing", message: displayPath(filePath) });
1749
+ const content = await extractTextFromFile(filePath);
1750
+ if (content && content.trim()) {
1751
+ const category = getFileCategory(filePath);
1752
+ files.push({
1753
+ path: filePath,
1754
+ displayPath: displayPath(filePath),
1755
+ content,
1756
+ format: category === "document" ? path10.extname(filePath).slice(1) : "text",
1757
+ sizeBytes: stat.size,
1758
+ folder: path10.dirname(filePath)
1759
+ });
1199
1760
  }
1761
+ } catch {
1200
1762
  }
1201
- } catch {
1202
- }
1203
- if (dirs.length === 0) {
1204
- dirs.push(home);
1205
1763
  }
1206
- return dirs;
1764
+ return files;
1207
1765
  }
1208
- function scanForBrain(folders, onProgress) {
1209
- const scanDirs = folders || getDefaultScanDirs();
1766
+ async function scanFolders(dirs, onProgress) {
1767
+ const allFiles = await scanFilesInDirs(dirs, onProgress);
1768
+ const folders = groupFilesByFolder(allFiles);
1769
+ const projects = detectProjects(dirs, onProgress);
1770
+ const configFiles = collectConfigFiles(projects, onProgress);
1771
+ const totalFiles = allFiles.length;
1772
+ const totalBytes = allFiles.reduce((sum, f) => sum + f.sizeBytes, 0);
1773
+ return { folders, configFiles, projects, allFiles, totalFiles, totalBytes };
1774
+ }
1775
+ function buildBrainInput(scanResult, selectedFolderPaths) {
1776
+ const selectedSet = new Set(selectedFolderPaths);
1777
+ const selectedFiles = scanResult.allFiles.filter((f) => selectedSet.has(f.folder));
1210
1778
  const folderTree = [];
1211
- for (const dir of scanDirs) {
1212
- folderTree.push(`${path7.basename(dir)}/`);
1213
- folderTree.push(...getFolderTree(dir, 1));
1779
+ for (const folder of scanResult.folders) {
1780
+ if (selectedSet.has(folder.path)) {
1781
+ folderTree.push(`${folder.displayPath}/ (${folder.fileCount} files)`);
1782
+ }
1214
1783
  }
1215
- const projects = detectProjects(scanDirs, onProgress);
1216
- const configFiles = collectConfigFiles(projects, onProgress);
1217
- const recentDocuments = collectRecentDocs(projects, onProgress);
1784
+ const personalDocuments = selectedFiles.map((f) => ({
1785
+ name: f.displayPath,
1786
+ content: f.content,
1787
+ format: f.format
1788
+ }));
1218
1789
  return {
1219
1790
  summary: {
1220
1791
  folders: folderTree,
1221
- projects: projects.map((p) => ({
1792
+ projects: scanResult.projects.map((p) => ({
1222
1793
  name: p.name,
1223
- path: p.path.replace(home, "~"),
1794
+ path: p.path.replace(home2, "~"),
1224
1795
  description: p.description
1225
1796
  })),
1226
- configFiles,
1227
- recentDocuments
1797
+ configFiles: scanResult.configFiles,
1798
+ recentDocuments: [],
1799
+ personalDocuments
1228
1800
  }
1229
1801
  };
1230
1802
  }
@@ -1234,14 +1806,14 @@ function scanForProjects(folders) {
1234
1806
  }
1235
1807
 
1236
1808
  // src/lib/store.ts
1237
- import fs6 from "fs";
1238
- import path8 from "path";
1239
- import os5 from "os";
1240
- var CONFIG_DIR = path8.join(os5.homedir(), ".piut");
1241
- var CONFIG_FILE2 = path8.join(CONFIG_DIR, "config.json");
1809
+ import fs8 from "fs";
1810
+ import path11 from "path";
1811
+ import os6 from "os";
1812
+ var CONFIG_DIR = path11.join(os6.homedir(), ".piut");
1813
+ var CONFIG_FILE2 = path11.join(CONFIG_DIR, "config.json");
1242
1814
  function readStore() {
1243
1815
  try {
1244
- const raw = fs6.readFileSync(CONFIG_FILE2, "utf-8");
1816
+ const raw = fs8.readFileSync(CONFIG_FILE2, "utf-8");
1245
1817
  return JSON.parse(raw);
1246
1818
  } catch {
1247
1819
  return {};
@@ -1250,18 +1822,19 @@ function readStore() {
1250
1822
  function updateStore(updates) {
1251
1823
  const config = readStore();
1252
1824
  const updated = { ...config, ...updates };
1253
- fs6.mkdirSync(CONFIG_DIR, { recursive: true });
1254
- fs6.writeFileSync(CONFIG_FILE2, JSON.stringify(updated, null, 2) + "\n", "utf-8");
1825
+ fs8.mkdirSync(CONFIG_DIR, { recursive: true });
1826
+ fs8.writeFileSync(CONFIG_FILE2, JSON.stringify(updated, null, 2) + "\n", "utf-8");
1255
1827
  return updated;
1256
1828
  }
1257
1829
  function clearStore() {
1258
1830
  try {
1259
- fs6.unlinkSync(CONFIG_FILE2);
1831
+ fs8.unlinkSync(CONFIG_FILE2);
1260
1832
  } catch {
1261
1833
  }
1262
1834
  }
1263
1835
 
1264
1836
  // src/commands/status.ts
1837
+ var API_BASE3 = process.env.PIUT_API_BASE || "https://piut.com";
1265
1838
  var PIUT_FILES = [
1266
1839
  "CLAUDE.md",
1267
1840
  ".cursor/rules/piut.mdc",
@@ -1272,25 +1845,67 @@ var PIUT_FILES = [
1272
1845
  ];
1273
1846
  function hasPiutReference(filePath) {
1274
1847
  try {
1275
- const content = fs7.readFileSync(filePath, "utf-8");
1848
+ const content = fs9.readFileSync(filePath, "utf-8");
1276
1849
  return content.includes("p\u0131ut Context") || content.includes("piut Context");
1277
1850
  } catch {
1278
1851
  return false;
1279
1852
  }
1280
1853
  }
1854
+ async function fetchRemoteConnections(key) {
1855
+ try {
1856
+ const res = await fetch(`${API_BASE3}/api/mcp/connections`, {
1857
+ headers: { Authorization: `Bearer ${key}` }
1858
+ });
1859
+ if (!res.ok) return [];
1860
+ const data = await res.json();
1861
+ return data.connections || [];
1862
+ } catch {
1863
+ return [];
1864
+ }
1865
+ }
1866
+ async function fetchRemoteProjects(key) {
1867
+ try {
1868
+ const res = await fetch(`${API_BASE3}/api/cli/projects`, {
1869
+ headers: { Authorization: `Bearer ${key}` }
1870
+ });
1871
+ if (!res.ok) return [];
1872
+ const data = await res.json();
1873
+ return data.projects || [];
1874
+ } catch {
1875
+ return [];
1876
+ }
1877
+ }
1878
+ function machineLabel(hostname, machineId) {
1879
+ if (hostname) return hostname;
1880
+ if (machineId && machineId !== "unknown") return machineId.slice(0, 8);
1881
+ return "unknown";
1882
+ }
1883
+ function timeAgo(dateStr) {
1884
+ const diff = Date.now() - new Date(dateStr).getTime();
1885
+ const minutes = Math.floor(diff / 6e4);
1886
+ if (minutes < 1) return "just now";
1887
+ if (minutes < 60) return `${minutes}m ago`;
1888
+ const hours = Math.floor(minutes / 60);
1889
+ if (hours < 24) return `${hours}h ago`;
1890
+ const days = Math.floor(hours / 24);
1891
+ if (days < 7) return `${days}d ago`;
1892
+ return new Date(dateStr).toLocaleDateString();
1893
+ }
1281
1894
  async function statusCommand(options = {}) {
1282
1895
  banner();
1283
1896
  if (options.verify) {
1284
1897
  await verifyStatus();
1285
1898
  return;
1286
1899
  }
1287
- console.log(" AI tool configuration:");
1900
+ const thisHostname = os7.hostname();
1901
+ const thisMachineId = getMachineId2();
1902
+ console.log(` AI tools on this machine ${dim(`(${thisHostname})`)}:`);
1288
1903
  console.log();
1289
1904
  let foundAny = false;
1290
1905
  for (const tool of TOOLS) {
1291
1906
  const paths = resolveConfigPaths(tool.configPaths);
1292
1907
  for (const configPath of paths) {
1293
- if (!fs7.existsSync(configPath)) continue;
1908
+ if (!fs9.existsSync(configPath)) continue;
1294
1909
  foundAny = true;
1295
1910
  const configured = isPiutConfigured(configPath, tool.configKey);
1296
1911
  if (configured) {
@@ -1306,15 +1921,15 @@ async function statusCommand(options = {}) {
1306
1921
  console.log(dim(" Run ") + brand("piut setup") + dim(" to configure your AI tools."));
1307
1922
  }
1308
1923
  console.log();
1309
- console.log(" Connected projects:");
1924
+ console.log(` Connected projects on this machine:`);
1310
1925
  console.log();
1311
1926
  const projects = scanForProjects();
1312
1927
  let connectedCount = 0;
1313
1928
  for (const project of projects) {
1314
1929
  const connectedFiles = [];
1315
1930
  for (const file of PIUT_FILES) {
1316
- const absPath = path9.join(project.path, file);
1317
- if (fs7.existsSync(absPath) && hasPiutReference(absPath)) {
1931
+ const absPath = path12.join(project.path, file);
1932
+ if (fs9.existsSync(absPath) && hasPiutReference(absPath)) {
1318
1933
  connectedFiles.push(file);
1319
1934
  }
1320
1935
  }
@@ -1331,6 +1946,33 @@ async function statusCommand(options = {}) {
1331
1946
  console.log(dim(` ${connectedCount} project(s) connected to your brain.`));
1332
1947
  }
1333
1948
  console.log();
1949
+ const store = readStore();
1950
+ if (store.apiKey) {
1951
+ const [remoteConnections, remoteProjects] = await Promise.all([
1952
+ fetchRemoteConnections(store.apiKey),
1953
+ fetchRemoteProjects(store.apiKey)
1954
+ ]);
1955
+ const otherMachineConns = remoteConnections.filter((c) => c.machine_id !== thisMachineId);
1956
+ const otherMachineProjects = remoteProjects.filter((p) => p.machineId !== thisMachineId);
1957
+ if (otherMachineConns.length > 0 || otherMachineProjects.length > 0) {
1958
+ console.log(` Other machines:`);
1959
+ console.log();
1960
+ if (otherMachineConns.length > 0) {
1961
+ for (const conn of otherMachineConns) {
1962
+ const machine = machineLabel(conn.hostname, conn.machine_id);
1963
+ const age = timeAgo(conn.last_connected_at);
1964
+ console.log(dim(` ${conn.tool_name}`) + dim(` @${machine}`) + dim(` \u2014 ${conn.request_count} requests, ${age}`));
1965
+ }
1966
+ }
1967
+ if (otherMachineProjects.length > 0) {
1968
+ for (const proj of otherMachineProjects) {
1969
+ const machine = machineLabel(proj.hostname, proj.machineId);
1970
+ console.log(dim(` ${proj.projectName}`) + dim(` @${machine}:${proj.projectPath}`));
1971
+ }
1972
+ }
1973
+ console.log();
1974
+ }
1975
+ }
1334
1976
  }
1335
1977
  async function verifyStatus() {
1336
1978
  const store = readStore();
@@ -1360,7 +2002,7 @@ async function verifyStatus() {
1360
2002
  for (const tool of TOOLS) {
1361
2003
  const paths = resolveConfigPaths(tool.configPaths);
1362
2004
  for (const configPath of paths) {
1363
- if (!fs7.existsSync(configPath)) continue;
2005
+ if (!fs9.existsSync(configPath)) continue;
1364
2006
  const piutConfig = getPiutConfig(configPath, tool.configKey);
1365
2007
  if (!piutConfig) {
1366
2008
  toolLine(tool.name, dim("installed, not connected"), "\u25CB");
@@ -1403,15 +2045,16 @@ async function verifyStatus() {
1403
2045
  }
1404
2046
 
1405
2047
  // src/commands/remove.ts
1406
- import fs8 from "fs";
2048
+ import fs10 from "fs";
1407
2049
  import { checkbox as checkbox2, confirm as confirm2 } from "@inquirer/prompts";
1408
2050
  async function removeCommand() {
1409
2051
  banner();
1410
2052
  const configured = [];
1411
2053
  for (const tool of TOOLS) {
2054
+ if (!tool.configKey) continue;
1412
2055
  const paths = resolveConfigPaths(tool.configPaths);
1413
2056
  for (const configPath of paths) {
1414
- if (fs8.existsSync(configPath) && isPiutConfigured(configPath, tool.configKey)) {
2057
+ if (fs10.existsSync(configPath) && isPiutConfigured(configPath, tool.configKey)) {
1415
2058
  configured.push({ tool, configPath });
1416
2059
  break;
1417
2060
  }
@@ -1440,134 +2083,220 @@ async function removeCommand() {
1440
2083
  });
1441
2084
  if (!proceed) return;
1442
2085
  console.log();
2086
+ const removedNames = [];
1443
2087
  for (const { tool, configPath } of selected) {
2088
+ if (!tool.configKey) continue;
1444
2089
  const removed = removeFromConfig(configPath, tool.configKey);
1445
2090
  if (removed) {
2091
+ removedNames.push(tool.name);
1446
2092
  toolLine(tool.name, success("removed"), "\u2714");
1447
2093
  } else {
1448
2094
  toolLine(tool.name, warning("not found"), "\xD7");
1449
2095
  }
1450
2096
  }
2097
+ const store = readStore();
2098
+ if (store.apiKey && removedNames.length > 0) {
2099
+ deleteConnections(store.apiKey, removedNames).catch(() => {
2100
+ });
2101
+ }
1451
2102
  console.log();
1452
2103
  console.log(dim(" Restart your AI tools for changes to take effect."));
1453
2104
  console.log();
1454
2105
  }
1455
2106
 
1456
2107
  // src/commands/build.ts
1457
- import { select, checkbox as checkbox3, input, confirm as confirm3 } from "@inquirer/prompts";
2108
+ import { select as select2, checkbox as checkbox3, input as input2, confirm as confirm3 } from "@inquirer/prompts";
1458
2109
  import chalk5 from "chalk";
1459
- import os6 from "os";
2110
+ import os8 from "os";
1460
2111
 
1461
2112
  // src/lib/auth.ts
1462
- import { password as password2 } from "@inquirer/prompts";
2113
+ import { select, input, password as password2 } from "@inquirer/prompts";
2114
+ import { exec } from "child_process";
1463
2115
  import chalk4 from "chalk";
2116
+ function openBrowser(url) {
2117
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2118
+ exec(`${cmd} "${url}"`);
2119
+ }
2120
+ async function loginWithEmailFlow() {
2121
+ const email = await input({
2122
+ message: "Email:",
2123
+ validate: (v) => v.includes("@") || "Enter a valid email address"
2124
+ });
2125
+ const pw = await password2({
2126
+ message: "Password:",
2127
+ mask: "*"
2128
+ });
2129
+ console.log(dim(" Authenticating..."));
2130
+ const result = await loginWithEmail(email, pw);
2131
+ return {
2132
+ apiKey: result.apiKey,
2133
+ validation: {
2134
+ slug: result.slug,
2135
+ displayName: result.displayName,
2136
+ serverUrl: result.serverUrl,
2137
+ planType: result.planType,
2138
+ status: result.status,
2139
+ _contractVersion: result._contractVersion
2140
+ }
2141
+ };
2142
+ }
2143
+ async function pasteKeyFlow() {
2144
+ const apiKey = await password2({
2145
+ message: "Enter your p\u0131ut API key:",
2146
+ mask: "*",
2147
+ validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
2148
+ });
2149
+ console.log(dim(" Validating key..."));
2150
+ const validation = await validateKey(apiKey);
2151
+ return { apiKey, validation };
2152
+ }
2153
+ async function browserFlow() {
2154
+ const url = "https://piut.com/dashboard/keys";
2155
+ console.log(dim(` Opening ${brand(url)}...`));
2156
+ openBrowser(url);
2157
+ console.log(dim(" Copy your API key from the dashboard, then paste it here."));
2158
+ console.log();
2159
+ return pasteKeyFlow();
2160
+ }
2161
+ async function promptLogin() {
2162
+ const method = await select({
2163
+ message: "How would you like to connect?",
2164
+ choices: [
2165
+ {
2166
+ name: "Log in with email",
2167
+ value: "email",
2168
+ description: "Use your p\u0131ut email and password"
2169
+ },
2170
+ {
2171
+ name: "Log in with browser",
2172
+ value: "browser",
2173
+ description: "Open piut.com to get your API key"
2174
+ },
2175
+ {
2176
+ name: "Paste API key",
2177
+ value: "api-key",
2178
+ description: "Enter an API key directly"
2179
+ }
2180
+ ]
2181
+ });
2182
+ switch (method) {
2183
+ case "email":
2184
+ return loginWithEmailFlow();
2185
+ case "browser":
2186
+ return browserFlow();
2187
+ case "api-key":
2188
+ return pasteKeyFlow();
2189
+ }
2190
+ }
1464
2191
  async function resolveApiKeyWithResult(keyOption) {
1465
2192
  const config = readStore();
1466
2193
  let apiKey = keyOption || config.apiKey;
1467
- if (!apiKey) {
1468
- apiKey = await password2({
1469
- message: "Enter your p\u0131ut API key:",
1470
- mask: "*",
1471
- validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
1472
- });
2194
+ if (apiKey) {
2195
+ console.log(dim(" Validating key..."));
2196
+ let result;
2197
+ try {
2198
+ result = await validateKey(apiKey);
2199
+ } catch (err) {
2200
+ console.log(chalk4.red(` \u2717 ${err.message}`));
2201
+ console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
2202
+ throw new CliError(err.message);
2203
+ }
2204
+ const label = result.slug ? `${result.displayName} (${result.slug})` : result.displayName;
2205
+ console.log(success(` \u2713 Connected as ${label}`));
2206
+ updateStore({ apiKey });
2207
+ return { apiKey, ...result };
1473
2208
  }
1474
- console.log(dim(" Validating key..."));
1475
- let result;
1476
2209
  try {
1477
- result = await validateKey(apiKey);
2210
+ const { apiKey: newKey, validation } = await promptLogin();
2211
+ const label = validation.slug ? `${validation.displayName} (${validation.slug})` : validation.displayName;
2212
+ console.log(success(` \u2713 Connected as ${label}`));
2213
+ updateStore({ apiKey: newKey });
2214
+ return { apiKey: newKey, ...validation };
1478
2215
  } catch (err) {
1479
2216
  console.log(chalk4.red(` \u2717 ${err.message}`));
1480
- console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
1481
2217
  throw new CliError(err.message);
1482
2218
  }
1483
- const label = result.slug ? `${result.displayName} (${result.slug})` : result.displayName;
1484
- console.log(success(` \u2713 Connected as ${label}`));
1485
- updateStore({ apiKey });
1486
- return { apiKey, ...result };
1487
2219
  }
1488
2220
 
1489
2221
  // src/commands/build.ts
1490
2222
  async function buildCommand(options) {
1491
2223
  banner();
1492
2224
  const { apiKey, serverUrl } = await resolveApiKeyWithResult(options.key);
1493
- let scanFolders;
2225
+ let scanDirs;
1494
2226
  if (options.folders) {
1495
- scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
1496
- }
1497
- const cwd = process.cwd();
1498
- const cwdDisplay = cwd.replace(os6.homedir(), "~");
1499
- if (!scanFolders) {
1500
- console.log(dim(` Current directory: `) + cwdDisplay);
1501
- console.log(dim(` We'll scan for AI config files and projects here.`));
2227
+ scanDirs = options.folders.split(",").map((f) => expandPath(f.trim()));
2228
+ } else {
2229
+ console.log(dim(" \u2501\u2501\u2501 BUILD YOUR BRAIN \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
1502
2230
  console.log();
1503
- const mode = await select({
1504
- message: "How do you want to build your brain?",
1505
- choices: [
1506
- { name: `Scan this directory (${cwdDisplay})`, value: "cwd", description: "Scan current directory for projects and config files" },
1507
- { name: "Select folder(s)...", value: "folders", description: "Choose a different directory to scan" }
1508
- ]
1509
- });
1510
- if (mode === "cwd") {
1511
- scanFolders = [cwd];
1512
- } else {
1513
- const defaults = getDefaultScanDirs();
1514
- const CUSTOM_VALUE = "__custom__";
1515
- const choices = [
1516
- ...defaults.map((d) => ({ name: d.replace(os6.homedir(), "~"), value: d })),
1517
- { name: chalk5.dim("Enter a custom path..."), value: CUSTOM_VALUE }
1518
- ];
1519
- const selected = await checkbox3({
1520
- message: "Which folders should we scan?",
1521
- choices,
1522
- required: true
1523
- });
1524
- if (selected.includes(CUSTOM_VALUE)) {
1525
- const custom = await input({
1526
- message: "Enter folder path(s), comma-separated:"
1527
- });
1528
- const customPaths = custom.split(",").map((f) => expandPath(f.trim())).filter(Boolean);
1529
- scanFolders = [
1530
- ...selected.filter((v) => v !== CUSTOM_VALUE),
1531
- ...customPaths
1532
- ];
1533
- } else {
1534
- scanFolders = selected;
1535
- }
1536
- if (scanFolders.length === 0) {
1537
- console.log(chalk5.yellow(" No folders selected."));
1538
- return;
1539
- }
1540
- }
2231
+ console.log(dim(" This is a local scan only \u2014 no files leave your machine"));
2232
+ console.log(dim(" until you review and explicitly approve."));
2233
+ console.log();
2234
+ scanDirs = await selectFolders();
1541
2235
  }
2236
+ if (scanDirs.length === 0) {
2237
+ console.log(chalk5.yellow(" No folders selected."));
2238
+ return;
2239
+ }
2240
+ console.log();
2241
+ console.log(dim(" Scanning locally \u2014 no data is shared..."));
1542
2242
  console.log();
1543
- let projectCount = 0;
1544
- let configCount = 0;
1545
- let docCount = 0;
2243
+ let fileCount = 0;
1546
2244
  const onProgress = (progress) => {
1547
- if (progress.phase === "projects") {
1548
- projectCount++;
1549
- console.log(dim(` [${projectCount}] ${progress.message}`));
2245
+ if (progress.phase === "scanning") {
2246
+ console.log(dim(` ${progress.message}`));
2247
+ } else if (progress.phase === "parsing") {
2248
+ fileCount++;
2249
+ console.log(dim(` [${fileCount}] ${progress.message}`));
2250
+ } else if (progress.phase === "projects") {
2251
+ console.log(dim(` [project] ${progress.message}`));
1550
2252
  } else if (progress.phase === "configs") {
1551
- configCount++;
1552
- console.log(dim(` [${configCount}] ${progress.message}`));
1553
- } else if (progress.phase === "docs") {
1554
- docCount++;
1555
- console.log(dim(` [${docCount}] ${progress.message}`));
2253
+ console.log(dim(` [config] ${progress.message}`));
2254
+ }
2255
+ };
2256
+ const scanResult = await scanFolders(scanDirs, onProgress);
2257
+ console.log();
2258
+ console.log(success(` \u2713 Scan complete: ${scanResult.totalFiles} files found across ${scanResult.folders.length} folders (local only)`));
2259
+ console.log();
2260
+ if (scanResult.totalFiles === 0 && scanResult.configFiles.length === 0) {
2261
+ console.log(chalk5.yellow(" No parseable files found in the selected folders."));
2262
+ console.log(dim(" Try scanning a different directory, or use --folders to specify paths."));
2263
+ console.log();
2264
+ return;
2265
+ }
2266
+ let selectedFolderPaths;
2267
+ if (options.yes || scanResult.folders.length === 0) {
2268
+ selectedFolderPaths = scanResult.folders.map((f) => f.path);
2269
+ } else {
2270
+ selectedFolderPaths = await reviewFolders(scanResult);
2271
+ }
2272
+ if (selectedFolderPaths.length === 0 && scanResult.configFiles.length === 0) {
2273
+ console.log(chalk5.yellow(" No folders selected."));
2274
+ return;
2275
+ }
2276
+ const selectedFolders = scanResult.folders.filter((f) => selectedFolderPaths.includes(f.path));
2277
+ const totalSelectedFiles = selectedFolders.reduce((sum, f) => sum + f.fileCount, 0) + scanResult.configFiles.length;
2278
+ if (!options.yes) {
2279
+ console.log();
2280
+ console.log(dim(" \u2501\u2501\u2501 READY TO BUILD \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
2281
+ console.log();
2282
+ console.log(dim(` ${totalSelectedFiles} files will be sent to p\u0131ut and processed by Claude`));
2283
+ console.log(dim(" Sonnet to design your brain. File contents are used for"));
2284
+ console.log(dim(" brain generation only and are not retained."));
2285
+ console.log();
2286
+ console.log(dim(` Privacy policy: ${brand("https://piut.com/privacy")}`));
2287
+ console.log();
2288
+ const consent = await confirm3({
2289
+ message: "Send files and build your brain?",
2290
+ default: false
2291
+ });
2292
+ if (!consent) {
2293
+ console.log();
2294
+ console.log(dim(" Build cancelled. No files were sent."));
2295
+ console.log();
2296
+ return;
1556
2297
  }
1557
- };
1558
- const brainInput = scanForBrain(scanFolders, onProgress);
1559
- const projCount = brainInput.summary.projects.length;
1560
- const cfgCount = brainInput.summary.configFiles.length;
1561
- const dcCount = brainInput.summary.recentDocuments.length;
1562
- console.log();
1563
- console.log(success(` Scanned: ${projCount} projects, ${cfgCount} config files, ${dcCount} recent docs`));
1564
- console.log();
1565
- if (projCount === 0 && cfgCount === 0) {
1566
- console.log(chalk5.yellow(" No projects or config files found to build from."));
1567
- console.log(dim(" Try running from a directory with your projects, or use --folders."));
1568
- console.log();
1569
- return;
1570
2298
  }
2299
+ const brainInput = buildBrainInput(scanResult, selectedFolderPaths);
1571
2300
  const spinner = new Spinner();
1572
2301
  spinner.start("Generating brain...");
1573
2302
  try {
@@ -1672,6 +2401,78 @@ async function buildCommand(options) {
1672
2401
  throw new CliError(hint);
1673
2402
  }
1674
2403
  }
2404
+ async function selectFolders() {
2405
+ const defaults = getDefaultScanDirs();
2406
+ const homeDir = os8.homedir();
2407
+ const ALL_VALUE = "__all__";
2408
+ const CUSTOM_VALUE = "__custom__";
2409
+ const choices = [
2410
+ { name: `All home folders (~) ${chalk5.dim("(Recommended)")}`, value: ALL_VALUE },
2411
+ { name: chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), value: "__sep__", disabled: true },
2412
+ ...defaults.map((d) => ({ name: displayPath(d), value: d })),
2413
+ { name: chalk5.dim("Browse to a folder..."), value: CUSTOM_VALUE }
2414
+ ];
2415
+ const selected = await checkbox3({
2416
+ message: "Select folders to scan:",
2417
+ choices,
2418
+ required: true
2419
+ });
2420
+ let scanDirs;
2421
+ if (selected.includes(ALL_VALUE)) {
2422
+ scanDirs = defaults;
2423
+ } else {
2424
+ scanDirs = selected.filter((v) => v !== CUSTOM_VALUE);
2425
+ if (selected.includes(CUSTOM_VALUE)) {
2426
+ const custom = await input2({
2427
+ message: "Enter folder path(s), comma-separated:"
2428
+ });
2429
+ const customPaths = custom.split(",").map((f) => expandPath(f.trim())).filter(Boolean);
2430
+ scanDirs = [...scanDirs, ...customPaths];
2431
+ }
2432
+ }
2433
+ if (scanDirs.length > 0 && !selected.includes(ALL_VALUE)) {
2434
+ console.log();
2435
+ console.log(dim(" Selected:"));
2436
+ for (const d of scanDirs) {
2437
+ console.log(dim(` ${displayPath(d)}`));
2438
+ }
2439
+ const addMore = await select2({
2440
+ message: "Add more folders or continue?",
2441
+ choices: [
2442
+ { name: "Continue with scan", value: "continue" },
2443
+ { name: "Add another folder...", value: "add" }
2444
+ ]
2445
+ });
2446
+ if (addMore === "add") {
2447
+ const extra = await input2({
2448
+ message: "Enter folder path(s), comma-separated:"
2449
+ });
2450
+ const extraPaths = extra.split(",").map((f) => expandPath(f.trim())).filter(Boolean);
2451
+ scanDirs = [...scanDirs, ...extraPaths];
2452
+ }
2453
+ }
2454
+ return scanDirs;
2455
+ }
2456
+ async function reviewFolders(scanResult) {
2457
+ if (scanResult.folders.length === 0) return [];
2458
+ console.log(dim(" \u2501\u2501\u2501 REVIEW SCANNED FOLDERS \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
2459
+ console.log();
2460
+ console.log(dim(" All folders selected by default. Deselect any you want to exclude."));
2461
+ console.log();
2462
+ const choices = scanResult.folders.map((folder) => ({
2463
+ name: formatFolderChoice(folder),
2464
+ value: folder.path,
2465
+ checked: true
2466
+ }));
2467
+ const selected = await checkbox3({
2468
+ message: "Select folders to include in your brain:",
2469
+ choices
2470
+ });
2471
+ const selectedFolders = scanResult.folders.filter((f) => selected.includes(f.path));
2472
+ console.log();
2473
+ console.log(dim(` ${formatSelectionSummary(selectedFolders)}`));
2474
+ return selected;
2475
+ }
1675
2476
 
1676
2477
  // src/commands/deploy.ts
1677
2478
  import chalk6 from "chalk";
@@ -1711,33 +2512,33 @@ async function deployCommand(options) {
1711
2512
  }
1712
2513
 
1713
2514
  // src/commands/connect.ts
1714
- import fs9 from "fs";
1715
- import path10 from "path";
2515
+ import fs11 from "fs";
2516
+ import path13 from "path";
1716
2517
  import { checkbox as checkbox4 } from "@inquirer/prompts";
1717
2518
  var RULE_FILES = [
1718
2519
  {
1719
2520
  tool: "Claude Code",
1720
2521
  filePath: "CLAUDE.md",
1721
2522
  strategy: "append",
1722
- detect: (p) => p.hasClaudeMd || fs9.existsSync(path10.join(p.path, ".claude"))
2523
+ detect: (p) => p.hasClaudeMd || fs11.existsSync(path13.join(p.path, ".claude"))
1723
2524
  },
1724
2525
  {
1725
2526
  tool: "Cursor",
1726
2527
  filePath: ".cursor/rules/piut.mdc",
1727
2528
  strategy: "create",
1728
- detect: (p) => p.hasCursorRules || fs9.existsSync(path10.join(p.path, ".cursor"))
2529
+ detect: (p) => p.hasCursorRules || fs11.existsSync(path13.join(p.path, ".cursor"))
1729
2530
  },
1730
2531
  {
1731
2532
  tool: "Windsurf",
1732
2533
  filePath: ".windsurf/rules/piut.md",
1733
2534
  strategy: "create",
1734
- detect: (p) => p.hasWindsurfRules || fs9.existsSync(path10.join(p.path, ".windsurf"))
2535
+ detect: (p) => p.hasWindsurfRules || fs11.existsSync(path13.join(p.path, ".windsurf"))
1735
2536
  },
1736
2537
  {
1737
2538
  tool: "GitHub Copilot",
1738
2539
  filePath: ".github/copilot-instructions.md",
1739
2540
  strategy: "append",
1740
- detect: (p) => p.hasCopilotInstructions || fs9.existsSync(path10.join(p.path, ".github"))
2541
+ detect: (p) => p.hasCopilotInstructions || fs11.existsSync(path13.join(p.path, ".github"))
1741
2542
  },
1742
2543
  {
1743
2544
  tool: "Amazon Q",
@@ -1749,7 +2550,19 @@ var RULE_FILES = [
1749
2550
  tool: "Zed",
1750
2551
  filePath: ".zed/rules.md",
1751
2552
  strategy: "create",
1752
- detect: (p) => p.hasZedRules || fs9.existsSync(path10.join(p.path, ".zed"))
2553
+ detect: (p) => p.hasZedRules || fs11.existsSync(path13.join(p.path, ".zed"))
2554
+ },
2555
+ {
2556
+ tool: "Gemini CLI",
2557
+ filePath: "GEMINI.md",
2558
+ strategy: "append",
2559
+ detect: (p) => fs11.existsSync(path13.join(p.path, ".gemini"))
2560
+ },
2561
+ {
2562
+ tool: "Paperclip",
2563
+ filePath: "AGENTS.md",
2564
+ strategy: "append",
2565
+ detect: (p) => fs11.existsSync(path13.join(p.path, ".paperclip"))
1753
2566
  }
1754
2567
  ];
1755
2568
  var DEDICATED_FILE_CONTENT = `## p\u0131ut Context (MCP Server: piut-context)
@@ -1784,7 +2597,7 @@ Full skill reference: .piut/skill.md
1784
2597
  `;
1785
2598
  function hasPiutReference2(filePath) {
1786
2599
  try {
1787
- const content = fs9.readFileSync(filePath, "utf-8");
2600
+ const content = fs11.readFileSync(filePath, "utf-8");
1788
2601
  return content.includes("p\u0131ut Context") || content.includes("piut Context");
1789
2602
  } catch {
1790
2603
  return false;
@@ -1807,13 +2620,13 @@ async function connectCommand(options) {
1807
2620
  console.log();
1808
2621
  return;
1809
2622
  }
1810
- let scanFolders;
2623
+ let scanFolders2;
1811
2624
  if (options.folders) {
1812
- scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
2625
+ scanFolders2 = options.folders.split(",").map((f) => expandPath(f.trim()));
1813
2626
  }
1814
2627
  console.log();
1815
2628
  console.log(dim(" Scanning for projects..."));
1816
- const projects = scanForProjects(scanFolders);
2629
+ const projects = scanForProjects(scanFolders2);
1817
2630
  if (projects.length === 0) {
1818
2631
  console.log(warning(" No projects found."));
1819
2632
  console.log(dim(" Try running from a directory with your projects, or use --folders."));
@@ -1824,20 +2637,20 @@ async function connectCommand(options) {
1824
2637
  for (const project of projects) {
1825
2638
  for (const rule of RULE_FILES) {
1826
2639
  if (!rule.detect(project)) continue;
1827
- const absPath = path10.join(project.path, rule.filePath);
1828
- if (fs9.existsSync(absPath) && hasPiutReference2(absPath)) continue;
2640
+ const absPath = path13.join(project.path, rule.filePath);
2641
+ if (fs11.existsSync(absPath) && hasPiutReference2(absPath)) continue;
1829
2642
  actions.push({
1830
2643
  project,
1831
2644
  tool: rule.tool,
1832
2645
  filePath: rule.filePath,
1833
2646
  absPath,
1834
- action: rule.strategy === "create" || !fs9.existsSync(absPath) ? "create" : "append"
2647
+ action: rule.strategy === "create" || !fs11.existsSync(absPath) ? "create" : "append"
1835
2648
  });
1836
2649
  }
1837
2650
  const hasAnyAction = actions.some((a) => a.project === project);
1838
2651
  if (!hasAnyAction) {
1839
- const claudeMdPath = path10.join(project.path, "CLAUDE.md");
1840
- if (!fs9.existsSync(claudeMdPath)) {
2652
+ const claudeMdPath = path13.join(project.path, "CLAUDE.md");
2653
+ if (!fs11.existsSync(claudeMdPath)) {
1841
2654
  actions.push({
1842
2655
  project,
1843
2656
  tool: "Claude Code",
@@ -1877,7 +2690,7 @@ async function connectCommand(options) {
1877
2690
  console.log();
1878
2691
  const projectChoices = [];
1879
2692
  for (const [projectPath, projectActions] of byProject) {
1880
- const projectName = path10.basename(projectPath);
2693
+ const projectName = path13.basename(projectPath);
1881
2694
  const desc = projectActions.map((a) => {
1882
2695
  const verb = a.action === "create" ? "will create" : "will append to";
1883
2696
  return `${verb} ${a.filePath}`;
@@ -1906,15 +2719,15 @@ async function connectCommand(options) {
1906
2719
  const copilotTool = TOOLS.find((t) => t.id === "copilot");
1907
2720
  for (const projectPath of selectedPaths) {
1908
2721
  const projectActions = byProject.get(projectPath) || [];
1909
- const projectName = path10.basename(projectPath);
2722
+ const projectName = path13.basename(projectPath);
1910
2723
  writePiutConfig(projectPath, { slug, apiKey, serverUrl });
1911
2724
  await writePiutSkill(projectPath, slug, apiKey);
1912
2725
  ensureGitignored(projectPath);
1913
2726
  console.log(success(` \u2713 ${projectName}/.piut/`) + dim(" \u2014 credentials + skill"));
1914
2727
  if (copilotTool) {
1915
- const hasCopilot = fs9.existsSync(path10.join(projectPath, ".github", "copilot-instructions.md")) || fs9.existsSync(path10.join(projectPath, ".github"));
2728
+ const hasCopilot = fs11.existsSync(path13.join(projectPath, ".github", "copilot-instructions.md")) || fs11.existsSync(path13.join(projectPath, ".github"));
1916
2729
  if (hasCopilot) {
1917
- const vscodeMcpPath = path10.join(projectPath, ".vscode", "mcp.json");
2730
+ const vscodeMcpPath = path13.join(projectPath, ".vscode", "mcp.json");
1918
2731
  const serverConfig = copilotTool.generateConfig(slug, apiKey);
1919
2732
  mergeConfig(vscodeMcpPath, copilotTool.configKey, serverConfig);
1920
2733
  console.log(success(` \u2713 ${projectName}/.vscode/mcp.json`) + dim(" \u2014 Copilot MCP"));
@@ -1924,26 +2737,28 @@ async function connectCommand(options) {
1924
2737
  if (action.action === "create") {
1925
2738
  const isAppendType = RULE_FILES.find((r) => r.filePath === action.filePath)?.strategy === "append";
1926
2739
  const content = isAppendType ? PROJECT_SKILL_SNIPPET + "\n" : DEDICATED_FILE_CONTENT;
1927
- fs9.mkdirSync(path10.dirname(action.absPath), { recursive: true });
1928
- fs9.writeFileSync(action.absPath, content, "utf-8");
2740
+ fs11.mkdirSync(path13.dirname(action.absPath), { recursive: true });
2741
+ fs11.writeFileSync(action.absPath, content, "utf-8");
1929
2742
  console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 created"));
1930
2743
  } else {
1931
- fs9.appendFileSync(action.absPath, APPEND_SECTION);
2744
+ fs11.appendFileSync(action.absPath, APPEND_SECTION);
1932
2745
  console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 appended"));
1933
2746
  }
1934
2747
  connected++;
1935
2748
  }
1936
2749
  }
1937
2750
  const machineId = getMachineId();
2751
+ const hostname = getHostname();
1938
2752
  for (const projectPath of selectedPaths) {
1939
2753
  const projectActions = byProject.get(projectPath) || [];
1940
- const projectName = path10.basename(projectPath);
2754
+ const projectName = path13.basename(projectPath);
1941
2755
  const toolsDetected = [...new Set(projectActions.map((a) => a.tool))];
1942
2756
  const configFilesWritten = projectActions.map((a) => a.filePath);
1943
2757
  registerProject(apiKey, {
1944
2758
  projectName,
1945
2759
  projectPath,
1946
2760
  machineId,
2761
+ hostname,
1947
2762
  toolsDetected,
1948
2763
  configFiles: configFilesWritten
1949
2764
  }).catch(() => {
@@ -1955,8 +2770,8 @@ async function connectCommand(options) {
1955
2770
  }
1956
2771
 
1957
2772
  // src/commands/disconnect.ts
1958
- import fs10 from "fs";
1959
- import path11 from "path";
2773
+ import fs12 from "fs";
2774
+ import path14 from "path";
1960
2775
  import { checkbox as checkbox5, confirm as confirm5 } from "@inquirer/prompts";
1961
2776
  var DEDICATED_FILES = /* @__PURE__ */ new Set([
1962
2777
  ".cursor/rules/piut.mdc",
@@ -1966,11 +2781,13 @@ var DEDICATED_FILES = /* @__PURE__ */ new Set([
1966
2781
  var APPEND_FILES = [
1967
2782
  "CLAUDE.md",
1968
2783
  ".github/copilot-instructions.md",
1969
- "CONVENTIONS.md"
2784
+ "CONVENTIONS.md",
2785
+ "GEMINI.md",
2786
+ "AGENTS.md"
1970
2787
  ];
1971
2788
  function hasPiutReference3(filePath) {
1972
2789
  try {
1973
- const content = fs10.readFileSync(filePath, "utf-8");
2790
+ const content = fs12.readFileSync(filePath, "utf-8");
1974
2791
  return content.includes("p\u0131ut Context") || content.includes("piut Context");
1975
2792
  } catch {
1976
2793
  return false;
@@ -1978,7 +2795,7 @@ function hasPiutReference3(filePath) {
1978
2795
  }
1979
2796
  function removePiutSection(filePath) {
1980
2797
  try {
1981
- let content = fs10.readFileSync(filePath, "utf-8");
2798
+ let content = fs12.readFileSync(filePath, "utf-8");
1982
2799
  const patterns = [
1983
2800
  /\n*## p[ıi]ut Context[\s\S]*?(?=\n## |\n---\n|$)/g
1984
2801
  ];
@@ -1992,7 +2809,7 @@ function removePiutSection(filePath) {
1992
2809
  }
1993
2810
  if (changed) {
1994
2811
  content = content.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1995
- fs10.writeFileSync(filePath, content, "utf-8");
2812
+ fs12.writeFileSync(filePath, content, "utf-8");
1996
2813
  }
1997
2814
  return changed;
1998
2815
  } catch {
@@ -2001,18 +2818,18 @@ function removePiutSection(filePath) {
2001
2818
  }
2002
2819
  async function disconnectCommand(options) {
2003
2820
  banner();
2004
- let scanFolders;
2821
+ let scanFolders2;
2005
2822
  if (options.folders) {
2006
- scanFolders = options.folders.split(",").map((f) => expandPath(f.trim()));
2823
+ scanFolders2 = options.folders.split(",").map((f) => expandPath(f.trim()));
2007
2824
  }
2008
2825
  console.log(dim(" Scanning for connected projects..."));
2009
- const projects = scanForProjects(scanFolders);
2826
+ const projects = scanForProjects(scanFolders2);
2010
2827
  const actions = [];
2011
2828
  for (const project of projects) {
2012
- const projectName = path11.basename(project.path);
2829
+ const projectName = path14.basename(project.path);
2013
2830
  for (const dedicatedFile of DEDICATED_FILES) {
2014
- const absPath = path11.join(project.path, dedicatedFile);
2015
- if (fs10.existsSync(absPath) && hasPiutReference3(absPath)) {
2831
+ const absPath = path14.join(project.path, dedicatedFile);
2832
+ if (fs12.existsSync(absPath) && hasPiutReference3(absPath)) {
2016
2833
  actions.push({
2017
2834
  projectPath: project.path,
2018
2835
  projectName,
@@ -2023,8 +2840,8 @@ async function disconnectCommand(options) {
2023
2840
  }
2024
2841
  }
2025
2842
  for (const appendFile of APPEND_FILES) {
2026
- const absPath = path11.join(project.path, appendFile);
2027
- if (fs10.existsSync(absPath) && hasPiutReference3(absPath)) {
2843
+ const absPath = path14.join(project.path, appendFile);
2844
+ if (fs12.existsSync(absPath) && hasPiutReference3(absPath)) {
2028
2845
  actions.push({
2029
2846
  projectPath: project.path,
2030
2847
  projectName,
@@ -2039,12 +2856,12 @@ async function disconnectCommand(options) {
2039
2856
  projectPath: project.path,
2040
2857
  projectName,
2041
2858
  filePath: ".piut/",
2042
- absPath: path11.join(project.path, ".piut"),
2859
+ absPath: path14.join(project.path, ".piut"),
2043
2860
  action: "remove-dir"
2044
2861
  });
2045
2862
  }
2046
- const vscodeMcpPath = path11.join(project.path, ".vscode", "mcp.json");
2047
- if (fs10.existsSync(vscodeMcpPath) && isPiutConfigured(vscodeMcpPath, "servers")) {
2863
+ const vscodeMcpPath = path14.join(project.path, ".vscode", "mcp.json");
2864
+ if (fs12.existsSync(vscodeMcpPath) && isPiutConfigured(vscodeMcpPath, "servers")) {
2048
2865
  actions.push({
2049
2866
  projectPath: project.path,
2050
2867
  projectName,
@@ -2066,7 +2883,7 @@ async function disconnectCommand(options) {
2066
2883
  }
2067
2884
  console.log();
2068
2885
  const projectChoices = Array.from(byProject.entries()).map(([projectPath, projectActions]) => {
2069
- const name = path11.basename(projectPath);
2886
+ const name = path14.basename(projectPath);
2070
2887
  const files = projectActions.map((a) => a.filePath).join(", ");
2071
2888
  return {
2072
2889
  name: `${name} ${dim(`(${files})`)}`,
@@ -2095,11 +2912,11 @@ async function disconnectCommand(options) {
2095
2912
  let disconnected = 0;
2096
2913
  for (const projectPath of selectedPaths) {
2097
2914
  const projectActions = byProject.get(projectPath) || [];
2098
- const projectName = path11.basename(projectPath);
2915
+ const projectName = path14.basename(projectPath);
2099
2916
  for (const action of projectActions) {
2100
2917
  if (action.action === "delete") {
2101
2918
  try {
2102
- fs10.unlinkSync(action.absPath);
2919
+ fs12.unlinkSync(action.absPath);
2103
2920
  console.log(success(` \u2713 ${projectName}/${action.filePath}`) + dim(" \u2014 deleted"));
2104
2921
  disconnected++;
2105
2922
  } catch {
@@ -2142,6 +2959,21 @@ async function disconnectCommand(options) {
2142
2959
  console.log();
2143
2960
  }
2144
2961
 
2962
+ // src/commands/login.ts
2963
+ import chalk7 from "chalk";
2964
+ async function loginCommand() {
2965
+ try {
2966
+ const { apiKey, validation } = await promptLogin();
2967
+ const label = validation.slug ? `${validation.displayName} (${validation.slug})` : validation.displayName;
2968
+ console.log(success(` \u2713 Connected as ${label}`));
2969
+ updateStore({ apiKey });
2970
+ console.log(dim(" API key saved. You can now use all p\u0131ut commands."));
2971
+ } catch (err) {
2972
+ console.log(chalk7.red(` \u2717 ${err.message}`));
2973
+ throw new CliError(err.message);
2974
+ }
2975
+ }
2976
+
2145
2977
  // src/commands/logout.ts
2146
2978
  async function logoutCommand() {
2147
2979
  banner();
@@ -2159,11 +2991,11 @@ async function logoutCommand() {
2159
2991
  }
2160
2992
 
2161
2993
  // src/commands/update.ts
2162
- import chalk8 from "chalk";
2994
+ import chalk9 from "chalk";
2163
2995
 
2164
2996
  // src/lib/update-check.ts
2165
2997
  import { execFile } from "child_process";
2166
- import chalk7 from "chalk";
2998
+ import chalk8 from "chalk";
2167
2999
  import { confirm as confirm6 } from "@inquirer/prompts";
2168
3000
  var PACKAGE_NAME = "@piut/cli";
2169
3001
  function isNpx() {
@@ -2200,7 +3032,7 @@ async function checkForUpdate(currentVersion) {
2200
3032
  const updateCmd = npx ? `npx ${PACKAGE_NAME}@latest` : `npm install -g ${PACKAGE_NAME}@latest`;
2201
3033
  console.log();
2202
3034
  console.log(brand(" Update available!") + dim(` ${currentVersion} \u2192 ${latest}`));
2203
- console.log(dim(` Run ${chalk7.bold(updateCmd)} to update`));
3035
+ console.log(dim(` Run ${chalk8.bold(updateCmd)} to update`));
2204
3036
  console.log();
2205
3037
  if (npx) return;
2206
3038
  try {
@@ -2212,12 +3044,12 @@ async function checkForUpdate(currentVersion) {
2212
3044
  console.log(dim(" Updating..."));
2213
3045
  const ok = await runUpdate();
2214
3046
  if (ok) {
2215
- console.log(chalk7.green(` \u2713 Updated to v${latest}`));
3047
+ console.log(chalk8.green(` \u2713 Updated to v${latest}`));
2216
3048
  console.log(dim(" Restart the CLI to use the new version."));
2217
3049
  process.exit(0);
2218
3050
  } else {
2219
- console.log(chalk7.yellow(` Could not auto-update. Run manually:`));
2220
- console.log(chalk7.bold(` npm install -g ${PACKAGE_NAME}@latest`));
3051
+ console.log(chalk8.yellow(` Could not auto-update. Run manually:`));
3052
+ console.log(chalk8.bold(` npm install -g ${PACKAGE_NAME}@latest`));
2221
3053
  console.log();
2222
3054
  }
2223
3055
  }
@@ -2232,7 +3064,7 @@ async function updateCommand(currentVersion) {
2232
3064
  console.log(dim(" Checking for updates..."));
2233
3065
  const latest = await getLatestVersion();
2234
3066
  if (!latest) {
2235
- console.log(chalk8.yellow(" Could not reach the npm registry. Check your connection."));
3067
+ console.log(chalk9.yellow(" Could not reach the npm registry. Check your connection."));
2236
3068
  return;
2237
3069
  }
2238
3070
  if (!isNewer(currentVersion, latest)) {
@@ -2244,7 +3076,7 @@ async function updateCommand(currentVersion) {
2244
3076
  if (isNpx()) {
2245
3077
  console.log();
2246
3078
  console.log(dim(" You're running via npx. Use the latest version with:"));
2247
- console.log(chalk8.bold(` npx ${PACKAGE_NAME2}@latest`));
3079
+ console.log(chalk9.bold(` npx ${PACKAGE_NAME2}@latest`));
2248
3080
  console.log();
2249
3081
  return;
2250
3082
  }
@@ -2254,14 +3086,14 @@ async function updateCommand(currentVersion) {
2254
3086
  console.log(success(` \u2713 Updated to v${latest}`));
2255
3087
  console.log(dim(" Restart the CLI to use the new version."));
2256
3088
  } else {
2257
- console.log(chalk8.yellow(" Could not auto-update. Run manually:"));
2258
- console.log(chalk8.bold(` npm install -g ${PACKAGE_NAME2}@latest`));
3089
+ console.log(chalk9.yellow(" Could not auto-update. Run manually:"));
3090
+ console.log(chalk9.bold(` npm install -g ${PACKAGE_NAME2}@latest`));
2259
3091
  }
2260
3092
  }
2261
3093
 
2262
3094
  // src/commands/doctor.ts
2263
- import fs11 from "fs";
2264
- import chalk9 from "chalk";
3095
+ import fs13 from "fs";
3096
+ import chalk10 from "chalk";
2265
3097
  async function doctorCommand(options) {
2266
3098
  if (!options.json) banner();
2267
3099
  const result = {
@@ -2312,7 +3144,7 @@ async function doctorCommand(options) {
2312
3144
  for (const tool of TOOLS) {
2313
3145
  const paths = resolveConfigPaths(tool.configPaths);
2314
3146
  for (const configPath of paths) {
2315
- if (!fs11.existsSync(configPath)) continue;
3147
+ if (!fs13.existsSync(configPath)) continue;
2316
3148
  const piutConfig = getPiutConfig(configPath, tool.configKey);
2317
3149
  if (!piutConfig) {
2318
3150
  result.tools.push({
@@ -2417,59 +3249,155 @@ async function doctorCommand(options) {
2417
3249
  const staleTools = result.tools.filter((t) => t.keyMatch === "stale" && !t.fixed);
2418
3250
  if (staleTools.length > 0 && !options.fix) {
2419
3251
  console.log();
2420
- console.log(dim(" Fix stale configs: ") + chalk9.cyan("piut doctor --fix"));
3252
+ console.log(dim(" Fix stale configs: ") + chalk10.cyan("piut doctor --fix"));
2421
3253
  }
2422
3254
  if (!result.key.valid) {
2423
- console.log(dim(" Set a valid key: ") + chalk9.cyan("piut setup --key pb_YOUR_KEY"));
3255
+ console.log(dim(" Set a valid key: ") + chalk10.cyan("piut setup --key pb_YOUR_KEY"));
2424
3256
  }
2425
3257
  }
2426
3258
  console.log();
2427
3259
  if (result.issues > 0) throw new CliError(`${result.issues} issue(s) found`);
2428
3260
  }
2429
3261
 
3262
+ // src/commands/vault.ts
3263
+ import fs14 from "fs";
3264
+ import path15 from "path";
3265
+ import chalk11 from "chalk";
3266
+ import { confirm as confirm7 } from "@inquirer/prompts";
3267
+ function formatSize2(bytes) {
3268
+ if (bytes < 1024) return `${bytes} B`;
3269
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3270
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3271
+ }
3272
+ function resolveApiKey(options) {
3273
+ const key = options.key || readStore().apiKey;
3274
+ if (!key) {
3275
+ console.log(warning(" No API key found."));
3276
+ console.log(dim(" Run ") + brand("piut login") + dim(" first, or pass --key."));
3277
+ throw new CliError();
3278
+ }
3279
+ return key;
3280
+ }
3281
+ async function vaultListCommand(options) {
3282
+ const key = resolveApiKey(options);
3283
+ const data = await listVaultFiles(key);
3284
+ if (data.files.length === 0) {
3285
+ console.log(dim(" No files in vault."));
3286
+ console.log(dim(" Upload with: ") + brand("piut vault upload <file>"));
3287
+ console.log();
3288
+ return;
3289
+ }
3290
+ console.log();
3291
+ for (const file of data.files) {
3292
+ const size = dim(`(${formatSize2(file.sizeBytes)})`);
3293
+ console.log(` ${file.filename} ${size}`);
3294
+ if (file.summary) {
3295
+ console.log(dim(` ${file.summary}`));
3296
+ }
3297
+ }
3298
+ console.log();
3299
+ console.log(dim(` ${data.usage.fileCount} file(s), ${formatSize2(data.usage.totalBytes)} / ${formatSize2(data.usage.maxBytes)} used`));
3300
+ console.log();
3301
+ }
3302
+ async function vaultUploadCommand(filePath, options) {
3303
+ const key = resolveApiKey(options);
3304
+ const resolved = path15.resolve(filePath);
3305
+ if (!fs14.existsSync(resolved)) {
3306
+ console.log(chalk11.red(` File not found: ${filePath}`));
3307
+ throw new CliError();
3308
+ }
3309
+ const stat = fs14.statSync(resolved);
3310
+ if (!stat.isFile()) {
3311
+ console.log(chalk11.red(` Not a file: ${filePath}`));
3312
+ throw new CliError();
3313
+ }
3314
+ const filename = path15.basename(resolved);
3315
+ const content = fs14.readFileSync(resolved, "utf-8");
3316
+ const spinner = new Spinner();
3317
+ spinner.start(`Uploading ${filename}...`);
3318
+ try {
3319
+ const result = await uploadVaultFile(key, filename, content);
3320
+ spinner.stop();
3321
+ console.log(success(` Uploaded ${result.filename}`) + dim(` (${formatSize2(result.sizeBytes)})`));
3322
+ if (result.summary) {
3323
+ console.log(dim(` ${result.summary}`));
3324
+ }
3325
+ console.log();
3326
+ } catch (err) {
3327
+ spinner.stop();
3328
+ console.log(chalk11.red(` ${err.message}`));
3329
+ throw new CliError();
3330
+ }
3331
+ }
3332
+ async function vaultReadCommand(filename, options) {
3333
+ const key = resolveApiKey(options);
3334
+ try {
3335
+ const file = await readVaultFile(key, filename);
3336
+ if (options.output) {
3337
+ const outPath = path15.resolve(options.output);
3338
+ fs14.writeFileSync(outPath, file.content, "utf-8");
3339
+ console.log(success(` Saved to ${outPath}`));
3340
+ console.log();
3341
+ } else {
3342
+ console.log();
3343
+ console.log(file.content);
3344
+ }
3345
+ } catch (err) {
3346
+ console.log(chalk11.red(` ${err.message}`));
3347
+ throw new CliError();
3348
+ }
3349
+ }
3350
+ async function vaultDeleteCommand(filename, options) {
3351
+ const key = resolveApiKey(options);
3352
+ if (!options.yes) {
3353
+ const confirmed = await confirm7({
3354
+ message: `Delete "${filename}" from vault? This cannot be undone.`,
3355
+ default: false
3356
+ });
3357
+ if (!confirmed) return;
3358
+ }
3359
+ try {
3360
+ await deleteVaultFile(key, filename);
3361
+ console.log(success(` Deleted ${filename}`));
3362
+ console.log();
3363
+ } catch (err) {
3364
+ console.log(chalk11.red(` ${err.message}`));
3365
+ throw new CliError();
3366
+ }
3367
+ }
3368
+
2430
3369
  // src/commands/interactive.ts
2431
- import { select as select2, confirm as confirm7, checkbox as checkbox6, password as password3 } from "@inquirer/prompts";
2432
- import fs12 from "fs";
2433
- import path12 from "path";
2434
- import chalk10 from "chalk";
3370
+ import { select as select3, confirm as confirm8, checkbox as checkbox6, Separator } from "@inquirer/prompts";
3371
+ import fs15 from "fs";
3372
+ import path16 from "path";
3373
+ import { exec as exec2 } from "child_process";
3374
+ import chalk12 from "chalk";
2435
3375
  async function authenticate() {
2436
3376
  const config = readStore();
2437
- let apiKey = config.apiKey;
3377
+ const apiKey = config.apiKey;
2438
3378
  if (apiKey) {
2439
3379
  try {
2440
- const result2 = await validateKey(apiKey);
2441
- console.log(success(` Connected as ${result2.displayName}`));
2442
- return { apiKey, validation: result2 };
3380
+ const result = await validateKey(apiKey);
3381
+ console.log(success(` Connected as ${result.displayName}`));
3382
+ return { apiKey, validation: result };
2443
3383
  } catch {
2444
3384
  console.log(dim(" Saved key expired. Please re-authenticate."));
2445
- apiKey = void 0;
2446
3385
  }
2447
3386
  }
2448
- console.log(dim(" Connect to p\u0131ut:"));
2449
- console.log(dim(" > Log in at piut.com"));
2450
- console.log(dim(" > Enter p\u0131ut API key"));
2451
- console.log();
2452
- apiKey = await password3({
2453
- message: "Enter your p\u0131ut API key:",
2454
- mask: "*",
2455
- validate: (v) => v.startsWith("pb_") || "Key must start with pb_"
2456
- });
2457
- console.log(dim(" Validating key..."));
2458
- let result;
2459
- try {
2460
- result = await validateKey(apiKey);
2461
- } catch (err) {
2462
- console.log(chalk10.red(` \u2717 ${err.message}`));
2463
- console.log(dim(" Get a key at https://piut.com/dashboard/keys"));
2464
- process.exit(1);
2465
- }
2466
- console.log(success(` \u2713 Connected as ${result.displayName}`));
2467
- updateStore({ apiKey });
2468
- return { apiKey, validation: result };
3387
+ const { apiKey: newKey, validation } = await promptLogin();
3388
+ const label = validation.slug ? `${validation.displayName} (${validation.slug})` : validation.displayName;
3389
+ console.log(success(` \u2713 Connected as ${label}`));
3390
+ updateStore({ apiKey: newKey });
3391
+ return { apiKey: newKey, validation };
2469
3392
  }
2470
3393
  function isPromptCancellation(err) {
2471
3394
  return !!(err && typeof err === "object" && "name" in err && err.name === "ExitPromptError");
2472
3395
  }
3396
+ function openInBrowser(url) {
3397
+ const platform = process.platform;
3398
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
3399
+ exec2(`${cmd} ${url}`);
3400
+ }
2473
3401
  async function interactiveMenu() {
2474
3402
  banner();
2475
3403
  let apiKey;
@@ -2482,7 +3410,7 @@ async function interactiveMenu() {
2482
3410
  console.log(warning(" You haven\u2019t built a brain yet."));
2483
3411
  console.log(dim(" Your brain is how AI tools learn about you \u2014 your projects, preferences, and context."));
2484
3412
  console.log();
2485
- const wantBuild = await confirm7({
3413
+ const wantBuild = await confirm8({
2486
3414
  message: "Build your brain now?",
2487
3415
  default: true
2488
3416
  });
@@ -2498,7 +3426,7 @@ async function interactiveMenu() {
2498
3426
  console.log(warning(" Your brain is built but not deployed yet."));
2499
3427
  console.log(dim(" Deploy it to make your MCP server live so AI tools can read your brain."));
2500
3428
  console.log();
2501
- const wantDeploy = await confirm7({
3429
+ const wantDeploy = await confirm8({
2502
3430
  message: "Deploy your brain now?",
2503
3431
  default: true
2504
3432
  });
@@ -2515,49 +3443,58 @@ async function interactiveMenu() {
2515
3443
  const isDeployed = currentValidation.status === "active";
2516
3444
  let action;
2517
3445
  try {
2518
- action = await select2({
3446
+ action = await select3({
2519
3447
  message: "What would you like to do?",
3448
+ loop: false,
2520
3449
  choices: [
2521
3450
  {
2522
- name: hasBrain ? "Rebuild Brain" : "Build Brain",
3451
+ name: "My Brain",
3452
+ value: "view-brain",
3453
+ description: "View all 5 brain sections",
3454
+ disabled: !hasBrain && "(build brain first)"
3455
+ },
3456
+ {
3457
+ name: "Build Brain",
2523
3458
  value: "build",
2524
3459
  description: hasBrain ? "Rebuild your brain from your files" : "Build your brain from your files"
2525
3460
  },
3461
+ {
3462
+ name: "Edit Brain",
3463
+ value: "edit-brain",
3464
+ description: "Open piut.com to edit your brain",
3465
+ disabled: !hasBrain && "(build brain first)"
3466
+ },
3467
+ new Separator(),
2526
3468
  {
2527
3469
  name: isDeployed ? "Undeploy Brain" : "Deploy Brain",
2528
3470
  value: "deploy",
2529
3471
  description: isDeployed ? "Take your MCP server offline" : "Publish your MCP server (requires paid account)"
2530
3472
  },
3473
+ new Separator(),
2531
3474
  {
2532
3475
  name: "Connect Tools",
2533
3476
  value: "connect-tools",
2534
- description: "Configure AI tools to use your MCP server",
2535
- disabled: !isDeployed && "(deploy brain first)"
2536
- },
2537
- {
2538
- name: "Disconnect Tools",
2539
- value: "disconnect-tools",
2540
- description: "Remove p\u0131ut from AI tool configs",
3477
+ description: "Manage which AI tools use your MCP server",
2541
3478
  disabled: !isDeployed && "(deploy brain first)"
2542
3479
  },
2543
3480
  {
2544
3481
  name: "Connect Projects",
2545
3482
  value: "connect-projects",
2546
- description: "Add brain references to project config files",
3483
+ description: "Manage brain references in project config files",
2547
3484
  disabled: !isDeployed && "(deploy brain first)"
2548
3485
  },
3486
+ new Separator(),
2549
3487
  {
2550
- name: "Disconnect Projects",
2551
- value: "disconnect-projects",
2552
- description: "Remove brain references from project configs",
2553
- disabled: !isDeployed && "(deploy brain first)"
3488
+ name: "View Files",
3489
+ value: "vault-view",
3490
+ description: "List and manage files in your vault"
2554
3491
  },
2555
3492
  {
2556
- name: "View Brain",
2557
- value: "view-brain",
2558
- description: "View all 5 brain sections",
2559
- disabled: !hasBrain && "(build brain first)"
3493
+ name: "Upload Files",
3494
+ value: "vault-upload",
3495
+ description: "Upload a file to your vault"
2560
3496
  },
3497
+ new Separator(),
2561
3498
  {
2562
3499
  name: "Status",
2563
3500
  value: "status",
@@ -2581,9 +3518,15 @@ async function interactiveMenu() {
2581
3518
  if (action === "exit") return;
2582
3519
  try {
2583
3520
  switch (action) {
3521
+ case "view-brain":
3522
+ await handleViewBrain(apiKey);
3523
+ break;
2584
3524
  case "build":
2585
3525
  await buildCommand({ key: apiKey });
2586
3526
  break;
3527
+ case "edit-brain":
3528
+ handleEditBrain();
3529
+ break;
2587
3530
  case "deploy":
2588
3531
  if (isDeployed) {
2589
3532
  await handleUndeploy(apiKey);
@@ -2594,17 +3537,14 @@ async function interactiveMenu() {
2594
3537
  case "connect-tools":
2595
3538
  await handleConnectTools(apiKey, currentValidation);
2596
3539
  break;
2597
- case "disconnect-tools":
2598
- await handleDisconnectTools();
2599
- break;
2600
3540
  case "connect-projects":
2601
- await connectCommand({ key: apiKey });
3541
+ await handleManageProjects(apiKey, currentValidation);
2602
3542
  break;
2603
- case "disconnect-projects":
2604
- await disconnectCommand({});
3543
+ case "vault-view":
3544
+ await handleVaultView(apiKey);
2605
3545
  break;
2606
- case "view-brain":
2607
- await handleViewBrain(apiKey);
3546
+ case "vault-upload":
3547
+ await handleVaultUpload(apiKey);
2608
3548
  break;
2609
3549
  case "status":
2610
3550
  statusCommand();
@@ -2627,7 +3567,7 @@ async function interactiveMenu() {
2627
3567
  } else if (err instanceof CliError) {
2628
3568
  console.log();
2629
3569
  } else {
2630
- console.log(chalk10.red(` Error: ${err.message}`));
3570
+ console.log(chalk12.red(` Error: ${err.message}`));
2631
3571
  console.log();
2632
3572
  }
2633
3573
  }
@@ -2638,7 +3578,7 @@ async function interactiveMenu() {
2638
3578
  }
2639
3579
  }
2640
3580
  async function handleUndeploy(apiKey) {
2641
- const confirmed = await confirm7({
3581
+ const confirmed = await confirm8({
2642
3582
  message: "Undeploy your brain? AI tools will lose access to your MCP server.",
2643
3583
  default: false
2644
3584
  });
@@ -2650,115 +3590,297 @@ async function handleUndeploy(apiKey) {
2650
3590
  console.log(dim(" Run ") + brand("piut deploy") + dim(" to re-deploy anytime."));
2651
3591
  console.log();
2652
3592
  } catch (err) {
2653
- console.log(chalk10.red(` \u2717 ${err.message}`));
3593
+ console.log(chalk12.red(` \u2717 ${err.message}`));
2654
3594
  }
2655
3595
  }
2656
3596
  async function handleConnectTools(apiKey, validation) {
2657
3597
  const { slug } = validation;
2658
- const unconfigured = [];
2659
- const alreadyConnected = [];
3598
+ const detected = [];
2660
3599
  for (const tool of TOOLS) {
2661
3600
  const paths = resolveConfigPaths(tool.configPaths);
2662
3601
  for (const configPath of paths) {
2663
- const exists = fs12.existsSync(configPath);
2664
- const parentExists = fs12.existsSync(path12.dirname(configPath));
3602
+ const exists = fs15.existsSync(configPath);
3603
+ const parentExists = fs15.existsSync(path16.dirname(configPath));
2665
3604
  if (exists || parentExists) {
2666
- if (exists && isPiutConfigured(configPath, tool.configKey)) {
2667
- alreadyConnected.push(tool.name);
2668
- } else {
2669
- unconfigured.push({ tool, configPath });
2670
- }
3605
+ const connected = exists && !!tool.configKey && isPiutConfigured(configPath, tool.configKey);
3606
+ detected.push({ tool, configPath, connected });
2671
3607
  break;
2672
3608
  }
2673
3609
  }
2674
3610
  }
2675
- if (unconfigured.length === 0) {
2676
- if (alreadyConnected.length > 0) {
2677
- console.log(dim(" All detected tools are already connected."));
2678
- } else {
2679
- console.log(warning(" No supported AI tools detected."));
2680
- console.log(dim(" Supported: Claude Code, Claude Desktop, Cursor, Windsurf, GitHub Copilot, Amazon Q, Zed"));
2681
- }
3611
+ if (detected.length === 0) {
3612
+ console.log(warning(" No supported AI tools detected."));
2682
3613
  console.log();
2683
3614
  return;
2684
3615
  }
2685
- if (alreadyConnected.length > 0) {
2686
- console.log(dim(` Already connected: ${alreadyConnected.join(", ")}`));
3616
+ const mcpTools = detected.filter((d) => !d.tool.skillOnly);
3617
+ const skillOnlyTools = detected.filter((d) => d.tool.skillOnly);
3618
+ const connectedCount = mcpTools.filter((d) => d.connected).length;
3619
+ const availableCount = mcpTools.length - connectedCount;
3620
+ if (connectedCount > 0 || availableCount > 0) {
3621
+ const parts = [];
3622
+ if (connectedCount > 0) parts.push(`${connectedCount} connected`);
3623
+ if (availableCount > 0) parts.push(`${availableCount} available`);
3624
+ if (skillOnlyTools.length > 0) parts.push(`${skillOnlyTools.length} skill-only`);
3625
+ console.log(dim(` ${parts.join(", ")}`));
3626
+ }
3627
+ console.log();
3628
+ if (mcpTools.length === 0) {
3629
+ console.log(dim(" Detected tools are skill-only (no MCP config to manage)."));
3630
+ if (skillOnlyTools.length > 0) {
3631
+ console.log(dim(` Skill-only: ${skillOnlyTools.map((d) => d.tool.name).join(", ")}`));
3632
+ }
3633
+ console.log(dim(' Use "Connect Projects" to add skill files to your projects.'));
2687
3634
  console.log();
3635
+ return;
2688
3636
  }
2689
- const choices = unconfigured.map((u) => ({
2690
- name: u.tool.name,
2691
- value: u,
2692
- checked: true
3637
+ const choices = mcpTools.map((d) => ({
3638
+ name: `${d.tool.name}${d.connected ? dim(" (connected)") : ""}`,
3639
+ value: d,
3640
+ checked: d.connected
2693
3641
  }));
2694
3642
  const selected = await checkbox6({
2695
- message: "Select tools to connect:",
3643
+ message: "Select tools to keep connected (toggle with space):",
2696
3644
  choices
2697
3645
  });
2698
- if (selected.length === 0) {
2699
- console.log(dim(" No tools selected."));
3646
+ const toConnect = selected.filter((s) => !s.connected);
3647
+ const toDisconnect = mcpTools.filter((d) => d.connected && !selected.includes(d));
3648
+ if (toConnect.length === 0 && toDisconnect.length === 0) {
3649
+ console.log(dim(" No changes."));
3650
+ console.log();
2700
3651
  return;
2701
3652
  }
2702
3653
  console.log();
2703
- for (const { tool, configPath } of selected) {
2704
- const serverConfig = tool.generateConfig(slug, apiKey);
2705
- mergeConfig(configPath, tool.configKey, serverConfig);
2706
- toolLine(tool.name, success("connected"), "\u2714");
3654
+ for (const { tool, configPath } of toConnect) {
3655
+ if (tool.generateConfig && tool.configKey) {
3656
+ const serverConfig = tool.generateConfig(slug, apiKey);
3657
+ mergeConfig(configPath, tool.configKey, serverConfig);
3658
+ toolLine(tool.name, success("connected"), "\u2714");
3659
+ }
3660
+ }
3661
+ const removedNames = [];
3662
+ for (const { tool, configPath } of toDisconnect) {
3663
+ if (!tool.configKey) continue;
3664
+ const removed = removeFromConfig(configPath, tool.configKey);
3665
+ if (removed) {
3666
+ removedNames.push(tool.name);
3667
+ toolLine(tool.name, warning("disconnected"), "\u2714");
3668
+ }
2707
3669
  }
2708
- if (validation.serverUrl) {
3670
+ if (skillOnlyTools.length > 0) {
3671
+ console.log(dim(` Skill-only (use "Connect Projects"): ${skillOnlyTools.map((d) => d.tool.name).join(", ")}`));
3672
+ }
3673
+ if (toConnect.length > 0 && validation.serverUrl) {
2709
3674
  await Promise.all(
2710
- selected.map(({ tool }) => pingMcp(validation.serverUrl, apiKey, tool.name))
3675
+ toConnect.map(({ tool }) => pingMcp(validation.serverUrl, apiKey, tool.name))
2711
3676
  );
2712
3677
  }
3678
+ if (removedNames.length > 0) {
3679
+ deleteConnections(apiKey, removedNames).catch(() => {
3680
+ });
3681
+ }
2713
3682
  console.log();
2714
3683
  console.log(dim(" Restart your AI tools for changes to take effect."));
2715
3684
  console.log();
2716
3685
  }
2717
- async function handleDisconnectTools() {
2718
- const configured = [];
2719
- for (const tool of TOOLS) {
2720
- const paths = resolveConfigPaths(tool.configPaths);
2721
- for (const configPath of paths) {
2722
- if (fs12.existsSync(configPath) && isPiutConfigured(configPath, tool.configKey)) {
2723
- configured.push({ tool, configPath });
2724
- break;
2725
- }
2726
- }
2727
- }
2728
- if (configured.length === 0) {
2729
- console.log(dim(" p\u0131ut is not configured in any detected AI tools."));
3686
+ async function handleManageProjects(apiKey, validation) {
3687
+ const { slug, serverUrl } = validation;
3688
+ console.log(dim(" Scanning for projects..."));
3689
+ const projects = scanForProjects();
3690
+ if (projects.length === 0) {
3691
+ console.log(warning(" No projects found."));
3692
+ console.log(dim(" Try running from a directory with your projects."));
2730
3693
  console.log();
2731
3694
  return;
2732
3695
  }
2733
- const choices = configured.map((c) => ({
2734
- name: c.tool.name,
2735
- value: c
3696
+ const items = projects.map((p) => ({
3697
+ project: p,
3698
+ connected: hasPiutDir(p.path)
3699
+ }));
3700
+ const connectedCount = items.filter((i) => i.connected).length;
3701
+ const availableCount = items.length - connectedCount;
3702
+ const parts = [];
3703
+ if (connectedCount > 0) parts.push(`${connectedCount} connected`);
3704
+ if (availableCount > 0) parts.push(`${availableCount} available`);
3705
+ console.log(dim(` ${parts.join(", ")}`));
3706
+ console.log();
3707
+ const choices = items.map((i) => ({
3708
+ name: `${i.project.name}${i.connected ? dim(" (connected)") : ""}`,
3709
+ value: i,
3710
+ checked: i.connected
2736
3711
  }));
2737
3712
  const selected = await checkbox6({
2738
- message: "Select tools to disconnect:",
3713
+ message: "Select projects to keep connected (toggle with space):",
2739
3714
  choices
2740
3715
  });
2741
- if (selected.length === 0) {
2742
- console.log(dim(" No tools selected."));
3716
+ const toConnect = selected.filter((s) => !s.connected);
3717
+ const toDisconnect = items.filter((i) => i.connected && !selected.includes(i));
3718
+ if (toConnect.length === 0 && toDisconnect.length === 0) {
3719
+ console.log(dim(" No changes."));
3720
+ console.log();
2743
3721
  return;
2744
3722
  }
2745
- const proceed = await confirm7({
2746
- message: `Disconnect p\u0131ut from ${selected.length} tool(s)?`,
2747
- default: false
2748
- });
2749
- if (!proceed) return;
2750
3723
  console.log();
2751
- for (const { tool, configPath } of selected) {
2752
- const removed = removeFromConfig(configPath, tool.configKey);
2753
- if (removed) {
2754
- toolLine(tool.name, success("disconnected"), "\u2714");
2755
- } else {
2756
- toolLine(tool.name, warning("not found"), "\xD7");
3724
+ const copilotTool = TOOLS.find((t) => t.id === "copilot");
3725
+ for (const { project } of toConnect) {
3726
+ const projectName = path16.basename(project.path);
3727
+ writePiutConfig(project.path, { slug, apiKey, serverUrl });
3728
+ await writePiutSkill(project.path, slug, apiKey);
3729
+ ensureGitignored(project.path);
3730
+ if (copilotTool) {
3731
+ const hasCopilot = fs15.existsSync(path16.join(project.path, ".github", "copilot-instructions.md")) || fs15.existsSync(path16.join(project.path, ".github"));
3732
+ if (hasCopilot) {
3733
+ const vscodeMcpPath = path16.join(project.path, ".vscode", "mcp.json");
3734
+ const serverConfig = copilotTool.generateConfig(slug, apiKey);
3735
+ mergeConfig(vscodeMcpPath, copilotTool.configKey, serverConfig);
3736
+ }
2757
3737
  }
3738
+ for (const rule of RULE_FILES) {
3739
+ if (!rule.detect(project)) continue;
3740
+ const absPath = path16.join(project.path, rule.filePath);
3741
+ if (fs15.existsSync(absPath) && hasPiutReference2(absPath)) continue;
3742
+ if (rule.strategy === "create" || !fs15.existsSync(absPath)) {
3743
+ const isAppendType = rule.strategy === "append";
3744
+ const content = isAppendType ? PROJECT_SKILL_SNIPPET + "\n" : DEDICATED_FILE_CONTENT;
3745
+ fs15.mkdirSync(path16.dirname(absPath), { recursive: true });
3746
+ fs15.writeFileSync(absPath, content, "utf-8");
3747
+ } else {
3748
+ fs15.appendFileSync(absPath, APPEND_SECTION);
3749
+ }
3750
+ }
3751
+ toolLine(projectName, success("connected"), "\u2714");
3752
+ const machineId = getMachineId();
3753
+ const toolsDetected = RULE_FILES.filter((r) => r.detect(project)).map((r) => r.tool);
3754
+ registerProject(apiKey, {
3755
+ projectName,
3756
+ projectPath: project.path,
3757
+ machineId,
3758
+ toolsDetected,
3759
+ configFiles: RULE_FILES.filter((r) => r.detect(project)).map((r) => r.filePath)
3760
+ }).catch(() => {
3761
+ });
3762
+ }
3763
+ for (const { project } of toDisconnect) {
3764
+ const projectName = path16.basename(project.path);
3765
+ for (const dedicatedFile of DEDICATED_FILES) {
3766
+ const absPath = path16.join(project.path, dedicatedFile);
3767
+ if (fs15.existsSync(absPath) && hasPiutReference2(absPath)) {
3768
+ try {
3769
+ fs15.unlinkSync(absPath);
3770
+ } catch {
3771
+ }
3772
+ }
3773
+ }
3774
+ for (const appendFile of APPEND_FILES) {
3775
+ const absPath = path16.join(project.path, appendFile);
3776
+ if (fs15.existsSync(absPath) && hasPiutReference2(absPath)) {
3777
+ removePiutSection(absPath);
3778
+ }
3779
+ }
3780
+ const vscodeMcpPath = path16.join(project.path, ".vscode", "mcp.json");
3781
+ if (fs15.existsSync(vscodeMcpPath) && isPiutConfigured(vscodeMcpPath, "servers")) {
3782
+ removeFromConfig(vscodeMcpPath, "servers");
3783
+ }
3784
+ removePiutDir(project.path);
3785
+ toolLine(projectName, warning("disconnected"), "\u2714");
3786
+ const machineId = getMachineId();
3787
+ unregisterProject(apiKey, project.path, machineId).catch(() => {
3788
+ });
2758
3789
  }
2759
3790
  console.log();
2760
- console.log(dim(" Restart your AI tools for changes to take effect."));
3791
+ if (toConnect.length > 0) {
3792
+ console.log(success(` ${toConnect.length} project(s) connected.`));
3793
+ }
3794
+ if (toDisconnect.length > 0) {
3795
+ console.log(success(` ${toDisconnect.length} project(s) disconnected.`));
3796
+ }
3797
+ console.log();
3798
+ }
3799
+ function handleEditBrain() {
3800
+ console.log(dim(" Opening piut.com/dashboard..."));
3801
+ openInBrowser("https://piut.com/dashboard");
3802
+ console.log(success(" \u2713 Opened in browser."));
3803
+ console.log();
3804
+ }
3805
+ function formatSize3(bytes) {
3806
+ if (bytes < 1024) return `${bytes} B`;
3807
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3808
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3809
+ }
3810
+ async function handleVaultView(apiKey) {
3811
+ const data = await listVaultFiles(apiKey);
3812
+ if (data.files.length === 0) {
3813
+ console.log(dim(' Vault is empty. Use "Upload Files" to add files.'));
3814
+ console.log();
3815
+ return;
3816
+ }
3817
+ console.log();
3818
+ for (const file of data.files) {
3819
+ const size = dim(`(${formatSize3(file.sizeBytes)})`);
3820
+ console.log(` ${file.filename} ${size}`);
3821
+ if (file.summary) console.log(dim(` ${file.summary}`));
3822
+ }
3823
+ console.log();
3824
+ console.log(dim(` ${data.usage.fileCount} file(s), ${formatSize3(data.usage.totalBytes)} / ${formatSize3(data.usage.maxBytes)} used`));
2761
3825
  console.log();
3826
+ const action = await select3({
3827
+ message: "Actions:",
3828
+ choices: [
3829
+ { name: "Delete a file", value: "delete" },
3830
+ { name: "Back", value: "back" }
3831
+ ]
3832
+ });
3833
+ if (action === "back") return;
3834
+ if (action === "delete") {
3835
+ const fileChoices = data.files.map((f) => ({
3836
+ name: `${f.filename} ${dim(`(${formatSize3(f.sizeBytes)})`)}`,
3837
+ value: f.filename
3838
+ }));
3839
+ const filename = await select3({
3840
+ message: "Which file to delete?",
3841
+ choices: fileChoices
3842
+ });
3843
+ const confirmed = await confirm8({
3844
+ message: `Delete "${filename}"? This cannot be undone.`,
3845
+ default: false
3846
+ });
3847
+ if (confirmed) {
3848
+ try {
3849
+ await deleteVaultFile(apiKey, filename);
3850
+ console.log(success(` Deleted ${filename}`));
3851
+ console.log();
3852
+ } catch (err) {
3853
+ console.log(chalk12.red(` ${err.message}`));
3854
+ console.log();
3855
+ }
3856
+ }
3857
+ }
3858
+ }
3859
+ async function handleVaultUpload(apiKey) {
3860
+ const { input: input3 } = await import("@inquirer/prompts");
3861
+ const filePath = await input3({ message: "File path:" });
3862
+ if (!filePath.trim()) return;
3863
+ const resolved = path16.resolve(filePath);
3864
+ if (!fs15.existsSync(resolved) || !fs15.statSync(resolved).isFile()) {
3865
+ console.log(chalk12.red(` File not found: ${filePath}`));
3866
+ console.log();
3867
+ return;
3868
+ }
3869
+ const filename = path16.basename(resolved);
3870
+ const content = fs15.readFileSync(resolved, "utf-8");
3871
+ const spinner = new Spinner();
3872
+ spinner.start(`Uploading ${filename}...`);
3873
+ try {
3874
+ const result = await uploadVaultFile(apiKey, filename, content);
3875
+ spinner.stop();
3876
+ console.log(success(` Uploaded ${result.filename}`) + dim(` (${formatSize3(result.sizeBytes)})`));
3877
+ if (result.summary) console.log(dim(` ${result.summary}`));
3878
+ console.log();
3879
+ } catch (err) {
3880
+ spinner.stop();
3881
+ console.log(chalk12.red(` ${err.message}`));
3882
+ console.log();
3883
+ }
2762
3884
  }
2763
3885
  async function handleViewBrain(apiKey) {
2764
3886
  console.log(dim(" Loading brain..."));
@@ -2788,7 +3910,7 @@ async function handleViewBrain(apiKey) {
2788
3910
  if (hasUnpublishedChanges) {
2789
3911
  console.log(warning(" You have unpublished changes."));
2790
3912
  console.log();
2791
- const wantPublish = await confirm7({
3913
+ const wantPublish = await confirm8({
2792
3914
  message: "Publish now?",
2793
3915
  default: true
2794
3916
  });
@@ -2802,11 +3924,11 @@ async function handleViewBrain(apiKey) {
2802
3924
  console.log();
2803
3925
  const msg = err.message;
2804
3926
  if (msg === "REQUIRES_SUBSCRIPTION") {
2805
- console.log(chalk10.yellow(" Deploy requires an active subscription ($10/mo)."));
3927
+ console.log(chalk12.yellow(" Deploy requires an active subscription ($10/mo)."));
2806
3928
  console.log(` Subscribe at: ${brand("https://piut.com/dashboard/billing")}`);
2807
3929
  console.log(dim(" 14-day free trial included."));
2808
3930
  } else {
2809
- console.log(chalk10.red(` \u2717 ${msg}`));
3931
+ console.log(chalk12.red(` \u2717 ${msg}`));
2810
3932
  }
2811
3933
  console.log();
2812
3934
  }
@@ -2815,7 +3937,7 @@ async function handleViewBrain(apiKey) {
2815
3937
  }
2816
3938
 
2817
3939
  // src/cli.ts
2818
- var VERSION = "3.5.1";
3940
+ var VERSION = "3.7.0";
2819
3941
  function withExit(fn) {
2820
3942
  return async (...args2) => {
2821
3943
  try {
@@ -2838,8 +3960,14 @@ program.command("disconnect").description("Remove brain references from project
2838
3960
  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(withExit(setupCommand));
2839
3961
  program.command("status").description("Show brain, deployment, and connected projects").option("--verify", "Validate API key, check tool configs, and test MCP endpoint").action(withExit(statusCommand));
2840
3962
  program.command("remove").description("Remove all p\u0131ut configurations").action(withExit(removeCommand));
3963
+ program.command("login").description("Authenticate with p\u0131ut (email, browser, or API key)").action(withExit(loginCommand));
2841
3964
  program.command("logout").description("Remove saved API key").action(logoutCommand);
2842
3965
  program.command("doctor").description("Diagnose and fix connection issues").option("-k, --key <key>", "API key to verify against").option("--fix", "Auto-fix stale configurations").option("--json", "Output results as JSON").action(withExit(doctorCommand));
3966
+ var vault = program.command("vault").description("Manage your file vault (upload, list, read, delete)");
3967
+ vault.command("list").description("List all files in your vault").option("-k, --key <key>", "API key").action(withExit(vaultListCommand));
3968
+ vault.command("upload <file>").description("Upload a file to your vault").option("-k, --key <key>", "API key").action(withExit(vaultUploadCommand));
3969
+ vault.command("read <filename>").description("Read a file from your vault").option("-k, --key <key>", "API key").option("-o, --output <path>", "Save to a local file instead of printing").action(withExit(vaultReadCommand));
3970
+ vault.command("delete <filename>").description("Delete a file from your vault").option("-k, --key <key>", "API key").option("-y, --yes", "Skip confirmation prompt").action(withExit(vaultDeleteCommand));
2843
3971
  program.command("update").description("Check for and install CLI updates").action(() => updateCommand(VERSION));
2844
3972
  var args = process.argv.slice(2);
2845
3973
  if (args.includes("--version") || args.includes("-V")) {