filebread-mcp 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +344 -0
  2. package/index.js +12 -2
  3. package/package.json +6 -5
package/cli.js ADDED
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+
3
+ const readline = require("readline");
4
+ const https = require("https");
5
+ const http = require("http");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const API_URL = process.env.FILEBREAD_API_URL || "https://file-bread.fly.dev";
10
+ const command = process.argv[2];
11
+
12
+ if (!command || command === "help" || command === "--help") {
13
+ console.log(`
14
+ filebread - Give AI agents context about your project
15
+
16
+ Commands:
17
+ init Set up FileBread in the current project (creates .mcp.json)
18
+ status Check connection and indexed sources
19
+ upload Upload a file to your knowledge base
20
+
21
+ Usage:
22
+ npx filebread init
23
+ npx filebread status
24
+ npx filebread upload ./docs/architecture.md
25
+ `);
26
+ process.exit(0);
27
+ }
28
+
29
+ // --- HTTP ---
30
+
31
+ function apiRequest(method, urlPath, body, headers = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ const url = new URL(urlPath, API_URL);
34
+ const mod = url.protocol === "https:" ? https : http;
35
+ const options = {
36
+ hostname: url.hostname,
37
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
38
+ path: url.pathname,
39
+ method,
40
+ headers: { "Content-Type": "application/json", ...headers },
41
+ };
42
+ const req = mod.request(options, (res) => {
43
+ let data = "";
44
+ res.on("data", (c) => (data += c));
45
+ res.on("end", () => {
46
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
47
+ catch { resolve({ status: res.statusCode, body: data }); }
48
+ });
49
+ });
50
+ req.on("error", reject);
51
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error("timeout")); });
52
+ if (body) req.write(typeof body === "string" ? body : JSON.stringify(body));
53
+ req.end();
54
+ });
55
+ }
56
+
57
+ function ask(rl, question) {
58
+ return new Promise((resolve) => rl.question(question, resolve));
59
+ }
60
+
61
+ function askHidden(rl, question) {
62
+ return new Promise((resolve) => {
63
+ const stdin = process.stdin;
64
+ process.stdout.write(question);
65
+ const wasRaw = stdin.isRaw;
66
+ stdin.setRawMode(true);
67
+ stdin.resume();
68
+ let input = "";
69
+ const onData = (ch) => {
70
+ const c = ch.toString();
71
+ if (c === "\n" || c === "\r") {
72
+ stdin.removeListener("data", onData);
73
+ stdin.setRawMode(wasRaw);
74
+ stdin.pause();
75
+ process.stdout.write("\n");
76
+ resolve(input);
77
+ } else if (c === "\u0003") {
78
+ process.exit(0);
79
+ } else if (c === "\u007f") {
80
+ input = input.slice(0, -1);
81
+ } else {
82
+ input += c;
83
+ process.stdout.write("*");
84
+ }
85
+ };
86
+ stdin.on("data", onData);
87
+ });
88
+ }
89
+
90
+ // --- Init ---
91
+
92
+ async function init() {
93
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
94
+
95
+ console.log("\n FileBread Setup\n ───────────────\n");
96
+
97
+ // Check if .mcp.json already exists
98
+ const mcpPath = path.join(process.cwd(), ".mcp.json");
99
+ if (fs.existsSync(mcpPath)) {
100
+ const existing = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
101
+ if (existing.mcpServers?.filebread) {
102
+ console.log(" FileBread is already configured in this project.");
103
+ console.log(" Run `npx filebread status` to check the connection.\n");
104
+ rl.close();
105
+ return;
106
+ }
107
+ }
108
+
109
+ // Check for saved config
110
+ const configDir = path.join(process.env.HOME || process.env.USERPROFILE, ".filebread");
111
+ const configPath = path.join(configDir, "config.json");
112
+ let apiKey = null;
113
+
114
+ if (fs.existsSync(configPath)) {
115
+ const saved = JSON.parse(fs.readFileSync(configPath, "utf8"));
116
+ if (saved.api_key) {
117
+ console.log(` Found saved account (${saved.email || "unknown"})`);
118
+ const reuse = await ask(rl, " Use this account? (Y/n) ");
119
+ if (!reuse || reuse.toLowerCase() !== "n") {
120
+ apiKey = saved.api_key;
121
+ }
122
+ }
123
+ }
124
+
125
+ if (!apiKey) {
126
+ // Register or login
127
+ const hasAccount = await ask(rl, " Do you have a FileBread account? (y/N) ");
128
+
129
+ let email, password, token;
130
+
131
+ if (hasAccount && hasAccount.toLowerCase() === "y") {
132
+ email = await ask(rl, " Email: ");
133
+ rl.close();
134
+ password = await askHidden(readline.createInterface({ input: process.stdin, output: process.stdout }), " Password: ");
135
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
136
+
137
+ const res = await apiRequest("POST", "/api/auth/login", { email, password });
138
+ if (res.status !== 200) {
139
+ console.log(`\n Login failed: ${res.body?.error || "unknown error"}`);
140
+ rl2.close();
141
+ process.exit(1);
142
+ }
143
+ token = res.body.token;
144
+ console.log(" Logged in!\n");
145
+ rl.close = () => rl2.close();
146
+ } else {
147
+ email = await ask(rl, " Email: ");
148
+ rl.close();
149
+ password = await askHidden(readline.createInterface({ input: process.stdin, output: process.stdout }), " Password (min 8 chars): ");
150
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
151
+ const name = await ask(rl2, " Your name: ");
152
+
153
+ console.log(" Creating account...");
154
+ const res = await apiRequest("POST", "/api/auth/register", { email, password, name });
155
+ if (res.status !== 200 && res.status !== 201) {
156
+ const err = res.body?.details?.email?.[0] || res.body?.error || "unknown error";
157
+ console.log(`\n Registration failed: ${err}`);
158
+ rl2.close();
159
+ process.exit(1);
160
+ }
161
+ token = res.body.token;
162
+ console.log(" Account created!\n");
163
+ rl.close = () => rl2.close();
164
+ }
165
+
166
+ // Create API key
167
+ console.log(" Creating API key...");
168
+ const keyRes = await apiRequest("POST", "/api/api-keys", { name: `cli-${Date.now()}` }, { Authorization: `Bearer ${token}` });
169
+ if (keyRes.status !== 200 && keyRes.status !== 201) {
170
+ console.log(` Failed to create API key: ${JSON.stringify(keyRes.body)}`);
171
+ process.exit(1);
172
+ }
173
+ apiKey = keyRes.body.api_key.key;
174
+
175
+ // Save config globally
176
+ fs.mkdirSync(configDir, { recursive: true });
177
+ fs.writeFileSync(configPath, JSON.stringify({ email, api_key: apiKey }, null, 2));
178
+ fs.chmodSync(configPath, 0o600);
179
+ console.log(` API key saved to ~/.filebread/config.json\n`);
180
+ }
181
+
182
+ // Write .mcp.json
183
+ let mcpConfig = {};
184
+ if (fs.existsSync(mcpPath)) {
185
+ mcpConfig = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
186
+ }
187
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
188
+ mcpConfig.mcpServers.filebread = {
189
+ command: "npx",
190
+ args: ["-y", "filebread-mcp"],
191
+ env: { FILEBREAD_API_KEY: apiKey },
192
+ };
193
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
194
+
195
+ // Add .mcp.json to .gitignore if exists
196
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
197
+ if (fs.existsSync(gitignorePath)) {
198
+ const gitignore = fs.readFileSync(gitignorePath, "utf8");
199
+ if (!gitignore.includes(".mcp.json")) {
200
+ fs.appendFileSync(gitignorePath, "\n.mcp.json\n");
201
+ console.log(" Added .mcp.json to .gitignore");
202
+ }
203
+ }
204
+
205
+ console.log(" Created .mcp.json\n");
206
+ console.log(" Done! Restart Claude Code or Cursor to activate.\n");
207
+ console.log(" Next steps:");
208
+ console.log(" - Upload docs: npx filebread upload ./docs/architecture.md");
209
+ console.log(" - Connect sources: visit https://file-bread.fly.dev");
210
+ console.log(" - Check status: npx filebread status\n");
211
+
212
+ try { rl.close(); } catch {}
213
+ }
214
+
215
+ // --- Status ---
216
+
217
+ async function status() {
218
+ const mcpPath = path.join(process.cwd(), ".mcp.json");
219
+ if (!fs.existsSync(mcpPath)) {
220
+ console.log("\n FileBread is not configured in this project.");
221
+ console.log(" Run `npx filebread init` to set up.\n");
222
+ process.exit(1);
223
+ }
224
+
225
+ const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
226
+ const apiKey = config.mcpServers?.filebread?.env?.FILEBREAD_API_KEY;
227
+
228
+ if (!apiKey) {
229
+ console.log("\n No API key found in .mcp.json");
230
+ process.exit(1);
231
+ }
232
+
233
+ console.log("\n FileBread Status\n ────────────────\n");
234
+
235
+ // Check API
236
+ const health = await apiRequest("GET", "/api/health", null);
237
+ console.log(` API: ${health.status === 200 ? "connected" : "error"}`);
238
+
239
+ // Check files
240
+ const files = await apiRequest("GET", "/api/files", null, { "X-API-Key": apiKey });
241
+ const fileList = files.body?.files || [];
242
+ console.log(` Sources: ${fileList.length} files indexed`);
243
+
244
+ if (fileList.length > 0) {
245
+ console.log("");
246
+ for (const f of fileList.slice(0, 10)) {
247
+ console.log(` - ${f.original_name} (${f.content_type})`);
248
+ }
249
+ if (fileList.length > 10) console.log(` ... and ${fileList.length - 10} more`);
250
+ }
251
+
252
+ console.log("");
253
+ }
254
+
255
+ // --- Upload ---
256
+
257
+ async function upload() {
258
+ const filePath = process.argv[3];
259
+ if (!filePath) {
260
+ console.log("\n Usage: npx filebread upload <file>\n");
261
+ console.log(" Examples:");
262
+ console.log(" npx filebread upload ./docs/architecture.md");
263
+ console.log(" npx filebread upload ./README.md\n");
264
+ process.exit(1);
265
+ }
266
+
267
+ const mcpPath = path.join(process.cwd(), ".mcp.json");
268
+ if (!fs.existsSync(mcpPath)) {
269
+ console.log("\n Run `npx filebread init` first.\n");
270
+ process.exit(1);
271
+ }
272
+
273
+ const config = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
274
+ const apiKey = config.mcpServers?.filebread?.env?.FILEBREAD_API_KEY;
275
+
276
+ const absPath = path.resolve(filePath);
277
+ if (!fs.existsSync(absPath)) {
278
+ console.log(`\n File not found: ${absPath}\n`);
279
+ process.exit(1);
280
+ }
281
+
282
+ const content = fs.readFileSync(absPath);
283
+ const fileName = path.relative(process.cwd(), absPath);
284
+ const ext = path.extname(absPath).toLowerCase();
285
+
286
+ const mimeTypes = {
287
+ ".md": "text/markdown", ".txt": "text/plain", ".js": "text/plain",
288
+ ".ts": "text/plain", ".jsx": "text/plain", ".tsx": "text/plain",
289
+ ".py": "text/plain", ".rb": "text/plain", ".go": "text/plain",
290
+ ".rs": "text/plain", ".java": "text/plain", ".sol": "text/plain",
291
+ ".yaml": "text/plain", ".yml": "text/plain", ".toml": "text/plain",
292
+ ".json": "application/json", ".html": "text/html", ".css": "text/css",
293
+ ".pdf": "application/pdf", ".ex": "text/plain", ".exs": "text/plain",
294
+ };
295
+ const contentType = mimeTypes[ext] || "text/plain";
296
+
297
+ // Multipart upload
298
+ const boundary = `----filebread${Date.now()}`;
299
+ const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: ${contentType}\r\n\r\n`;
300
+ const footer = `\r\n--${boundary}--\r\n`;
301
+ const body = Buffer.concat([Buffer.from(header), content, Buffer.from(footer)]);
302
+
303
+ const url = new URL("/api/files", API_URL);
304
+ const mod = url.protocol === "https:" ? https : http;
305
+
306
+ const res = await new Promise((resolve, reject) => {
307
+ const req = mod.request({
308
+ hostname: url.hostname,
309
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
310
+ path: url.pathname,
311
+ method: "POST",
312
+ headers: {
313
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
314
+ "X-API-Key": apiKey,
315
+ "Content-Length": body.length,
316
+ },
317
+ }, (res) => {
318
+ let data = "";
319
+ res.on("data", (c) => (data += c));
320
+ res.on("end", () => {
321
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
322
+ catch { resolve({ status: res.statusCode, body: data }); }
323
+ });
324
+ });
325
+ req.on("error", reject);
326
+ req.write(body);
327
+ req.end();
328
+ });
329
+
330
+ if (res.status === 201 || res.status === 200) {
331
+ console.log(` Uploaded: ${fileName} (indexing...)`);
332
+ } else {
333
+ console.log(` Error: ${JSON.stringify(res.body)}`);
334
+ }
335
+ }
336
+
337
+ // --- Run ---
338
+
339
+ switch (command) {
340
+ case "init": init().catch((e) => { console.error(` Error: ${e.message}`); process.exit(1); }); break;
341
+ case "status": status().catch((e) => { console.error(` Error: ${e.message}`); process.exit(1); }); break;
342
+ case "upload": upload().catch((e) => { console.error(` Error: ${e.message}`); process.exit(1); }); break;
343
+ default: console.log(` Unknown command: ${command}. Run 'npx filebread help' for usage.`); process.exit(1);
344
+ }
package/index.js CHANGED
@@ -1,16 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // If called with a subcommand (init, status, upload, help), run the CLI instead
4
+ const subcommand = process.argv[2];
5
+ if (subcommand && ["init", "status", "upload", "help", "--help"].includes(subcommand)) {
6
+ require("./cli.js");
7
+ process.exitCode = undefined; // let cli.js handle exit
8
+ } else {
9
+ // MCP server mode starts here
10
+
3
11
  const readline = require("readline");
4
12
  const https = require("https");
5
13
  const http = require("http");
6
14
 
7
15
  const API_KEY = process.env.FILEBREAD_API_KEY;
8
- const API_URL = process.env.FILEBREAD_API_URL || "https://filebread.com";
16
+ const API_URL = process.env.FILEBREAD_API_URL || "https://file-bread.fly.dev";
9
17
 
10
18
  if (!API_KEY) {
11
19
  process.stderr.write(
12
20
  "[filebread-mcp] Error: FILEBREAD_API_KEY is required.\n" +
13
- "Get your API key at https://filebread.com/settings/api-keys\n"
21
+ "Run `npx filebread-mcp init` to set up.\n"
14
22
  );
15
23
  process.exit(1);
16
24
  }
@@ -272,3 +280,5 @@ rl.on("line", (line) => {
272
280
  });
273
281
 
274
282
  rl.on("close", () => process.exit(0));
283
+
284
+ } // end of MCP server mode
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "filebread-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server for FileBread - give AI agents context about your project",
3
+ "version": "0.2.1",
4
+ "description": "Give AI agents context about your project. One command setup.",
5
5
  "bin": {
6
- "filebread-mcp": "./index.js"
6
+ "filebread-mcp": "./index.js",
7
+ "filebread": "./cli.js"
7
8
  },
8
- "keywords": ["mcp", "context", "ai", "claude", "cursor", "rag"],
9
+ "keywords": ["mcp", "context", "ai", "claude", "cursor", "rag", "developer-tools"],
9
10
  "license": "MIT",
10
11
  "repository": {
11
12
  "type": "git",
@@ -14,5 +15,5 @@
14
15
  "engines": {
15
16
  "node": ">=18"
16
17
  },
17
- "files": ["index.js"]
18
+ "files": ["index.js", "cli.js"]
18
19
  }