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 +2 -1
- package/bin/50c.js +93 -0
- package/lib/caz-context.js +461 -0
- package/lib/file-memory.js +301 -0
- package/package.json +43 -40
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.
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
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
|
+
}
|