@lucenaone/coder 1.1.6 → 1.1.17
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/package.json +1 -1
- package/src/agent.js +78 -7
- package/src/main.js +30 -3
- package/src/pro-token.js +47 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -8,10 +8,13 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
8
8
|
import { FIREBASE_CONFIG } from './config.js';
|
|
9
9
|
import { buildIndex, reindexFile } from './cli-indexer.js';
|
|
10
10
|
import { LucenaShell } from './lucena-shell.js';
|
|
11
|
+
import { storeProToken } from './pro-token.js';
|
|
11
12
|
|
|
12
13
|
const IGNORED_PATTERNS = [
|
|
13
14
|
'node_modules', '.git', '.next', '.wrangler', '.DS_Store',
|
|
14
|
-
'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase'
|
|
15
|
+
'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase',
|
|
16
|
+
'.venv', 'venv', '__pycache__', '.pytest_cache', '.mypy_cache',
|
|
17
|
+
'coverage', '.coverage', 'target', 'out', '.gradle'
|
|
15
18
|
];
|
|
16
19
|
|
|
17
20
|
const SEARCH_GLOB = '*.{js,jsx,ts,tsx,json,md,css,html,py,rb,go,rs}';
|
|
@@ -143,6 +146,11 @@ export class LucenaAgent {
|
|
|
143
146
|
if (typeof data.stripCwd === 'boolean') {
|
|
144
147
|
this.stripCwd = data.stripCwd;
|
|
145
148
|
}
|
|
149
|
+
|
|
150
|
+
if (data.proToken) {
|
|
151
|
+
await storeProToken({ tokenForPro: data.proToken, email: data.proEmail });
|
|
152
|
+
console.log(` ${'\x1b[36m'}PRO token stored. Future runs can auto-launch.${'\x1b[0m'}`);
|
|
153
|
+
}
|
|
146
154
|
|
|
147
155
|
// Index is guaranteed ready (awaited in start()), push immediately
|
|
148
156
|
if (this.indexData) {
|
|
@@ -210,16 +218,24 @@ export class LucenaAgent {
|
|
|
210
218
|
case 'execute': return this.executeCommand(command);
|
|
211
219
|
case 'read_file': return this.readFileCmd(command);
|
|
212
220
|
case 'write_file': return this.writeFileCmd(command);
|
|
221
|
+
case 'list_files': return this.listFiles(command);
|
|
213
222
|
case 'list_dir': return this.listDir(command);
|
|
214
223
|
case 'stat': return this.statFile(command);
|
|
215
224
|
case 'delete_file': return this.deleteFile(command);
|
|
216
225
|
case 'mkdir': return this.mkdirCmd(command);
|
|
217
226
|
case 'search': return this.searchCodebase(command);
|
|
227
|
+
case 'store_pro_token': return this.storeProTokenCmd(command);
|
|
218
228
|
case 'ping': return this.pushResponse(messageId, 'pong', '');
|
|
219
229
|
default: return this.pushResponse(messageId, 'error', `Unknown command type: ${type}`);
|
|
220
230
|
}
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
async storeProTokenCmd({ messageId, tokenForPro, email }) {
|
|
234
|
+
if (!tokenForPro) return this.pushResponse(messageId, 'error', 'tokenForPro is required');
|
|
235
|
+
await storeProToken({ tokenForPro, email });
|
|
236
|
+
this.pushResponse(messageId, 'done', 'PRO token stored');
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
pushResponse(messageId, type, text, extra = {}) {
|
|
224
240
|
const responsesRef = ref(this.db, `tunnels/${this.tunnelId}/responses`);
|
|
225
241
|
push(responsesRef, {
|
|
@@ -272,7 +288,7 @@ export class LucenaAgent {
|
|
|
272
288
|
|
|
273
289
|
async writeFileCmd({ messageId, path: filePath, content }) {
|
|
274
290
|
const fullPath = getJailedPath(this.cwd, filePath);
|
|
275
|
-
const relPath =
|
|
291
|
+
const relPath = toBrowserPath(relative(this.cwd, fullPath));
|
|
276
292
|
try {
|
|
277
293
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
278
294
|
await writeFile(fullPath, content, 'utf-8');
|
|
@@ -296,6 +312,38 @@ export class LucenaAgent {
|
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
314
|
|
|
315
|
+
async listFiles({ messageId }) {
|
|
316
|
+
try {
|
|
317
|
+
const files = await this.walkFiles(this.cwd);
|
|
318
|
+
this.pushResponse(messageId, 'done', JSON.stringify(files));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
this.pushResponse(messageId, 'error', this._sanitize(err.message));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async walkFiles(dirPath, relativeDir = '') {
|
|
325
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
326
|
+
const files = [];
|
|
327
|
+
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (IGNORED_PATTERNS.includes(entry.name)) continue;
|
|
330
|
+
|
|
331
|
+
const relPath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
332
|
+
const fullPath = join(dirPath, entry.name);
|
|
333
|
+
|
|
334
|
+
if (entry.isDirectory()) {
|
|
335
|
+
files.push(...await this.walkFiles(fullPath, relPath));
|
|
336
|
+
} else if (entry.isFile()) {
|
|
337
|
+
files.push({
|
|
338
|
+
path: relPath,
|
|
339
|
+
lineCount: await countTextFileLines(fullPath),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return files;
|
|
345
|
+
}
|
|
346
|
+
|
|
299
347
|
async statFile({ messageId, path: filePath }) {
|
|
300
348
|
const fullPath = getJailedPath(this.cwd, filePath);
|
|
301
349
|
try {
|
|
@@ -314,7 +362,7 @@ export class LucenaAgent {
|
|
|
314
362
|
|
|
315
363
|
async deleteFile({ messageId, path: filePath }) {
|
|
316
364
|
const fullPath = getJailedPath(this.cwd, filePath);
|
|
317
|
-
const relPath =
|
|
365
|
+
const relPath = toBrowserPath(relative(this.cwd, fullPath));
|
|
318
366
|
try {
|
|
319
367
|
if ((await stat(fullPath)).isDirectory()) {
|
|
320
368
|
await rm(fullPath, { recursive: true });
|
|
@@ -329,7 +377,7 @@ export class LucenaAgent {
|
|
|
329
377
|
|
|
330
378
|
async mkdirCmd({ messageId, path: dirPath }) {
|
|
331
379
|
const fullPath = getJailedPath(this.cwd, dirPath);
|
|
332
|
-
const relPath =
|
|
380
|
+
const relPath = toBrowserPath(relative(this.cwd, fullPath));
|
|
333
381
|
try {
|
|
334
382
|
await mkdir(fullPath, { recursive: true });
|
|
335
383
|
this.pushResponse(messageId, 'done', `Created ${relPath}`);
|
|
@@ -390,22 +438,23 @@ export class LucenaAgent {
|
|
|
390
438
|
|
|
391
439
|
this.watcher.on('all', (event, filePath) => {
|
|
392
440
|
const relPath = relative(this.cwd, filePath);
|
|
441
|
+
const normalizedRelPath = relPath.replace(/\\/g, '/');
|
|
393
442
|
|
|
394
443
|
// Stop anything escaping local disk
|
|
395
|
-
if (!
|
|
444
|
+
if (!normalizedRelPath || normalizedRelPath.startsWith('..') || isAbsolute(relPath)) {
|
|
396
445
|
return;
|
|
397
446
|
}
|
|
398
447
|
|
|
399
448
|
const changesRef = ref(this.db, `tunnels/${this.tunnelId}/fileChanges`);
|
|
400
449
|
push(changesRef, {
|
|
401
450
|
event,
|
|
402
|
-
path:
|
|
451
|
+
path: normalizedRelPath,
|
|
403
452
|
timestamp: serverTimestamp()
|
|
404
453
|
});
|
|
405
454
|
|
|
406
455
|
// ── Push incremental index delta for changed files ──
|
|
407
456
|
if (event === 'change' || event === 'add') {
|
|
408
|
-
this.pushIndexDelta(
|
|
457
|
+
this.pushIndexDelta(normalizedRelPath).catch(() => {}); // Non-blocking, non-fatal
|
|
409
458
|
}
|
|
410
459
|
});
|
|
411
460
|
}
|
|
@@ -426,3 +475,25 @@ export class LucenaAgent {
|
|
|
426
475
|
this.connected = false;
|
|
427
476
|
}
|
|
428
477
|
}
|
|
478
|
+
|
|
479
|
+
function toBrowserPath(pathValue) {
|
|
480
|
+
return '/' + String(pathValue || '').replace(/\\/g, '/');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function countTextFileLines(fullPath) {
|
|
484
|
+
if (!isTextLikeFile(fullPath)) return null;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const info = await stat(fullPath);
|
|
488
|
+
if (info.size > 5 * 1024 * 1024) return null;
|
|
489
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
490
|
+
if (!content) return 0;
|
|
491
|
+
return content.split('\n').length;
|
|
492
|
+
} catch {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function isTextLikeFile(filePath) {
|
|
498
|
+
return /\.(cjs|css|csv|go|html?|js|jsx|json|mdx?|mjs|py|rb|rs|sql|svg|ts|tsx|txt|xml|ya?ml)$/i.test(filePath);
|
|
499
|
+
}
|
package/src/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/main.js — CLI entry point for the Lucena agent
|
|
2
2
|
import { LucenaAgent } from './agent.js';
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { validateStoredProToken } from './pro-token.js';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
// Standard ANSI Terminal Colors
|
|
@@ -35,13 +36,28 @@ ${c.dim} =========================================${c.reset}
|
|
|
35
36
|
to your local folders.
|
|
36
37
|
`;
|
|
37
38
|
|
|
39
|
+
function openBrowser(url) {
|
|
40
|
+
const command = process.platform === 'darwin'
|
|
41
|
+
? 'open'
|
|
42
|
+
: process.platform === 'win32'
|
|
43
|
+
? 'cmd'
|
|
44
|
+
: 'xdg-open';
|
|
45
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
46
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
47
|
+
child.unref();
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
export async function main() {
|
|
39
51
|
const cwd = process.cwd();
|
|
52
|
+
const proStatus = await validateStoredProToken();
|
|
40
53
|
|
|
41
54
|
console.log(BANNER);
|
|
42
55
|
console.log(` ${c.cyan}📍 Scoped to:${c.reset} ${cwd}`);
|
|
43
56
|
console.log(` ${c.yellow}🛡️ Safe Mode:${c.reset} ON by default (All edits require approval)`);
|
|
44
57
|
console.log(` ${c.dim}Optionally switch to YOLO on LucenaCoder.com${c.reset}\n`);
|
|
58
|
+
if (proStatus.valid) {
|
|
59
|
+
console.log(` ${c.cyan}PRO detected.${c.reset} Browser auto-launch enabled.\n`);
|
|
60
|
+
}
|
|
45
61
|
|
|
46
62
|
const agent = new LucenaAgent(cwd);
|
|
47
63
|
|
|
@@ -72,11 +88,22 @@ export async function main() {
|
|
|
72
88
|
const urlBoxWidth = urlLabel.length + webUrl.length + 5;
|
|
73
89
|
const urlBorder = '─'.repeat(urlBoxWidth);
|
|
74
90
|
|
|
91
|
+
if (proStatus.valid) {
|
|
92
|
+
try {
|
|
93
|
+
openBrowser(webUrl);
|
|
94
|
+
console.log(`\n ${c.green}✔ Opening LucenaCoder...${c.reset}`);
|
|
95
|
+
} catch {
|
|
96
|
+
console.log(`\n ${c.yellow}Could not auto-open your browser.${c.reset}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
console.log(`\n ${c.dim}┌${urlBorder}┐${c.reset}`);
|
|
76
101
|
console.log(` ${c.dim}│${c.reset} ${urlLabel}${c.reset} ${c.bold}${webUrl}${c.reset} ${c.dim}│${c.reset}`);
|
|
77
102
|
console.log(` ${c.dim}└${urlBorder}┘${c.reset}`);
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
|
|
104
|
+
if (!proStatus.valid) {
|
|
105
|
+
console.log(`\n ${c.dim}Open the URL above in your browser to connect.${c.reset}`);
|
|
106
|
+
}
|
|
80
107
|
console.log(` ${c.dim}Press Ctrl+C to disconnect${c.reset}\n`);
|
|
81
108
|
} catch (err) {
|
|
82
109
|
console.error(`\n ${c.yellow}✖ Failed to start tunnel: ${err.message}${c.reset}\n`);
|
package/src/pro-token.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
const TOKEN_PATH = join(homedir(), '.lucenacoder', 'pro.json');
|
|
6
|
+
const VALIDATE_URL = process.env.LUCENA_VALIDATE_PRO_URL || 'https://lucenacoder.com/api/pro/validate-token';
|
|
7
|
+
|
|
8
|
+
export async function readStoredProToken() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(TOKEN_PATH, 'utf-8');
|
|
11
|
+
const data = JSON.parse(raw);
|
|
12
|
+
if (!data?.tokenForPro) return null;
|
|
13
|
+
return data;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function storeProToken({ tokenForPro, email }) {
|
|
20
|
+
if (!tokenForPro) return null;
|
|
21
|
+
const data = {
|
|
22
|
+
tokenForPro,
|
|
23
|
+
email: email || '',
|
|
24
|
+
savedAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
await mkdir(dirname(TOKEN_PATH), { recursive: true });
|
|
27
|
+
await writeFile(TOKEN_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function validateStoredProToken() {
|
|
32
|
+
const stored = await readStoredProToken();
|
|
33
|
+
if (!stored?.tokenForPro) return { valid: false };
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(VALIDATE_URL, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ tokenForPro: stored.tokenForPro }),
|
|
40
|
+
});
|
|
41
|
+
const payload = await response.json().catch(() => ({}));
|
|
42
|
+
if (!response.ok || !payload.valid) return { valid: false, stored };
|
|
43
|
+
return { valid: true, stored, ...payload };
|
|
44
|
+
} catch {
|
|
45
|
+
return { valid: false, stored };
|
|
46
|
+
}
|
|
47
|
+
}
|