@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucenaone/coder",
3
- "version": "1.1.6",
3
+ "version": "1.1.17",
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": {
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 = '/' + relative(this.cwd, fullPath);
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 = '/' + relative(this.cwd, fullPath);
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 = '/' + relative(this.cwd, fullPath);
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 (!relPath || relPath.startsWith('..') || isAbsolute(relPath)) {
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: relPath,
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(relPath).catch(() => {}); // Non-blocking, non-fatal
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 { basename } from 'path';
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
- console.log(`\n ${c.dim}Open the URL above in your browser to connect.${c.reset}`);
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`);
@@ -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
+ }