@lucenaone/coder 1.0.0 → 1.1.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.
- package/.WorkspaceBrain/workspace.json +4 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +10 -4
- package/src/agent.js +134 -19
- package/src/cli-indexer.js +466 -0
- package/src/main.js +3 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucenaone/coder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Private tunnel for connecting LucenaCoder.com to your local folder. Always remains folder scoped while providing full terminal access.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
"start": "node bin/lucena.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"chokidar": "^4.0.0",
|
|
13
14
|
"firebase": "^11.0.0",
|
|
14
|
-
"
|
|
15
|
+
"web-tree-sitter": "^0.26.9"
|
|
15
16
|
},
|
|
16
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"lucena",
|
|
19
|
+
"lucenacoder",
|
|
20
|
+
"tunnel",
|
|
21
|
+
"lucenaone"
|
|
22
|
+
],
|
|
17
23
|
"author": "LucenaOne",
|
|
18
24
|
"license": "MIT"
|
|
19
|
-
}
|
|
25
|
+
}
|
package/src/agent.js
CHANGED
|
@@ -3,13 +3,14 @@ import { getDatabase, ref, push, set, onChildAdded, onDisconnect, serverTimestam
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { watch } from 'chokidar';
|
|
5
5
|
import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from 'fs/promises';
|
|
6
|
-
import { join, resolve, dirname, basename, relative, isAbsolute } from 'path';
|
|
7
|
-
import { existsSync } from 'fs';
|
|
6
|
+
import { join, resolve, dirname, basename, relative, isAbsolute, extname } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
8
8
|
import { FIREBASE_CONFIG } from './config.js';
|
|
9
|
+
import { buildIndex, reindexFile } from './cli-indexer.js';
|
|
9
10
|
|
|
10
11
|
const IGNORED_PATTERNS = [
|
|
11
12
|
'node_modules', '.git', '.next', '.wrangler', '.DS_Store',
|
|
12
|
-
'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase'
|
|
13
|
+
'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase'
|
|
13
14
|
];
|
|
14
15
|
|
|
15
16
|
// ── The CLI Jailer ──
|
|
@@ -36,6 +37,19 @@ function getJailedPath(baseDir, rawPath) {
|
|
|
36
37
|
return resolve(baseDir, safeParts.join('/'));
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// ── Path Sanitizer ──
|
|
41
|
+
// Strips the absolute cwd from any text output so the AI never sees
|
|
42
|
+
// real filesystem paths. Replaces /Users/whoever/projects/foo with /
|
|
43
|
+
function stripCwd(cwd, text) {
|
|
44
|
+
if (!text || typeof text !== 'string') return text;
|
|
45
|
+
// Normalize both to forward-slash form for reliable matching
|
|
46
|
+
const normalized = cwd.replace(/\\/g, '/');
|
|
47
|
+
// Escape regex special chars in the path
|
|
48
|
+
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
// Replace the absolute path (with or without trailing slash) with just /
|
|
50
|
+
return text.replace(new RegExp(escaped + '/?', 'g'), '/');
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export class LucenaAgent {
|
|
40
54
|
constructor(cwd) {
|
|
41
55
|
this.cwd = resolve(cwd);
|
|
@@ -45,9 +59,49 @@ export class LucenaAgent {
|
|
|
45
59
|
this.watcher = null;
|
|
46
60
|
this.activeCommands = new Map();
|
|
47
61
|
this.connected = false;
|
|
62
|
+
this.indexData = null; // Pre-built index from CLI-side parsing
|
|
63
|
+
this.indexPromise = null; // The in-flight indexing promise
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_scaffoldWorkspaceBrain() {
|
|
67
|
+
const brainDir = join(this.cwd, '.WorkspaceBrain');
|
|
68
|
+
const subdirs = ['memories', 'skills', 'threads'];
|
|
69
|
+
try {
|
|
70
|
+
if (!existsSync(brainDir)) mkdirSync(brainDir, { recursive: true });
|
|
71
|
+
for (const sub of subdirs) {
|
|
72
|
+
const subPath = join(brainDir, sub);
|
|
73
|
+
if (!existsSync(subPath)) mkdirSync(subPath, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
// Scaffold workspace.json if it doesn't exist
|
|
76
|
+
const wsJson = join(brainDir, 'workspace.json');
|
|
77
|
+
if (!existsSync(wsJson)) {
|
|
78
|
+
writeFileSync(wsJson, JSON.stringify({ name: basename(this.cwd), createdAt: new Date().toISOString() }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Non-fatal — the brain is best-effort
|
|
82
|
+
console.warn('[workspace-brain] scaffold failed:', err.message);
|
|
83
|
+
}
|
|
48
84
|
}
|
|
49
85
|
|
|
50
86
|
async start() {
|
|
87
|
+
// Ensure .WorkspaceBrain/ exists on disk with subdirectories
|
|
88
|
+
this._scaffoldWorkspaceBrain();
|
|
89
|
+
|
|
90
|
+
// ── Start indexing immediately on the CLI machine ──
|
|
91
|
+
this.indexPromise = buildIndex(this.cwd, (progress) => {
|
|
92
|
+
if (progress.phase === 'parsing' && progress.current > 0) {
|
|
93
|
+
process.stdout.write(`\r ${'\x1b[90m'}⏳ Indexing... ${progress.current}/${progress.total} files${'\x1b[0m'}`);
|
|
94
|
+
}
|
|
95
|
+
}).then((result) => {
|
|
96
|
+
this.indexData = result;
|
|
97
|
+
const { stats } = result;
|
|
98
|
+
process.stdout.write(`\r ${'\x1b[32m'}✔ Indexed ${stats.filesParsed} files — ${stats.symbolCount} symbols, ${stats.stringCount} strings${'\x1b[0m'} \n`);
|
|
99
|
+
return result;
|
|
100
|
+
}).catch((err) => {
|
|
101
|
+
console.warn(`\n ${'\x1b[33m'}⚠ Indexing failed: ${err.message}${'\x1b[0m'}`);
|
|
102
|
+
return null;
|
|
103
|
+
});
|
|
104
|
+
|
|
51
105
|
this.app = initializeApp(FIREBASE_CONFIG, `agent-${this.tunnelId}`);
|
|
52
106
|
this.db = getDatabase(this.app);
|
|
53
107
|
const tunnelRef = ref(this.db, `tunnels/${this.tunnelId}`);
|
|
@@ -55,8 +109,7 @@ export class LucenaAgent {
|
|
|
55
109
|
await set(tunnelRef, {
|
|
56
110
|
meta: {
|
|
57
111
|
createdAt: serverTimestamp(),
|
|
58
|
-
|
|
59
|
-
cwdName: basename(this.cwd),
|
|
112
|
+
cwdName: basename(this.cwd), // Never expose full cwd to the browser
|
|
60
113
|
status: 'active',
|
|
61
114
|
pid: process.pid,
|
|
62
115
|
platform: process.platform
|
|
@@ -69,6 +122,20 @@ export class LucenaAgent {
|
|
|
69
122
|
onDisconnect(presenceRef).set(false);
|
|
70
123
|
onDisconnect(ref(this.db, `tunnels/${this.tunnelId}/meta/status`)).set('disconnected');
|
|
71
124
|
|
|
125
|
+
// ── Listen for browser connect events ──
|
|
126
|
+
// When the browser connects, push the pre-built index immediately
|
|
127
|
+
const connectRef = ref(this.db, `tunnels/${this.tunnelId}/meta/browserConnected`);
|
|
128
|
+
onChildAdded(ref(this.db, `tunnels/${this.tunnelId}/browserConnect`), async (snapshot) => {
|
|
129
|
+
const data = snapshot.val();
|
|
130
|
+
if (!data) return;
|
|
131
|
+
remove(snapshot.ref);
|
|
132
|
+
|
|
133
|
+
// Index is guaranteed ready (awaited in start()), push immediately
|
|
134
|
+
if (this.indexData) {
|
|
135
|
+
this.pushIndexSnapshot(this.indexData);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
72
139
|
const commandsRef = ref(this.db, `tunnels/${this.tunnelId}/commands`);
|
|
73
140
|
onChildAdded(commandsRef, async (snapshot) => {
|
|
74
141
|
const command = snapshot.val();
|
|
@@ -78,15 +145,51 @@ export class LucenaAgent {
|
|
|
78
145
|
try {
|
|
79
146
|
await this.handleCommand(command);
|
|
80
147
|
} catch (err) {
|
|
81
|
-
this.pushResponse(command.messageId, 'error', err.message);
|
|
148
|
+
this.pushResponse(command.messageId, 'error', stripCwd(this.cwd, err.message));
|
|
82
149
|
}
|
|
83
150
|
});
|
|
84
151
|
|
|
85
152
|
this.startWatcher();
|
|
86
153
|
this.connected = true;
|
|
154
|
+
|
|
155
|
+
// ── Wait for indexing to finish before opening the browser ──
|
|
156
|
+
// This guarantees the snapshot is ready the instant the browser connects
|
|
157
|
+
await this.indexPromise;
|
|
158
|
+
|
|
87
159
|
return this.tunnelId;
|
|
88
160
|
}
|
|
89
161
|
|
|
162
|
+
// ── Push the full pre-built index to the browser via RTDB ──
|
|
163
|
+
pushIndexSnapshot(index) {
|
|
164
|
+
const snapshotRef = ref(this.db, `tunnels/${this.tunnelId}/indexSnapshot`);
|
|
165
|
+
// Push as a single message — the browser hydrates from this
|
|
166
|
+
set(snapshotRef, {
|
|
167
|
+
symbols: index.symbols,
|
|
168
|
+
strings: index.strings,
|
|
169
|
+
stats: index.stats,
|
|
170
|
+
timestamp: serverTimestamp(),
|
|
171
|
+
});
|
|
172
|
+
console.log(` ${'\x1b[36m'}📡 Index snapshot pushed to browser${'\x1b[0m'}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Push an incremental delta when a file changes ──
|
|
176
|
+
async pushIndexDelta(relPath) {
|
|
177
|
+
const ext = extname(relPath);
|
|
178
|
+
const SUPPORTED_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs'];
|
|
179
|
+
if (!SUPPORTED_EXTS.includes(ext)) return;
|
|
180
|
+
|
|
181
|
+
const delta = await reindexFile(this.cwd, '/' + relPath);
|
|
182
|
+
if (!delta) return;
|
|
183
|
+
|
|
184
|
+
const deltaRef = ref(this.db, `tunnels/${this.tunnelId}/indexDeltas`);
|
|
185
|
+
push(deltaRef, {
|
|
186
|
+
filePath: delta.filePath,
|
|
187
|
+
symbols: delta.symbols,
|
|
188
|
+
strings: delta.strings,
|
|
189
|
+
timestamp: serverTimestamp(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
90
193
|
async handleCommand(command) {
|
|
91
194
|
const { type, messageId } = command;
|
|
92
195
|
switch (type) {
|
|
@@ -117,8 +220,12 @@ export class LucenaAgent {
|
|
|
117
220
|
async executeCommand({ messageId, command }) {
|
|
118
221
|
const child = spawn('sh', ['-c', command], { cwd: this.cwd });
|
|
119
222
|
|
|
120
|
-
child.stdout.on('data', (data) =>
|
|
121
|
-
|
|
223
|
+
child.stdout.on('data', (data) => {
|
|
224
|
+
this.pushResponse(messageId, 'output', stripCwd(this.cwd, data.toString()));
|
|
225
|
+
});
|
|
226
|
+
child.stderr.on('data', (data) => {
|
|
227
|
+
this.pushResponse(messageId, 'output', stripCwd(this.cwd, data.toString()));
|
|
228
|
+
});
|
|
122
229
|
child.on('close', (code) => {
|
|
123
230
|
this.pushResponse(messageId, 'done', '', { exitCode: code ?? 0 });
|
|
124
231
|
this.activeCommands.delete(messageId);
|
|
@@ -134,18 +241,19 @@ export class LucenaAgent {
|
|
|
134
241
|
this.pushResponse(messageId, 'output', content);
|
|
135
242
|
this.pushResponse(messageId, 'done', '');
|
|
136
243
|
} catch (err) {
|
|
137
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
244
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
138
245
|
}
|
|
139
246
|
}
|
|
140
247
|
|
|
141
248
|
async writeFileCmd({ messageId, path: filePath, content }) {
|
|
142
249
|
const fullPath = getJailedPath(this.cwd, filePath);
|
|
250
|
+
const relPath = '/' + relative(this.cwd, fullPath);
|
|
143
251
|
try {
|
|
144
252
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
145
253
|
await writeFile(fullPath, content, 'utf-8');
|
|
146
|
-
this.pushResponse(messageId, 'done', `Wrote ${
|
|
254
|
+
this.pushResponse(messageId, 'done', `Wrote ${relPath}`);
|
|
147
255
|
} catch (err) {
|
|
148
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
256
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
149
257
|
}
|
|
150
258
|
}
|
|
151
259
|
|
|
@@ -159,7 +267,7 @@ export class LucenaAgent {
|
|
|
159
267
|
.join('\n');
|
|
160
268
|
this.pushResponse(messageId, 'done', listing || '(empty)');
|
|
161
269
|
} catch (err) {
|
|
162
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
270
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
163
271
|
}
|
|
164
272
|
}
|
|
165
273
|
|
|
@@ -175,31 +283,33 @@ export class LucenaAgent {
|
|
|
175
283
|
created: s.birthtime
|
|
176
284
|
}));
|
|
177
285
|
} catch (err) {
|
|
178
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
286
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
179
287
|
}
|
|
180
288
|
}
|
|
181
289
|
|
|
182
290
|
async deleteFile({ messageId, path: filePath }) {
|
|
183
291
|
const fullPath = getJailedPath(this.cwd, filePath);
|
|
292
|
+
const relPath = '/' + relative(this.cwd, fullPath);
|
|
184
293
|
try {
|
|
185
294
|
if ((await stat(fullPath)).isDirectory()) {
|
|
186
295
|
await rm(fullPath, { recursive: true });
|
|
187
296
|
} else {
|
|
188
297
|
await unlink(fullPath);
|
|
189
298
|
}
|
|
190
|
-
this.pushResponse(messageId, 'done', `Deleted ${
|
|
299
|
+
this.pushResponse(messageId, 'done', `Deleted ${relPath}`);
|
|
191
300
|
} catch (err) {
|
|
192
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
301
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
193
302
|
}
|
|
194
303
|
}
|
|
195
304
|
|
|
196
305
|
async mkdirCmd({ messageId, path: dirPath }) {
|
|
197
306
|
const fullPath = getJailedPath(this.cwd, dirPath);
|
|
307
|
+
const relPath = '/' + relative(this.cwd, fullPath);
|
|
198
308
|
try {
|
|
199
309
|
await mkdir(fullPath, { recursive: true });
|
|
200
|
-
this.pushResponse(messageId, 'done', `Created ${
|
|
310
|
+
this.pushResponse(messageId, 'done', `Created ${relPath}`);
|
|
201
311
|
} catch (err) {
|
|
202
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
312
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
203
313
|
}
|
|
204
314
|
}
|
|
205
315
|
|
|
@@ -216,10 +326,10 @@ export class LucenaAgent {
|
|
|
216
326
|
child.stderr.on('data', (d) => { output += d.toString(); });
|
|
217
327
|
|
|
218
328
|
child.on('close', (code) => {
|
|
219
|
-
this.pushResponse(messageId, 'done', output || 'No matches found');
|
|
329
|
+
this.pushResponse(messageId, 'done', stripCwd(this.cwd, output) || 'No matches found');
|
|
220
330
|
});
|
|
221
331
|
} catch (err) {
|
|
222
|
-
this.pushResponse(messageId, 'error', err.message);
|
|
332
|
+
this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
|
|
223
333
|
}
|
|
224
334
|
}
|
|
225
335
|
|
|
@@ -244,6 +354,11 @@ export class LucenaAgent {
|
|
|
244
354
|
path: relPath,
|
|
245
355
|
timestamp: serverTimestamp()
|
|
246
356
|
});
|
|
357
|
+
|
|
358
|
+
// ── Push incremental index delta for changed files ──
|
|
359
|
+
if (event === 'change' || event === 'add') {
|
|
360
|
+
this.pushIndexDelta(relPath).catch(() => {}); // Non-blocking, non-fatal
|
|
361
|
+
}
|
|
247
362
|
});
|
|
248
363
|
}
|
|
249
364
|
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// src/cli-indexer.js — Tree-sitter WASM indexer for the CLI agent
|
|
2
|
+
// Runs on the user's machine at startup, parses all code files locally,
|
|
3
|
+
// and builds a symbol + string index that can be pushed to the browser
|
|
4
|
+
// via RTDB when the tunnel connects.
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir } from 'fs/promises';
|
|
7
|
+
import { join, extname, relative } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// ── Language Support ──
|
|
16
|
+
const LANGUAGE_MAP = {
|
|
17
|
+
'.js': 'javascript',
|
|
18
|
+
'.jsx': 'javascript',
|
|
19
|
+
'.ts': 'typescript',
|
|
20
|
+
'.tsx': 'tsx',
|
|
21
|
+
'.py': 'python',
|
|
22
|
+
'.rb': 'ruby',
|
|
23
|
+
'.go': 'go',
|
|
24
|
+
'.rs': 'rust',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const IGNORED_PATTERNS = [
|
|
28
|
+
'node_modules', '.git', '.next', '.wrangler', '.DS_Store',
|
|
29
|
+
'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase',
|
|
30
|
+
'.WorkspaceBrain',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ── Parser singleton ──
|
|
34
|
+
let _Parser = null;
|
|
35
|
+
let _Language = null;
|
|
36
|
+
let isInitialized = false;
|
|
37
|
+
const languageCache = new Map();
|
|
38
|
+
|
|
39
|
+
function getGrammarsDir() {
|
|
40
|
+
return join(__dirname, '..', 'grammars');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function initParser() {
|
|
44
|
+
if (isInitialized) return;
|
|
45
|
+
const mod = await import('web-tree-sitter');
|
|
46
|
+
_Parser = mod.Parser;
|
|
47
|
+
_Language = mod.Language;
|
|
48
|
+
|
|
49
|
+
const grammarsDir = getGrammarsDir();
|
|
50
|
+
|
|
51
|
+
await _Parser.init({
|
|
52
|
+
locateFile: (scriptName) => {
|
|
53
|
+
return join(grammarsDir, scriptName);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
isInitialized = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadLanguage(langExt) {
|
|
60
|
+
if (languageCache.has(langExt)) return languageCache.get(langExt);
|
|
61
|
+
|
|
62
|
+
const langName = LANGUAGE_MAP[langExt];
|
|
63
|
+
if (!langName) throw new Error(`Unsupported language: ${langExt}`);
|
|
64
|
+
|
|
65
|
+
const grammarsDir = getGrammarsDir();
|
|
66
|
+
const wasmFile = join(grammarsDir, `tree-sitter-${langName}.wasm`);
|
|
67
|
+
const language = await _Language.load(wasmFile);
|
|
68
|
+
languageCache.set(langExt, language);
|
|
69
|
+
return language;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Chunk Extraction (mirrors ast-worker.js exactly) ──
|
|
73
|
+
|
|
74
|
+
function extractChunks(rootNode, code) {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
const strings = [];
|
|
77
|
+
|
|
78
|
+
function walk(node, depth, parentType) {
|
|
79
|
+
const type = node.type;
|
|
80
|
+
|
|
81
|
+
// ── Imports ──
|
|
82
|
+
if (type === 'import_declaration' || type === 'import_statement' || type === 'import_from_statement') {
|
|
83
|
+
chunks.push({
|
|
84
|
+
type: 'import',
|
|
85
|
+
name: node.text.slice(0, 80),
|
|
86
|
+
startByte: node.startIndex,
|
|
87
|
+
endByte: node.endIndex,
|
|
88
|
+
text: node.text,
|
|
89
|
+
});
|
|
90
|
+
extractStringsFromNode(node, strings);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Classes ──
|
|
95
|
+
if (type === 'class_declaration' || type === 'class_definition' || type === 'class') {
|
|
96
|
+
const nameNode = node.childForFieldName('name');
|
|
97
|
+
const name = nameNode ? nameNode.text : '<anonymous>';
|
|
98
|
+
const isExported = isExportedNode(node);
|
|
99
|
+
chunks.push({
|
|
100
|
+
type: 'class',
|
|
101
|
+
name,
|
|
102
|
+
exported: isExported,
|
|
103
|
+
startByte: node.startIndex,
|
|
104
|
+
endByte: node.endIndex,
|
|
105
|
+
text: node.text,
|
|
106
|
+
});
|
|
107
|
+
extractNestedNames(node, chunks, name);
|
|
108
|
+
extractStringsFromNode(node, strings);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Function declarations ──
|
|
113
|
+
if (type === 'function_declaration' || type === 'function_definition' || type === 'method_definition') {
|
|
114
|
+
if (depth > 1) {
|
|
115
|
+
extractStringsFromNode(node, strings);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const nameNode = node.childForFieldName('name');
|
|
119
|
+
const name = nameNode ? nameNode.text : '<anonymous>';
|
|
120
|
+
const isExported = isExportedNode(node) || isExportedNode(node.parent);
|
|
121
|
+
chunks.push({
|
|
122
|
+
type: 'function',
|
|
123
|
+
name,
|
|
124
|
+
exported: isExported,
|
|
125
|
+
startByte: node.startIndex,
|
|
126
|
+
endByte: node.endIndex,
|
|
127
|
+
text: node.text,
|
|
128
|
+
});
|
|
129
|
+
extractStringsFromNode(node, strings);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Variable declarations (top-level only) ──
|
|
134
|
+
if ((type === 'variable_declarator' || type === 'variable_declaration' || type === 'lexical_declaration') && depth === 0) {
|
|
135
|
+
if (type === 'variable_declaration' || type === 'lexical_declaration') {
|
|
136
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
137
|
+
const child = node.child(i);
|
|
138
|
+
if (child.type === 'variable_declarator') {
|
|
139
|
+
const nameNode = child.childForFieldName('name');
|
|
140
|
+
const valueNode = child.childForFieldName('value');
|
|
141
|
+
const name = nameNode ? nameNode.text : '<unknown>';
|
|
142
|
+
const isArrowFn = valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function');
|
|
143
|
+
const isExported = isExportedNode(node);
|
|
144
|
+
chunks.push({
|
|
145
|
+
type: isArrowFn ? 'function' : 'variable',
|
|
146
|
+
name,
|
|
147
|
+
exported: isExported,
|
|
148
|
+
startByte: node.startIndex,
|
|
149
|
+
endByte: node.endIndex,
|
|
150
|
+
text: node.text,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
extractStringsFromNode(node, strings);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nameNode = node.childForFieldName('name');
|
|
159
|
+
const valueNode = node.childForFieldName('value');
|
|
160
|
+
const name = nameNode ? nameNode.text : '<unknown>';
|
|
161
|
+
const isArrowFn = valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function');
|
|
162
|
+
const isExported = isExportedNode(node.parent);
|
|
163
|
+
chunks.push({
|
|
164
|
+
type: isArrowFn ? 'function' : 'variable',
|
|
165
|
+
name,
|
|
166
|
+
exported: isExported,
|
|
167
|
+
startByte: node.parent ? node.parent.startIndex : node.startIndex,
|
|
168
|
+
endByte: node.parent ? node.parent.endIndex : node.endIndex,
|
|
169
|
+
text: node.parent ? node.parent.text : node.text,
|
|
170
|
+
});
|
|
171
|
+
extractStringsFromNode(node, strings);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Python assignments at top level ──
|
|
176
|
+
if (type === 'assignment' && depth === 0) {
|
|
177
|
+
const leftNode = node.childForFieldName('left');
|
|
178
|
+
const name = leftNode ? leftNode.text : '<unknown>';
|
|
179
|
+
chunks.push({
|
|
180
|
+
type: 'variable',
|
|
181
|
+
name,
|
|
182
|
+
exported: false,
|
|
183
|
+
startByte: node.startIndex,
|
|
184
|
+
endByte: node.endIndex,
|
|
185
|
+
text: node.text,
|
|
186
|
+
});
|
|
187
|
+
extractStringsFromNode(node, strings);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Go top-level var/const ──
|
|
192
|
+
if ((type === 'var_declaration' || type === 'const_declaration') && depth === 0) {
|
|
193
|
+
chunks.push({
|
|
194
|
+
type: 'variable',
|
|
195
|
+
name: node.text.slice(0, 60),
|
|
196
|
+
exported: false,
|
|
197
|
+
startByte: node.startIndex,
|
|
198
|
+
endByte: node.endIndex,
|
|
199
|
+
text: node.text,
|
|
200
|
+
});
|
|
201
|
+
extractStringsFromNode(node, strings);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Ruby method definitions ──
|
|
206
|
+
if (type === 'method' || type === 'singleton_method') {
|
|
207
|
+
const nameNode = node.childForFieldName('name');
|
|
208
|
+
const name = nameNode ? nameNode.text : '<anonymous>';
|
|
209
|
+
chunks.push({
|
|
210
|
+
type: 'function',
|
|
211
|
+
name,
|
|
212
|
+
exported: false,
|
|
213
|
+
startByte: node.startIndex,
|
|
214
|
+
endByte: node.endIndex,
|
|
215
|
+
text: node.text,
|
|
216
|
+
});
|
|
217
|
+
extractStringsFromNode(node, strings);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Rust function definitions ──
|
|
222
|
+
if (type === 'function_item' || type === 'function_signature_item') {
|
|
223
|
+
const nameNode = node.childForFieldName('name');
|
|
224
|
+
const name = nameNode ? nameNode.text : '<anonymous>';
|
|
225
|
+
const isExported = isExportedNode(node) || isExportedNode(node.parent);
|
|
226
|
+
chunks.push({
|
|
227
|
+
type: 'function',
|
|
228
|
+
name,
|
|
229
|
+
exported: isExported,
|
|
230
|
+
startByte: node.startIndex,
|
|
231
|
+
endByte: node.endIndex,
|
|
232
|
+
text: node.text,
|
|
233
|
+
});
|
|
234
|
+
extractStringsFromNode(node, strings);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Rust impl blocks ──
|
|
239
|
+
if (type === 'impl_item') {
|
|
240
|
+
const typeNode = node.childForFieldName('type');
|
|
241
|
+
const name = typeNode ? typeNode.text : '<impl>';
|
|
242
|
+
chunks.push({
|
|
243
|
+
type: 'class',
|
|
244
|
+
name,
|
|
245
|
+
exported: false,
|
|
246
|
+
startByte: node.startIndex,
|
|
247
|
+
endByte: node.endIndex,
|
|
248
|
+
text: node.text,
|
|
249
|
+
});
|
|
250
|
+
extractStringsFromNode(node, strings);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Export statements wrapping other declarations ──
|
|
255
|
+
if (type === 'export_statement' || type === 'export_default_declaration') {
|
|
256
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
257
|
+
walk(node.child(i), depth, type);
|
|
258
|
+
}
|
|
259
|
+
extractStringsFromNode(node, strings);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Continue walking children ──
|
|
264
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
265
|
+
walk(node.child(i), depth + 1, type);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
walk(rootNode, 0, null);
|
|
270
|
+
return { chunks, strings };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isExportedNode(node) {
|
|
274
|
+
if (!node) return false;
|
|
275
|
+
if (node.type === 'export_statement' || node.type === 'export_default_declaration') return true;
|
|
276
|
+
if (node.type === 'pub') return true; // Rust
|
|
277
|
+
if (node.parent && (node.parent.type === 'export_statement' || node.parent.type === 'export_default_declaration')) return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractNestedNames(node, chunks, parentName) {
|
|
282
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
283
|
+
const child = node.child(i);
|
|
284
|
+
if (child.type === 'method_definition' || child.type === 'function_definition' || child.type === 'method') {
|
|
285
|
+
const nameNode = child.childForFieldName('name');
|
|
286
|
+
if (nameNode) {
|
|
287
|
+
chunks.push({
|
|
288
|
+
type: 'nested_function',
|
|
289
|
+
name: nameNode.text,
|
|
290
|
+
parent: parentName,
|
|
291
|
+
startByte: child.startIndex,
|
|
292
|
+
endByte: child.endIndex,
|
|
293
|
+
text: child.text,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function extractStringsFromNode(node, strings) {
|
|
301
|
+
const stringTypes = new Set([
|
|
302
|
+
'string', 'string_literal', 'template_string', 'template_literal',
|
|
303
|
+
'comment', 'line_comment', 'block_comment', 'jsx_text',
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
function walkStrings(n) {
|
|
307
|
+
if (stringTypes.has(n.type)) {
|
|
308
|
+
const text = n.text.trim();
|
|
309
|
+
if (text.length > 1 && text.length < 500) {
|
|
310
|
+
strings.push({
|
|
311
|
+
text,
|
|
312
|
+
startByte: n.startIndex,
|
|
313
|
+
endByte: n.endIndex,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
319
|
+
walkStrings(n.child(i));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
walkStrings(node);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── File Tree Walker ──
|
|
327
|
+
|
|
328
|
+
async function walkDirectory(dirPath, cwd, results = []) {
|
|
329
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
330
|
+
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (IGNORED_PATTERNS.some(p => entry.name.includes(p))) continue;
|
|
333
|
+
|
|
334
|
+
const fullPath = join(dirPath, entry.name);
|
|
335
|
+
if (entry.isDirectory()) {
|
|
336
|
+
await walkDirectory(fullPath, cwd, results);
|
|
337
|
+
} else if (entry.isFile()) {
|
|
338
|
+
const ext = extname(entry.name);
|
|
339
|
+
if (LANGUAGE_MAP[ext]) {
|
|
340
|
+
results.push({
|
|
341
|
+
filePath: '/' + relative(cwd, fullPath),
|
|
342
|
+
fullPath,
|
|
343
|
+
ext,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Main Index Builder ──
|
|
352
|
+
|
|
353
|
+
export async function buildIndex(cwd, onProgress) {
|
|
354
|
+
await initParser();
|
|
355
|
+
|
|
356
|
+
const files = await walkDirectory(cwd, cwd);
|
|
357
|
+
const totalFiles = files.length;
|
|
358
|
+
|
|
359
|
+
if (onProgress) onProgress({ phase: 'parsing', current: 0, total: totalFiles });
|
|
360
|
+
|
|
361
|
+
// Symbol index: Array<{ name, filePath, type, exported, parent }>
|
|
362
|
+
const symbolEntries = [];
|
|
363
|
+
// String index: Array<{ text, filePath }>
|
|
364
|
+
const stringEntries = [];
|
|
365
|
+
|
|
366
|
+
let parsed = 0;
|
|
367
|
+
let errors = 0;
|
|
368
|
+
|
|
369
|
+
for (const file of files) {
|
|
370
|
+
try {
|
|
371
|
+
const code = await readFile(file.fullPath, 'utf-8');
|
|
372
|
+
const lang = await loadLanguage(file.ext);
|
|
373
|
+
const parser = new _Parser();
|
|
374
|
+
parser.setLanguage(lang);
|
|
375
|
+
const tree = parser.parse(code);
|
|
376
|
+
const { chunks, strings } = extractChunks(tree.rootNode, code);
|
|
377
|
+
tree.delete();
|
|
378
|
+
|
|
379
|
+
// Build symbol entries
|
|
380
|
+
for (const chunk of chunks) {
|
|
381
|
+
if (chunk.type === 'import') continue; // Skip imports for symbol index
|
|
382
|
+
symbolEntries.push({
|
|
383
|
+
name: chunk.name,
|
|
384
|
+
filePath: file.filePath,
|
|
385
|
+
type: chunk.type,
|
|
386
|
+
exported: chunk.exported || false,
|
|
387
|
+
parent: chunk.parent || null,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Build string entries
|
|
392
|
+
for (const str of strings) {
|
|
393
|
+
stringEntries.push({
|
|
394
|
+
text: str.text,
|
|
395
|
+
filePath: file.filePath,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
parsed++;
|
|
400
|
+
if (onProgress && parsed % 25 === 0) {
|
|
401
|
+
onProgress({ phase: 'parsing', current: parsed, total: totalFiles });
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
errors++;
|
|
405
|
+
console.error(`[cli-indexer] Error parsing ${file.filePath}: ${err.message}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (onProgress) onProgress({ phase: 'done', current: parsed, total: totalFiles });
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
symbols: symbolEntries,
|
|
413
|
+
strings: stringEntries,
|
|
414
|
+
stats: {
|
|
415
|
+
filesParsed: parsed,
|
|
416
|
+
filesErrored: errors,
|
|
417
|
+
symbolCount: symbolEntries.length,
|
|
418
|
+
stringCount: stringEntries.length,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Single-file re-index (for incremental updates) ──
|
|
424
|
+
|
|
425
|
+
export async function reindexFile(cwd, relPath) {
|
|
426
|
+
await initParser();
|
|
427
|
+
|
|
428
|
+
const ext = extname(relPath);
|
|
429
|
+
const langName = LANGUAGE_MAP[ext];
|
|
430
|
+
if (!langName) return null;
|
|
431
|
+
|
|
432
|
+
const fullPath = join(cwd, relPath.replace(/^\//, ''));
|
|
433
|
+
if (!existsSync(fullPath)) return { filePath: relPath, symbols: [], strings: [] };
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const code = await readFile(fullPath, 'utf-8');
|
|
437
|
+
const lang = await loadLanguage(ext);
|
|
438
|
+
const parser = new _Parser();
|
|
439
|
+
parser.setLanguage(lang);
|
|
440
|
+
const tree = parser.parse(code);
|
|
441
|
+
const { chunks, strings } = extractChunks(tree.rootNode, code);
|
|
442
|
+
tree.delete();
|
|
443
|
+
|
|
444
|
+
const symbolEntries = [];
|
|
445
|
+
for (const chunk of chunks) {
|
|
446
|
+
if (chunk.type === 'import') continue;
|
|
447
|
+
symbolEntries.push({
|
|
448
|
+
name: chunk.name,
|
|
449
|
+
filePath: relPath,
|
|
450
|
+
type: chunk.type,
|
|
451
|
+
exported: chunk.exported || false,
|
|
452
|
+
parent: chunk.parent || null,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const stringEntries = strings.map(s => ({
|
|
457
|
+
text: s.text,
|
|
458
|
+
filePath: relPath,
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
return { filePath: relPath, symbols: symbolEntries, strings: stringEntries };
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.error(`[cli-indexer] Error re-indexing ${relPath}: ${err.message}`);
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
package/src/main.js
CHANGED
|
@@ -42,7 +42,6 @@ export async function main() {
|
|
|
42
42
|
console.log(` ${c.cyan}📍 Scoped to:${c.reset} ${cwd}`);
|
|
43
43
|
console.log(` ${c.yellow}🛡️ Safe Mode:${c.reset} ON by default (All edits require approval)`);
|
|
44
44
|
console.log(` ${c.dim}Optionally switch to YOLO on LucenaCoder.com${c.reset}\n`);
|
|
45
|
-
console.log(` Starting tunnel...`);
|
|
46
45
|
|
|
47
46
|
const agent = new LucenaAgent(cwd);
|
|
48
47
|
|
|
@@ -56,8 +55,10 @@ export async function main() {
|
|
|
56
55
|
process.on('SIGTERM', shutdown);
|
|
57
56
|
|
|
58
57
|
try {
|
|
58
|
+
console.log(` ${c.dim}⏳ Indexing workspace...${c.reset}`);
|
|
59
59
|
const tunnelId = await agent.start();
|
|
60
|
-
console.log(
|
|
60
|
+
console.log(` ${c.green}✔ Indexed${c.reset}\n`);
|
|
61
|
+
console.log(` ${c.green}✔ Tunnel active!${c.reset}\n`);
|
|
61
62
|
|
|
62
63
|
const idLabel = "Tunnel ID:";
|
|
63
64
|
const boxWidth = idLabel.length + tunnelId.length + 5;
|