@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 +1 -1
- package/src/agent.js +18 -5
- package/src/main.js +57 -2
- package/src/pro-token.js +75 -0
package/package.json
CHANGED
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
|
-
|
|
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`);
|
package/src/pro-token.js
ADDED
|
@@ -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
|
+
}
|