@lucenaone/coder 1.1.16 → 1.1.18

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.16",
3
+ "version": "1.1.18",
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,6 +8,7 @@ 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',
@@ -67,6 +68,7 @@ export class LucenaAgent {
67
68
  this.stripCwd = true; // Default: strip absolute paths (Browser Mode safety)
68
69
  this.indexData = null; // Pre-built index from CLI-side parsing
69
70
  this.indexPromise = null; // The in-flight indexing promise
71
+ this.browserConnected = false;
70
72
  this.shell = new LucenaShell(this.cwd);
71
73
  }
72
74
 
@@ -108,6 +110,9 @@ export class LucenaAgent {
108
110
  this.indexData = result;
109
111
  const { stats } = result;
110
112
  process.stdout.write(`\r ${'\x1b[32m'}✔ Indexed ${stats.filesParsed} files — ${stats.symbolCount} symbols, ${stats.stringCount} strings${'\x1b[0m'} \n`);
113
+ if (this.browserConnected && this.db) {
114
+ this.pushIndexSnapshot(result);
115
+ }
111
116
  return result;
112
117
  }).catch((err) => {
113
118
  console.warn(`\n ${'\x1b[33m'}⚠ Indexing failed: ${err.message}${'\x1b[0m'}`);
@@ -140,13 +145,18 @@ export class LucenaAgent {
140
145
  const data = snapshot.val();
141
146
  if (!data) return;
142
147
  remove(snapshot.ref);
148
+ this.browserConnected = true;
143
149
 
144
150
  // Browser tells us whether to strip cwd from output
145
151
  if (typeof data.stripCwd === 'boolean') {
146
152
  this.stripCwd = data.stripCwd;
147
153
  }
154
+
155
+ if (data.proToken) {
156
+ await storeProToken({ tokenForPro: data.proToken, email: data.proEmail });
157
+ console.log(` ${'\x1b[36m'}PRO token stored. Future runs can auto-launch.${'\x1b[0m'}`);
158
+ }
148
159
 
149
- // Index is guaranteed ready (awaited in start()), push immediately
150
160
  if (this.indexData) {
151
161
  this.pushIndexSnapshot(this.indexData);
152
162
  }
@@ -168,10 +178,6 @@ export class LucenaAgent {
168
178
  this.startWatcher();
169
179
  this.connected = true;
170
180
 
171
- // ── Wait for indexing to finish before opening the browser ──
172
- // This guarantees the snapshot is ready the instant the browser connects
173
- await this.indexPromise;
174
-
175
181
  return this.tunnelId;
176
182
  }
177
183
 
@@ -218,11 +224,18 @@ export class LucenaAgent {
218
224
  case 'delete_file': return this.deleteFile(command);
219
225
  case 'mkdir': return this.mkdirCmd(command);
220
226
  case 'search': return this.searchCodebase(command);
227
+ case 'store_pro_token': return this.storeProTokenCmd(command);
221
228
  case 'ping': return this.pushResponse(messageId, 'pong', '');
222
229
  default: return this.pushResponse(messageId, 'error', `Unknown command type: ${type}`);
223
230
  }
224
231
  }
225
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
+
226
239
  pushResponse(messageId, type, text, extra = {}) {
227
240
  const responsesRef = ref(this.db, `tunnels/${this.tunnelId}/responses`);
228
241
  push(responsesRef, {
package/src/main.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // src/main.js — CLI entry point for the Lucena agent
2
2
  import { LucenaAgent } from './agent.js';
3
+ import { spawn } from 'child_process';
4
+ import { registerProTunnel, validateStoredProToken } from './pro-token.js';
3
5
  import { basename } from 'path';
4
6
 
5
7
 
@@ -35,18 +37,45 @@ ${c.dim} =========================================${c.reset}
35
37
  to your local folders.
36
38
  `;
37
39
 
40
+ function openBrowser(url) {
41
+ const command = process.platform === 'darwin'
42
+ ? 'open'
43
+ : process.platform === 'win32'
44
+ ? 'cmd'
45
+ : 'xdg-open';
46
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
47
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
48
+ child.unref();
49
+ }
50
+
38
51
  export async function main() {
39
52
  const cwd = process.cwd();
53
+ const proStatus = await validateStoredProToken();
54
+ let activeTunnelId = null;
40
55
 
41
56
  console.log(BANNER);
42
57
  console.log(` ${c.cyan}📍 Scoped to:${c.reset} ${cwd}`);
43
58
  console.log(` ${c.yellow}🛡️ Safe Mode:${c.reset} ON by default (All edits require approval)`);
44
59
  console.log(` ${c.dim}Optionally switch to YOLO on LucenaCoder.com${c.reset}\n`);
60
+ if (proStatus.valid) {
61
+ console.log(` ${c.cyan}PRO detected.${c.reset} Browser auto-launch enabled.\n`);
62
+ }
45
63
 
46
64
  const agent = new LucenaAgent(cwd);
47
65
 
48
66
  const shutdown = async () => {
49
67
  console.log(`\n ${c.dim}Shutting down tunnel...${c.reset}`);
68
+ if (proStatus.valid && activeTunnelId) {
69
+ await registerProTunnel({
70
+ tokenForPro: proStatus.stored?.tokenForPro,
71
+ tunnelId: activeTunnelId,
72
+ cwdName: basename(cwd),
73
+ platform: process.platform,
74
+ pid: process.pid,
75
+ status: 'disconnected',
76
+ online: false,
77
+ });
78
+ }
50
79
  await agent.shutdown();
51
80
  process.exit(0);
52
81
  };
@@ -56,6 +85,21 @@ export async function main() {
56
85
 
57
86
  try {
58
87
  const tunnelId = await agent.start();
88
+ activeTunnelId = tunnelId;
89
+ if (proStatus.valid) {
90
+ const registered = await registerProTunnel({
91
+ tokenForPro: proStatus.stored?.tokenForPro,
92
+ tunnelId,
93
+ cwdName: basename(cwd),
94
+ platform: process.platform,
95
+ pid: process.pid,
96
+ });
97
+ if (registered.ok) {
98
+ console.log(` ${c.cyan}RemoteControl registered.${c.reset} This run is visible on rc.lucenacoder.com.`);
99
+ } else {
100
+ console.log(` ${c.yellow}RemoteControl registration failed.${c.reset} ${registered.error || 'Run will not appear in the remote list.'}`);
101
+ }
102
+ }
59
103
  console.log(` ${c.green}✔ Tunnel active!${c.reset}\n`);
60
104
 
61
105
  const idLabel = "Tunnel ID:";
@@ -72,11 +116,22 @@ export async function main() {
72
116
  const urlBoxWidth = urlLabel.length + webUrl.length + 5;
73
117
  const urlBorder = '─'.repeat(urlBoxWidth);
74
118
 
119
+ if (proStatus.valid) {
120
+ try {
121
+ openBrowser(webUrl);
122
+ console.log(`\n ${c.green}✔ Opening LucenaCoder...${c.reset}`);
123
+ } catch {
124
+ console.log(`\n ${c.yellow}Could not auto-open your browser.${c.reset}`);
125
+ }
126
+ }
127
+
75
128
  console.log(`\n ${c.dim}┌${urlBorder}┐${c.reset}`);
76
129
  console.log(` ${c.dim}│${c.reset} ${urlLabel}${c.reset} ${c.bold}${webUrl}${c.reset} ${c.dim}│${c.reset}`);
77
130
  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}`);
131
+
132
+ if (!proStatus.valid) {
133
+ console.log(`\n ${c.dim}Open the URL above in your browser to connect.${c.reset}`);
134
+ }
80
135
  console.log(` ${c.dim}Press Ctrl+C to disconnect${c.reset}\n`);
81
136
  } catch (err) {
82
137
  console.error(`\n ${c.yellow}✖ Failed to start tunnel: ${err.message}${c.reset}\n`);
@@ -0,0 +1,75 @@
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
+ const REGISTER_TUNNEL_URL = process.env.LUCENA_REGISTER_TUNNEL_URL || 'https://lucenacoder.com/api/remote/register-tunnel';
8
+
9
+ export async function readStoredProToken() {
10
+ try {
11
+ const raw = await readFile(TOKEN_PATH, 'utf-8');
12
+ const data = JSON.parse(raw);
13
+ if (!data?.tokenForPro) return null;
14
+ return data;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export async function storeProToken({ tokenForPro, email }) {
21
+ if (!tokenForPro) return null;
22
+ const data = {
23
+ tokenForPro,
24
+ email: email || '',
25
+ savedAt: new Date().toISOString(),
26
+ };
27
+ await mkdir(dirname(TOKEN_PATH), { recursive: true });
28
+ await writeFile(TOKEN_PATH, JSON.stringify(data, null, 2), 'utf-8');
29
+ return data;
30
+ }
31
+
32
+ export async function validateStoredProToken() {
33
+ const stored = await readStoredProToken();
34
+ if (!stored?.tokenForPro) return { valid: false };
35
+
36
+ try {
37
+ const response = await fetch(VALIDATE_URL, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({ tokenForPro: stored.tokenForPro }),
41
+ });
42
+ const payload = await response.json().catch(() => ({}));
43
+ if (!response.ok || !payload.valid) return { valid: false, stored };
44
+ return { valid: true, stored, ...payload };
45
+ } catch {
46
+ return { valid: false, stored };
47
+ }
48
+ }
49
+
50
+ export async function registerProTunnel({ tokenForPro, tunnelId, cwdName, platform, pid, source = 'local-npx', status = 'active', online = true }) {
51
+ if (!tokenForPro || !tunnelId) return { ok: false };
52
+
53
+ try {
54
+ const response = await fetch(REGISTER_TUNNEL_URL, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({
58
+ tokenForPro,
59
+ tunnelId,
60
+ cwdName,
61
+ displayName: cwdName,
62
+ platform,
63
+ pid,
64
+ source,
65
+ status,
66
+ online,
67
+ }),
68
+ });
69
+ const payload = await response.json().catch(() => ({}));
70
+ if (!response.ok) return { ok: false, ...payload };
71
+ return { ok: true, ...payload };
72
+ } catch {
73
+ return { ok: false };
74
+ }
75
+ }