50c 3.0.7 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  AI developer tools from $0.01-$0.65 per call. No subscriptions.
6
6
 
7
+ **Requirements:** Node.js 18+
8
+
7
9
  ## Quick Start
8
10
 
9
11
  ```bash
@@ -180,7 +182,6 @@ export FIFTYC_API_KEY=cv_xxx
180
182
 
181
183
  - Website: https://50c.ai
182
184
  - Docs: https://docs.50c.ai
183
- - GitHub: https://github.com/genxis/50c
184
185
 
185
186
  ---
186
187
 
package/bin/50c.js CHANGED
@@ -617,10 +617,103 @@ function startMCPMode() {
617
617
  }
618
618
 
619
619
  async function handleMCPRequest(request) {
620
+ // Handle local file-memory tools first (FREE, no API call)
621
+ const localResult = await handleLocalTools(request);
622
+ if (localResult) return localResult;
623
+
620
624
  // Forward to remote MCP endpoint
621
625
  return callRemoteMCP(request);
622
626
  }
623
627
 
628
+ // Local file-memory tools (FREE, runs on user machine)
629
+ const { FILE_TOOLS, indexFile, findSymbol, getLines, searchFile, fileSummary } = require('../lib/file-memory.js');
630
+
631
+ const LOCAL_TOOLS = [
632
+ { name: "fm_index", description: "Index a large file for fast symbol/line lookup. Auto-detects Python/JS/TS. FREE.", inputSchema: { type: "object", properties: { filepath: { type: "string", description: "Absolute or relative path to file" } }, required: ["filepath"] } },
633
+ { name: "fm_find", description: "Find where a function/class is defined across all indexed files. FREE.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Symbol name (function, class, variable)" }, filepath: { type: "string", description: "Optional: limit search to one file" } }, required: ["name"] } },
634
+ { name: "fm_lines", description: "Get specific line range from a file with line numbers. FREE.", inputSchema: { type: "object", properties: { filepath: { type: "string" }, start: { type: "number", description: "Start line number" }, end: { type: "number", description: "End line number" } }, required: ["filepath", "start", "end"] } },
635
+ { name: "fm_search", description: "Search for symbols and content in a file. Auto-indexes if needed. FREE.", inputSchema: { type: "object", properties: { filepath: { type: "string" }, query: { type: "string", description: "Search term" } }, required: ["filepath", "query"] } },
636
+ { name: "fm_summary", description: "Get summary of a file: line count, all functions, classes. FREE.", inputSchema: { type: "object", properties: { filepath: { type: "string" } }, required: ["filepath"] } },
637
+ { name: "fm_list", description: "List all indexed files with stats. FREE.", inputSchema: { type: "object", properties: {} } },
638
+ { name: "fm_context", description: "Get context around a symbol (function body, class methods). FREE.", inputSchema: { type: "object", properties: { filepath: { type: "string" }, symbol: { type: "string", description: "Function or class name" }, lines_after: { type: "number", description: "Lines to include after definition (default 50)" } }, required: ["filepath", "symbol"] } }
639
+ ];
640
+
641
+ async function handleLocalTools(request) {
642
+ const { id, method, params } = request;
643
+
644
+ // Handle tools/list - merge local + remote
645
+ if (method === 'tools/list') {
646
+ try {
647
+ const remote = await callRemoteMCP(request);
648
+ const remoteTools = remote.result?.tools || [];
649
+ return {
650
+ jsonrpc: '2.0',
651
+ id,
652
+ result: { tools: [...LOCAL_TOOLS, ...remoteTools] }
653
+ };
654
+ } catch (e) {
655
+ return {
656
+ jsonrpc: '2.0',
657
+ id,
658
+ result: { tools: LOCAL_TOOLS }
659
+ };
660
+ }
661
+ }
662
+
663
+ // Handle local tool calls
664
+ if (method === 'tools/call') {
665
+ const { name, arguments: args } = params || {};
666
+
667
+ if (name === 'fm_index') {
668
+ return mcpResult(id, indexFile(args.filepath));
669
+ }
670
+ if (name === 'fm_find') {
671
+ return mcpResult(id, findSymbol(args.name, args.filepath));
672
+ }
673
+ if (name === 'fm_lines') {
674
+ return mcpResult(id, getLines(args.filepath, args.start, args.end));
675
+ }
676
+ if (name === 'fm_search') {
677
+ return mcpResult(id, searchFile(args.filepath, args.query));
678
+ }
679
+ if (name === 'fm_summary') {
680
+ return mcpResult(id, fileSummary(args.filepath));
681
+ }
682
+ if (name === 'fm_list') {
683
+ const fs = require('fs');
684
+ const INDEX_DIR = path.join(os.homedir(), '.50c', 'file_index');
685
+ if (!fs.existsSync(INDEX_DIR)) return mcpResult(id, []);
686
+ const files = fs.readdirSync(INDEX_DIR).filter(f => f.endsWith('.json'));
687
+ const results = [];
688
+ for (const file of files) {
689
+ try {
690
+ const index = JSON.parse(fs.readFileSync(path.join(INDEX_DIR, file), 'utf8'));
691
+ results.push({ filename: index.filename, filepath: index.filepath, lines: index.totalLines, symbols: index.symbols?.length || 0 });
692
+ } catch (e) {}
693
+ }
694
+ return mcpResult(id, results.sort((a, b) => b.lines - a.lines));
695
+ }
696
+ if (name === 'fm_context') {
697
+ const symbols = findSymbol(args.symbol, args.filepath);
698
+ if (symbols.length === 0) return mcpResult(id, { error: `Symbol not found: ${args.symbol}` });
699
+ const sym = symbols[0];
700
+ const linesAfter = args.lines_after || 50;
701
+ return mcpResult(id, { symbol: sym, content: getLines(sym.filepath, sym.line, sym.line + linesAfter) });
702
+ }
703
+ }
704
+
705
+ return null; // Not a local tool, forward to remote
706
+ }
707
+
708
+ function mcpResult(id, result) {
709
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
710
+ return {
711
+ jsonrpc: '2.0',
712
+ id,
713
+ result: { content: [{ type: 'text', text }] }
714
+ };
715
+ }
716
+
624
717
  // ═══════════════════════════════════════════════════════════════
625
718
  // API HELPERS
626
719
  // ═══════════════════════════════════════════════════════════════
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 50c-context - Thin client for CAZ context compression
4
+ *
5
+ * LOCAL (FREE): Basic indexing, hash lookup, line retrieval
6
+ * API (PRO): CAZ dedup, SimHash, BM25 ranking, fog detection
7
+ *
8
+ * User sees this. Secret sauce stays on server.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const os = require('os');
15
+
16
+ const INDEX_DIR = path.join(os.homedir(), '.50c', 'context');
17
+ const API_BASE = process.env.FIFTYC_API_URL || 'https://api.50c.ai';
18
+
19
+ if (!fs.existsSync(INDEX_DIR)) {
20
+ fs.mkdirSync(INDEX_DIR, { recursive: true });
21
+ }
22
+
23
+ function hash(content) {
24
+ return crypto.createHash('blake2b512').update(content).digest('hex').slice(0, 32);
25
+ }
26
+
27
+ function hashPath(filepath) {
28
+ return crypto.createHash('md5').update(filepath).digest('hex').slice(0, 12);
29
+ }
30
+
31
+ function getIndexPath(filepath) {
32
+ return path.join(INDEX_DIR, `${hashPath(filepath)}.json`);
33
+ }
34
+
35
+ function simpleChunk(content, targetSize = 1024) {
36
+ const chunks = [];
37
+ let start = 0;
38
+ while (start < content.length) {
39
+ let end = Math.min(start + targetSize, content.length);
40
+ if (end < content.length) {
41
+ const newline = content.lastIndexOf('\n', end);
42
+ if (newline > start + targetSize / 2) end = newline + 1;
43
+ }
44
+ chunks.push({
45
+ content: content.slice(start, end),
46
+ start,
47
+ end,
48
+ hash: hash(content.slice(start, end))
49
+ });
50
+ start = end;
51
+ }
52
+ return chunks;
53
+ }
54
+
55
+ function extractSymbols(lines, ext) {
56
+ const symbols = [];
57
+ const isPython = ['.py'].includes(ext);
58
+ const isJS = ['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext);
59
+
60
+ lines.forEach((line, i) => {
61
+ const trimmed = line.trim();
62
+ const lineNum = i + 1;
63
+
64
+ if (isPython) {
65
+ if (trimmed.startsWith('def ')) {
66
+ const name = trimmed.slice(4).split('(')[0].trim();
67
+ symbols.push({ name, type: 'function', line: lineNum });
68
+ }
69
+ else if (trimmed.startsWith('async def ')) {
70
+ const name = trimmed.slice(10).split('(')[0].trim();
71
+ symbols.push({ name, type: 'async_function', line: lineNum });
72
+ }
73
+ else if (trimmed.startsWith('class ')) {
74
+ const name = trimmed.slice(6).split('(')[0].split(':')[0].trim();
75
+ symbols.push({ name, type: 'class', line: lineNum });
76
+ }
77
+ }
78
+
79
+ if (isJS) {
80
+ if (trimmed.match(/^(export\s+)?(async\s+)?function\s+\w+/)) {
81
+ const match = trimmed.match(/function\s+(\w+)/);
82
+ if (match) symbols.push({ name: match[1], type: 'function', line: lineNum });
83
+ }
84
+ else if (trimmed.match(/^(export\s+)?class\s+\w+/)) {
85
+ const match = trimmed.match(/class\s+(\w+)/);
86
+ if (match) symbols.push({ name: match[1], type: 'class', line: lineNum });
87
+ }
88
+ else if (trimmed.match(/^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?(\(|function)/)) {
89
+ const match = trimmed.match(/(const|let|var)\s+(\w+)/);
90
+ if (match) symbols.push({ name: match[2], type: 'arrow_fn', line: lineNum });
91
+ }
92
+ }
93
+ });
94
+
95
+ return symbols;
96
+ }
97
+
98
+ function indexFile(filepath) {
99
+ filepath = path.resolve(filepath);
100
+ if (!fs.existsSync(filepath)) return { error: `File not found: ${filepath}` };
101
+
102
+ const content = fs.readFileSync(filepath, 'utf8');
103
+ const lines = content.split('\n');
104
+ const ext = path.extname(filepath).toLowerCase();
105
+ const symbols = extractSymbols(lines, ext);
106
+ const chunks = simpleChunk(content);
107
+ const fileHash = hash(content);
108
+
109
+ const indexPath = getIndexPath(filepath);
110
+ if (fs.existsSync(indexPath)) {
111
+ try {
112
+ const existing = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
113
+ if (existing.hash === fileHash) {
114
+ return { status: 'current', filepath, lines: lines.length };
115
+ }
116
+ } catch (e) {}
117
+ }
118
+
119
+ const blockHashes = {};
120
+ chunks.forEach(c => {
121
+ if (!blockHashes[c.hash]) blockHashes[c.hash] = [];
122
+ blockHashes[c.hash].push({ start: c.start, end: c.end });
123
+ });
124
+
125
+ const index = {
126
+ filepath,
127
+ filename: path.basename(filepath),
128
+ hash: fileHash,
129
+ totalLines: lines.length,
130
+ indexed: new Date().toISOString(),
131
+ symbols,
132
+ chunks: chunks.map(c => ({ hash: c.hash, start: c.start, end: c.end })),
133
+ uniqueBlocks: Object.keys(blockHashes).length,
134
+ totalBlocks: chunks.length,
135
+ dedupRatio: chunks.length / Object.keys(blockHashes).length
136
+ };
137
+
138
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
139
+
140
+ return {
141
+ status: 'indexed',
142
+ filepath,
143
+ lines: lines.length,
144
+ symbols: symbols.length,
145
+ uniqueBlocks: index.uniqueBlocks,
146
+ totalBlocks: index.totalBlocks,
147
+ dedupRatio: index.dedupRatio.toFixed(2)
148
+ };
149
+ }
150
+
151
+ function findSymbol(name, filepath = null) {
152
+ const indexFiles = filepath
153
+ ? [getIndexPath(path.resolve(filepath))]
154
+ : fs.readdirSync(INDEX_DIR).filter(f => f.endsWith('.json')).map(f => path.join(INDEX_DIR, f));
155
+
156
+ const results = [];
157
+ const nameLower = name.toLowerCase();
158
+
159
+ for (const indexFile of indexFiles) {
160
+ if (!fs.existsSync(indexFile)) continue;
161
+ try {
162
+ const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
163
+ const matches = index.symbols.filter(s =>
164
+ s.name.toLowerCase().includes(nameLower)
165
+ );
166
+ matches.forEach(m => results.push({
167
+ name: m.name, type: m.type, line: m.line,
168
+ file: index.filename, filepath: index.filepath
169
+ }));
170
+ } catch (e) {}
171
+ }
172
+
173
+ return results.sort((a, b) => {
174
+ const aExact = a.name.toLowerCase() === nameLower ? 0 : 1;
175
+ const bExact = b.name.toLowerCase() === nameLower ? 0 : 1;
176
+ return aExact - bExact || a.line - b.line;
177
+ });
178
+ }
179
+
180
+ function getLines(filepath, start, end) {
181
+ filepath = path.resolve(filepath);
182
+ if (!fs.existsSync(filepath)) return { error: `File not found: ${filepath}` };
183
+
184
+ const lines = fs.readFileSync(filepath, 'utf8').split('\n');
185
+ start = Math.max(1, start);
186
+ end = Math.min(lines.length, end);
187
+
188
+ const result = [];
189
+ for (let i = start - 1; i < end; i++) {
190
+ result.push(`${(i + 1).toString().padStart(5)}| ${lines[i]}`);
191
+ }
192
+ return result.join('\n');
193
+ }
194
+
195
+ function getBlocksByQuery(filepath, query) {
196
+ filepath = path.resolve(filepath);
197
+ const indexPath = getIndexPath(filepath);
198
+
199
+ if (!fs.existsSync(indexPath)) indexFile(filepath);
200
+ if (!fs.existsSync(indexPath)) return { error: `Could not index: ${filepath}` };
201
+
202
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
203
+ const content = fs.readFileSync(filepath, 'utf8');
204
+ const queryLower = query.toLowerCase();
205
+
206
+ const relevantSymbols = index.symbols.filter(s =>
207
+ s.name.toLowerCase().includes(queryLower)
208
+ );
209
+
210
+ const relevantChunks = [];
211
+ const lines = content.split('\n');
212
+
213
+ for (const sym of relevantSymbols) {
214
+ const startLine = sym.line;
215
+ let endLine = startLine + 50;
216
+ for (let i = startLine; i < Math.min(startLine + 100, lines.length); i++) {
217
+ const line = lines[i];
218
+ if (line && !line.startsWith(' ') && !line.startsWith('\t') && line.trim() && i > startLine) {
219
+ endLine = i;
220
+ break;
221
+ }
222
+ }
223
+ relevantChunks.push({
224
+ symbol: sym.name,
225
+ startLine,
226
+ endLine,
227
+ content: lines.slice(startLine - 1, endLine).join('\n')
228
+ });
229
+ }
230
+
231
+ return {
232
+ query,
233
+ file: index.filename,
234
+ matchedSymbols: relevantSymbols.length,
235
+ chunks: relevantChunks.slice(0, 10),
236
+ totalLines: index.totalLines,
237
+ sentLines: relevantChunks.reduce((sum, c) => sum + (c.endLine - c.startLine), 0),
238
+ reduction: ((1 - relevantChunks.reduce((sum, c) => sum + (c.endLine - c.startLine), 0) / index.totalLines) * 100).toFixed(1) + '%'
239
+ };
240
+ }
241
+
242
+ async function apiCall(endpoint, data, apiKey) {
243
+ const key = apiKey || process.env.FIFTYC_API_KEY;
244
+ if (!key) return { error: 'No API key. Set FIFTYC_API_KEY' };
245
+
246
+ try {
247
+ const fetch = globalThis.fetch || require('node-fetch');
248
+ const res = await fetch(`${API_BASE}${endpoint}`, {
249
+ method: 'POST',
250
+ headers: {
251
+ 'Content-Type': 'application/json',
252
+ 'X-API-Key': key
253
+ },
254
+ body: JSON.stringify(data)
255
+ });
256
+ return await res.json();
257
+ } catch (e) {
258
+ return { error: e.message };
259
+ }
260
+ }
261
+
262
+ async function cazDedup(filepath, apiKey) {
263
+ filepath = path.resolve(filepath);
264
+ if (!fs.existsSync(filepath)) return { error: `File not found: ${filepath}` };
265
+
266
+ const content = fs.readFileSync(filepath, 'utf8');
267
+ const localIndex = indexFile(filepath);
268
+
269
+ return await apiCall('/tools/caz_dedup', {
270
+ filepath: path.basename(filepath),
271
+ content,
272
+ localStats: localIndex
273
+ }, apiKey);
274
+ }
275
+
276
+ async function contextCompress(filepath, query, apiKey) {
277
+ filepath = path.resolve(filepath);
278
+ if (!fs.existsSync(filepath)) return { error: `File not found: ${filepath}` };
279
+
280
+ const content = fs.readFileSync(filepath, 'utf8');
281
+ const localBlocks = getBlocksByQuery(filepath, query);
282
+
283
+ return await apiCall('/tools/context_compress', {
284
+ filepath: path.basename(filepath),
285
+ query,
286
+ localBlocks,
287
+ content
288
+ }, apiKey);
289
+ }
290
+
291
+ async function fogCheck(messages, apiKey) {
292
+ return await apiCall('/tools/fog_check', { messages }, apiKey);
293
+ }
294
+
295
+ async function fogClear(messages, mode, apiKey) {
296
+ return await apiCall('/tools/fog_clear', { messages, mode }, apiKey);
297
+ }
298
+
299
+ function listIndexed() {
300
+ const files = fs.readdirSync(INDEX_DIR).filter(f => f.endsWith('.json'));
301
+ const results = [];
302
+
303
+ for (const file of files) {
304
+ try {
305
+ const index = JSON.parse(fs.readFileSync(path.join(INDEX_DIR, file), 'utf8'));
306
+ results.push({
307
+ filename: index.filename,
308
+ filepath: index.filepath,
309
+ lines: index.totalLines,
310
+ symbols: index.symbols.length,
311
+ dedupRatio: index.dedupRatio?.toFixed(2) || 'N/A'
312
+ });
313
+ } catch (e) {}
314
+ }
315
+
316
+ return results.sort((a, b) => b.lines - a.lines);
317
+ }
318
+
319
+ function stats() {
320
+ const files = listIndexed();
321
+ const totalLines = files.reduce((sum, f) => sum + f.lines, 0);
322
+ const totalSymbols = files.reduce((sum, f) => sum + f.symbols, 0);
323
+
324
+ return {
325
+ indexedFiles: files.length,
326
+ totalLines,
327
+ totalSymbols,
328
+ indexDir: INDEX_DIR
329
+ };
330
+ }
331
+
332
+ const CONTEXT_TOOLS = {
333
+ ctx_index: {
334
+ description: "Index file for CAZ context compression. FREE.",
335
+ handler: (args) => indexFile(args.filepath)
336
+ },
337
+ ctx_find: {
338
+ description: "Find symbol across indexed files. FREE.",
339
+ handler: (args) => findSymbol(args.name, args.filepath)
340
+ },
341
+ ctx_lines: {
342
+ description: "Get specific line range. FREE.",
343
+ handler: (args) => getLines(args.filepath, args.start, args.end)
344
+ },
345
+ ctx_blocks: {
346
+ description: "Get relevant blocks for query. FREE.",
347
+ handler: (args) => getBlocksByQuery(args.filepath, args.query)
348
+ },
349
+ ctx_list: {
350
+ description: "List indexed files. FREE.",
351
+ handler: () => listIndexed()
352
+ },
353
+ ctx_stats: {
354
+ description: "Get indexing stats. FREE.",
355
+ handler: () => stats()
356
+ },
357
+ caz_dedup: {
358
+ description: "Full CAZ deduplication. PRO $0.02.",
359
+ handler: async (args) => await cazDedup(args.filepath, args.apiKey)
360
+ },
361
+ context_compress: {
362
+ description: "Compress context for query. PRO $0.03.",
363
+ handler: async (args) => await contextCompress(args.filepath, args.query, args.apiKey)
364
+ },
365
+ fog_check: {
366
+ description: "Check fog level. FREE.",
367
+ handler: async (args) => await fogCheck(args.messages, args.apiKey)
368
+ },
369
+ fog_clear: {
370
+ description: "Clear context fog. PRO $0.03.",
371
+ handler: async (args) => await fogClear(args.messages, args.mode, args.apiKey)
372
+ }
373
+ };
374
+
375
+ module.exports = {
376
+ CONTEXT_TOOLS,
377
+ indexFile,
378
+ findSymbol,
379
+ getLines,
380
+ getBlocksByQuery,
381
+ listIndexed,
382
+ stats,
383
+ cazDedup,
384
+ contextCompress,
385
+ fogCheck,
386
+ fogClear
387
+ };
388
+
389
+ if (require.main === module) {
390
+ const args = process.argv.slice(2);
391
+ const cmd = args[0];
392
+
393
+ if (!cmd || cmd === 'help') {
394
+ console.log(`
395
+ 50c-context - CAZ Context Compression
396
+
397
+ LOCAL (FREE):
398
+ ctx_index <file> Index file for dedup
399
+ ctx_find <name> [file] Find symbol
400
+ ctx_lines <file> <s> <e> Get line range
401
+ ctx_blocks <file> <query> Get relevant blocks
402
+ ctx_list List indexed files
403
+ ctx_stats Show stats
404
+
405
+ API (PRO):
406
+ caz_dedup <file> Full CAZ deduplication
407
+ context_compress <f> <q> Compress for query
408
+ fog_check Check fog level
409
+ fog_clear Clear context fog
410
+
411
+ Test:
412
+ node caz-context.js test
413
+ `);
414
+ return;
415
+ }
416
+
417
+ if (cmd === 'test') {
418
+ const testFile = 'C:\\Users\\Administrator\\Desktop\\50c\\nj-deploy\\api-unified-v8-FINAL-ALL.py';
419
+
420
+ console.log('='.repeat(60));
421
+ console.log('50c-context TEST');
422
+ console.log('='.repeat(60));
423
+
424
+ console.log('\n[1] Indexing...');
425
+ const idx = indexFile(testFile);
426
+ console.log(idx);
427
+
428
+ console.log('\n[2] Find fog_check...');
429
+ console.log(findSymbol('fog_check'));
430
+
431
+ console.log('\n[3] Get blocks for "fog"...');
432
+ const blocks = getBlocksByQuery(testFile, 'fog');
433
+ console.log(`Matched: ${blocks.matchedSymbols} symbols`);
434
+ console.log(`Sent: ${blocks.sentLines} lines (${blocks.reduction} reduction)`);
435
+ console.log(`First chunk: ${blocks.chunks[0]?.symbol} at line ${blocks.chunks[0]?.startLine}`);
436
+
437
+ console.log('\n[4] Stats...');
438
+ console.log(stats());
439
+
440
+ console.log('\n[5] List indexed...');
441
+ console.log(listIndexed());
442
+ }
443
+ else if (cmd === 'index') {
444
+ console.log(indexFile(args[1]));
445
+ }
446
+ else if (cmd === 'find') {
447
+ console.log(JSON.stringify(findSymbol(args[1], args[2]), null, 2));
448
+ }
449
+ else if (cmd === 'lines') {
450
+ console.log(getLines(args[1], parseInt(args[2]), parseInt(args[3])));
451
+ }
452
+ else if (cmd === 'blocks') {
453
+ console.log(JSON.stringify(getBlocksByQuery(args[1], args[2]), null, 2));
454
+ }
455
+ else if (cmd === 'list') {
456
+ console.log(JSON.stringify(listIndexed(), null, 2));
457
+ }
458
+ else if (cmd === 'stats') {
459
+ console.log(JSON.stringify(stats(), null, 2));
460
+ }
461
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * FILE MEMORY - Local file indexing for LLM assistance
3
+ * No dependencies, pure Node.js
4
+ *
5
+ * Helps LLM not "phone it in" on 4000-8000 line files by providing:
6
+ * - Function/class locations
7
+ * - Line range retrieval
8
+ * - Keyword search
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const INDEX_DIR = path.join(os.homedir(), '.50c', 'file_index');
16
+
17
+ // Ensure index directory exists
18
+ if (!fs.existsSync(INDEX_DIR)) {
19
+ fs.mkdirSync(INDEX_DIR, { recursive: true });
20
+ }
21
+
22
+ function getIndexPath(filepath) {
23
+ const hash = require('crypto').createHash('md5')
24
+ .update(filepath).digest('hex').slice(0, 12);
25
+ return path.join(INDEX_DIR, `${hash}.json`);
26
+ }
27
+
28
+ function extractSymbols(lines) {
29
+ const symbols = [];
30
+ lines.forEach((line, i) => {
31
+ const trimmed = line.trim();
32
+ const lineNum = i + 1;
33
+
34
+ // Python
35
+ if (trimmed.startsWith('def ')) {
36
+ const name = trimmed.slice(4).split('(')[0].trim();
37
+ symbols.push({ name, type: 'function', line: lineNum, sig: trimmed.split(':')[0] });
38
+ }
39
+ else if (trimmed.startsWith('async def ')) {
40
+ const name = trimmed.slice(10).split('(')[0].trim();
41
+ symbols.push({ name, type: 'async_function', line: lineNum, sig: trimmed.split(':')[0] });
42
+ }
43
+ else if (trimmed.startsWith('class ')) {
44
+ const name = trimmed.slice(6).split('(')[0].split(':')[0].trim();
45
+ symbols.push({ name, type: 'class', line: lineNum, sig: trimmed.split(':')[0] });
46
+ }
47
+ // JavaScript/TypeScript
48
+ else if (trimmed.match(/^(export\s+)?(async\s+)?function\s+\w+/)) {
49
+ const match = trimmed.match(/function\s+(\w+)/);
50
+ if (match) symbols.push({ name: match[1], type: 'function', line: lineNum, sig: trimmed.split('{')[0].trim() });
51
+ }
52
+ else if (trimmed.match(/^(export\s+)?class\s+\w+/)) {
53
+ const match = trimmed.match(/class\s+(\w+)/);
54
+ if (match) symbols.push({ name: match[1], type: 'class', line: lineNum, sig: trimmed.split('{')[0].trim() });
55
+ }
56
+ else if (trimmed.match(/^(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/)) {
57
+ const match = trimmed.match(/^(const|let|var)\s+(\w+)/);
58
+ if (match) symbols.push({ name: match[2], type: 'arrow_function', line: lineNum, sig: trimmed.slice(0, 60) });
59
+ }
60
+ });
61
+ return symbols;
62
+ }
63
+
64
+ function indexFile(filepath) {
65
+ filepath = path.resolve(filepath);
66
+
67
+ if (!fs.existsSync(filepath)) {
68
+ return { error: `File not found: ${filepath}` };
69
+ }
70
+
71
+ const content = fs.readFileSync(filepath, 'utf8');
72
+ const lines = content.split('\n');
73
+ const symbols = extractSymbols(lines);
74
+
75
+ // Create chunks (100 lines each)
76
+ const chunkSize = 100;
77
+ const chunks = [];
78
+ for (let i = 0; i < lines.length; i += chunkSize) {
79
+ const chunkLines = lines.slice(i, i + chunkSize);
80
+ const chunkSymbols = symbols.filter(s => s.line > i && s.line <= i + chunkSize);
81
+ chunks.push({
82
+ start: i + 1,
83
+ end: Math.min(i + chunkSize, lines.length),
84
+ preview: chunkLines.slice(0, 3).join(' ').slice(0, 100),
85
+ functions: chunkSymbols.filter(s => s.type !== 'class').map(s => s.name),
86
+ classes: chunkSymbols.filter(s => s.type === 'class').map(s => s.name)
87
+ });
88
+ }
89
+
90
+ const index = {
91
+ filepath,
92
+ totalLines: lines.length,
93
+ indexed: new Date().toISOString(),
94
+ symbols,
95
+ chunks
96
+ };
97
+
98
+ fs.writeFileSync(getIndexPath(filepath), JSON.stringify(index, null, 2));
99
+
100
+ return {
101
+ status: 'indexed',
102
+ filepath,
103
+ lines: lines.length,
104
+ functions: symbols.filter(s => s.type !== 'class').length,
105
+ classes: symbols.filter(s => s.type === 'class').length
106
+ };
107
+ }
108
+
109
+ function findSymbol(name, filepath = null) {
110
+ const files = filepath ? [getIndexPath(path.resolve(filepath))] :
111
+ fs.readdirSync(INDEX_DIR).map(f => path.join(INDEX_DIR, f));
112
+
113
+ const results = [];
114
+ for (const indexFile of files) {
115
+ if (!fs.existsSync(indexFile)) continue;
116
+ try {
117
+ const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
118
+ const matches = index.symbols.filter(s =>
119
+ s.name.toLowerCase().includes(name.toLowerCase())
120
+ );
121
+ matches.forEach(m => results.push({ ...m, filepath: index.filepath }));
122
+ } catch (e) {}
123
+ }
124
+ return results;
125
+ }
126
+
127
+ function getLines(filepath, start, end) {
128
+ filepath = path.resolve(filepath);
129
+ if (!fs.existsSync(filepath)) {
130
+ return { error: `File not found: ${filepath}` };
131
+ }
132
+
133
+ const lines = fs.readFileSync(filepath, 'utf8').split('\n');
134
+ const result = [];
135
+ for (let i = start - 1; i < Math.min(end, lines.length); i++) {
136
+ result.push(`${(i + 1).toString().padStart(5)}| ${lines[i]}`);
137
+ }
138
+ return result.join('\n');
139
+ }
140
+
141
+ function searchFile(filepath, query) {
142
+ filepath = path.resolve(filepath);
143
+ const indexPath = getIndexPath(filepath);
144
+
145
+ if (!fs.existsSync(indexPath)) {
146
+ return { error: `File not indexed. Run: file_index("${filepath}")` };
147
+ }
148
+
149
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
150
+ const queryLower = query.toLowerCase();
151
+
152
+ // Search symbols first
153
+ const symbolMatches = index.symbols.filter(s =>
154
+ s.name.toLowerCase().includes(queryLower) ||
155
+ s.sig.toLowerCase().includes(queryLower)
156
+ ).map(s => ({
157
+ type: 'symbol',
158
+ name: s.name,
159
+ line: s.line,
160
+ sig: s.sig
161
+ }));
162
+
163
+ // Search chunks
164
+ const content = fs.readFileSync(filepath, 'utf8');
165
+ const lines = content.split('\n');
166
+ const lineMatches = [];
167
+
168
+ lines.forEach((line, i) => {
169
+ if (line.toLowerCase().includes(queryLower)) {
170
+ lineMatches.push({
171
+ type: 'line',
172
+ line: i + 1,
173
+ content: line.trim().slice(0, 100)
174
+ });
175
+ }
176
+ });
177
+
178
+ return {
179
+ symbols: symbolMatches.slice(0, 10),
180
+ lines: lineMatches.slice(0, 20)
181
+ };
182
+ }
183
+
184
+ function fileSummary(filepath) {
185
+ filepath = path.resolve(filepath);
186
+ const indexPath = getIndexPath(filepath);
187
+
188
+ if (!fs.existsSync(indexPath)) {
189
+ return { error: `File not indexed. Run: file_index("${filepath}")` };
190
+ }
191
+
192
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
193
+
194
+ return {
195
+ filepath: index.filepath,
196
+ totalLines: index.totalLines,
197
+ indexed: index.indexed,
198
+ functions: index.symbols.filter(s => s.type !== 'class').length,
199
+ classes: index.symbols.filter(s => s.type === 'class').length,
200
+ topSymbols: index.symbols.slice(0, 30).map(s => ({
201
+ name: s.name,
202
+ type: s.type,
203
+ line: s.line
204
+ }))
205
+ };
206
+ }
207
+
208
+ // MCP Tool definitions for local file operations
209
+ const FILE_TOOLS = {
210
+ file_index: {
211
+ description: "Index a large local file for fast symbol/line lookup. FREE.",
212
+ schema: {
213
+ type: "object",
214
+ properties: { filepath: { type: "string", description: "Path to file" } },
215
+ required: ["filepath"]
216
+ },
217
+ handler: (args) => indexFile(args.filepath)
218
+ },
219
+ file_find: {
220
+ description: "Find where a function/class is defined in indexed files. FREE.",
221
+ schema: {
222
+ type: "object",
223
+ properties: {
224
+ name: { type: "string", description: "Symbol name to find" },
225
+ filepath: { type: "string", description: "Optional: limit to one file" }
226
+ },
227
+ required: ["name"]
228
+ },
229
+ handler: (args) => findSymbol(args.name, args.filepath)
230
+ },
231
+ file_lines: {
232
+ description: "Get specific line range from a file with line numbers. FREE.",
233
+ schema: {
234
+ type: "object",
235
+ properties: {
236
+ filepath: { type: "string" },
237
+ start: { type: "number", description: "Start line" },
238
+ end: { type: "number", description: "End line" }
239
+ },
240
+ required: ["filepath", "start", "end"]
241
+ },
242
+ handler: (args) => getLines(args.filepath, args.start, args.end)
243
+ },
244
+ file_search: {
245
+ description: "Search for content in an indexed file. FREE.",
246
+ schema: {
247
+ type: "object",
248
+ properties: {
249
+ filepath: { type: "string" },
250
+ query: { type: "string", description: "Search query" }
251
+ },
252
+ required: ["filepath", "query"]
253
+ },
254
+ handler: (args) => searchFile(args.filepath, args.query)
255
+ },
256
+ file_summary: {
257
+ description: "Get summary of indexed file (functions, classes, line count). FREE.",
258
+ schema: {
259
+ type: "object",
260
+ properties: { filepath: { type: "string" } },
261
+ required: ["filepath"]
262
+ },
263
+ handler: (args) => fileSummary(args.filepath)
264
+ }
265
+ };
266
+
267
+ module.exports = {
268
+ FILE_TOOLS,
269
+ indexFile,
270
+ findSymbol,
271
+ getLines,
272
+ searchFile,
273
+ fileSummary
274
+ };
275
+
276
+ // CLI test
277
+ if (require.main === module) {
278
+ const testFile = process.argv[2] || 'C:\\Users\\Administrator\\Desktop\\50c\\nj-deploy\\api-unified-v8-FINAL-ALL.py';
279
+
280
+ console.log('='.repeat(60));
281
+ console.log('FILE MEMORY - Node.js Test');
282
+ console.log('='.repeat(60));
283
+
284
+ console.log('\n[1] Indexing...');
285
+ console.log(indexFile(testFile));
286
+
287
+ console.log('\n[2] Finding fog_check...');
288
+ console.log(findSymbol('fog_check'));
289
+
290
+ console.log('\n[3] Lines 2385-2395...');
291
+ console.log(getLines(testFile, 2385, 2395));
292
+
293
+ console.log('\n[4] Search "beacon"...');
294
+ const search = searchFile(testFile, 'beacon');
295
+ console.log('Symbols:', search.symbols?.slice(0, 5));
296
+ console.log('Lines:', search.lines?.slice(0, 5));
297
+
298
+ console.log('\n[5] Summary...');
299
+ const sum = fileSummary(testFile);
300
+ console.log(`${sum.totalLines} lines, ${sum.functions} functions, ${sum.classes} classes`);
301
+ }
package/package.json CHANGED
@@ -1,40 +1,43 @@
1
- {
2
- "name": "50c",
3
- "version": "3.0.7",
4
- "description": "50c Hub - One Hub, Many Packs, Infinite Tools. AI developer tools from $0.01-$0.65.",
5
- "bin": {
6
- "50c": "./bin/50c.js"
7
- },
8
- "scripts": {
9
- "postinstall": "node -e \"console.log('\\n50c Hub installed. Run: 50c install\\n')\""
10
- },
11
- "keywords": [
12
- "mcp",
13
- "ai",
14
- "llm",
15
- "cli",
16
- "agent",
17
- "50c",
18
- "hints",
19
- "genius",
20
- "beacon",
21
- "developer-tools",
22
- "context",
23
- "compression"
24
- ],
25
- "author": "genxis",
26
- "license": "SEE LICENSE IN LICENSE",
27
- "homepage": "https://50c.ai",
28
- "repository": {
29
- "type": "git",
30
- "url": "https://github.com/genxis/50c"
31
- },
32
- "engines": {
33
- "node": ">=18.0.0"
34
- },
35
- "files": [
36
- "bin/",
37
- "README.md",
38
- "LICENSE"
39
- ]
40
- }
1
+ {
2
+ "name": "50c",
3
+ "version": "3.0.8",
4
+ "description": "50c Hub - One Hub, Many Packs, Infinite Tools. AI developer tools from $0.01-$0.65.",
5
+ "bin": {
6
+ "50c": "./bin/50c.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node -e \"console.log('\\n50c Hub installed. Run: 50c install\\n')\""
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "ai",
14
+ "llm",
15
+ "cli",
16
+ "agent",
17
+ "50c",
18
+ "hints",
19
+ "genius",
20
+ "beacon",
21
+ "developer-tools",
22
+ "context",
23
+ "compression",
24
+ "caz",
25
+ "file-memory"
26
+ ],
27
+ "author": "genxis",
28
+ "license": "SEE LICENSE IN LICENSE",
29
+ "homepage": "https://50c.ai",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/50c-ai/50c-mcp"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "lib/",
40
+ "README.md",
41
+ "LICENSE"
42
+ ]
43
+ }