@lucenaone/coder 1.0.0

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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # 🌴 Lucena Coder Local Tunnel
2
+
3
+ **The official local connection agent for [LucenaCoder.com](https://lucenacoder.com).**
4
+
5
+ Create a private, secure tunnel connecting your browser-based AI coding assistant directly to your local filesystem.
6
+
7
+ ## Quick Start
8
+
9
+ Navigate to the project folder you want the AI to work on, and run the tunnel:
10
+
11
+ ```bash
12
+ cd your-project
13
+ npx lucenacoder
14
+ ```
15
+
16
+ ## How it Works
17
+
18
+ 1. **Run the Command**: The agent connects to the Lucena real-time relay.
19
+ 2. **Get your Tunnel ID**: The terminal will print a secure `Tunnel ID`.
20
+ 3. **Connect**: Paste this ID into your LucenaCoder.com workspace.
21
+ 4. **Code**: The AI can now read files, propose edits, and run terminal commands **strictly scoped** to that folder.
22
+
23
+ ## 🛡️ Built for Safety
24
+
25
+ Giving an AI access to your machine should be done securely.
26
+
27
+ * **Folder Scoped**: The tunnel is strictly jailed to the directory where you executed the command. It cannot access files outside of this workspace.
28
+ * **Safe Mode by Default**: Every file edit and terminal command requires your explicit approval before executing.
29
+ * **YOLO Mode**: Fully trust the AI for a specific task? You can optionally toggle YOLO mode from the web interface for hands-free, autonomous coding.
30
+
31
+ ## Requirements
32
+
33
+ - Node.js 18+
package/bin/lucena.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/main.js';
3
+ main().catch((err) => {
4
+ console.error('\n ✖ ' + (err.message || err));
5
+ process.exit(1);
6
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@lucenaone/coder",
3
+ "version": "1.0.0",
4
+ "description": "Private tunnel for connecting LucenaCoder.com to your local folder. Always remains folder scoped while providing full terminal access.",
5
+ "type": "module",
6
+ "bin": {
7
+ "lucenacoder": "./bin/lucena.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/lucena.js"
11
+ },
12
+ "dependencies": {
13
+ "firebase": "^11.0.0",
14
+ "chokidar": "^4.0.0"
15
+ },
16
+ "keywords": ["lucena", "lucenacoder", "tunnel", "lucenaone"],
17
+ "author": "LucenaOne",
18
+ "license": "MIT"
19
+ }
package/src/agent.js ADDED
@@ -0,0 +1,265 @@
1
+ import { initializeApp } from 'firebase/app';
2
+ import { getDatabase, ref, push, set, onChildAdded, onDisconnect, serverTimestamp, remove, get } from 'firebase/database';
3
+ import { spawn } from 'child_process';
4
+ import { watch } from 'chokidar';
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';
8
+ import { FIREBASE_CONFIG } from './config.js';
9
+
10
+ const IGNORED_PATTERNS = [
11
+ 'node_modules', '.git', '.next', '.wrangler', '.DS_Store',
12
+ 'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase', 'Users'
13
+ ];
14
+
15
+ // ── The CLI Jailer ──
16
+ function getJailedPath(baseDir, rawPath) {
17
+ let p = rawPath.replace(/\\/g, '/');
18
+
19
+ // If the AI hallucinates the absolute path of the workspace, dynamically strip it out
20
+ const normalizedBase = baseDir.replace(/\\/g, '/');
21
+ if (p.startsWith(normalizedBase)) {
22
+ p = p.slice(normalizedBase.length);
23
+ }
24
+
25
+ // Strip leading slashes to force relative resolution
26
+ p = p.replace(/^\/+/, '');
27
+
28
+ const parts = p.split('/').filter(part => part !== '.' && part !== '');
29
+ const safeParts = [];
30
+
31
+ for (const part of parts) {
32
+ if (part === '..') safeParts.pop(); // Traversal clamping
33
+ else safeParts.push(part);
34
+ }
35
+
36
+ return resolve(baseDir, safeParts.join('/'));
37
+ }
38
+
39
+ export class LucenaAgent {
40
+ constructor(cwd) {
41
+ this.cwd = resolve(cwd);
42
+ this.tunnelId = crypto.randomUUID();
43
+ this.app = null;
44
+ this.db = null;
45
+ this.watcher = null;
46
+ this.activeCommands = new Map();
47
+ this.connected = false;
48
+ }
49
+
50
+ async start() {
51
+ this.app = initializeApp(FIREBASE_CONFIG, `agent-${this.tunnelId}`);
52
+ this.db = getDatabase(this.app);
53
+ const tunnelRef = ref(this.db, `tunnels/${this.tunnelId}`);
54
+
55
+ await set(tunnelRef, {
56
+ meta: {
57
+ createdAt: serverTimestamp(),
58
+ cwd: this.cwd,
59
+ cwdName: basename(this.cwd),
60
+ status: 'active',
61
+ pid: process.pid,
62
+ platform: process.platform
63
+ }
64
+ });
65
+
66
+ onChildAdded(ref(this.db, `.info`), () => {});
67
+ const presenceRef = ref(this.db, `tunnels/${this.tunnelId}/meta/online`);
68
+ await set(presenceRef, true);
69
+ onDisconnect(presenceRef).set(false);
70
+ onDisconnect(ref(this.db, `tunnels/${this.tunnelId}/meta/status`)).set('disconnected');
71
+
72
+ const commandsRef = ref(this.db, `tunnels/${this.tunnelId}/commands`);
73
+ onChildAdded(commandsRef, async (snapshot) => {
74
+ const command = snapshot.val();
75
+ if (!command) return;
76
+ remove(snapshot.ref);
77
+
78
+ try {
79
+ await this.handleCommand(command);
80
+ } catch (err) {
81
+ this.pushResponse(command.messageId, 'error', err.message);
82
+ }
83
+ });
84
+
85
+ this.startWatcher();
86
+ this.connected = true;
87
+ return this.tunnelId;
88
+ }
89
+
90
+ async handleCommand(command) {
91
+ const { type, messageId } = command;
92
+ switch (type) {
93
+ case 'execute': return this.executeCommand(command);
94
+ case 'read_file': return this.readFileCmd(command);
95
+ case 'write_file': return this.writeFileCmd(command);
96
+ case 'list_dir': return this.listDir(command);
97
+ case 'stat': return this.statFile(command);
98
+ case 'delete_file': return this.deleteFile(command);
99
+ case 'mkdir': return this.mkdirCmd(command);
100
+ case 'search': return this.searchCodebase(command);
101
+ case 'ping': return this.pushResponse(messageId, 'pong', '');
102
+ default: return this.pushResponse(messageId, 'error', `Unknown command type: ${type}`);
103
+ }
104
+ }
105
+
106
+ pushResponse(messageId, type, text, extra = {}) {
107
+ const responsesRef = ref(this.db, `tunnels/${this.tunnelId}/responses`);
108
+ push(responsesRef, {
109
+ messageId,
110
+ type,
111
+ text,
112
+ timestamp: serverTimestamp(),
113
+ ...extra
114
+ });
115
+ }
116
+
117
+ async executeCommand({ messageId, command }) {
118
+ const child = spawn('sh', ['-c', command], { cwd: this.cwd });
119
+
120
+ child.stdout.on('data', (data) => this.pushResponse(messageId, 'output', data.toString()));
121
+ child.stderr.on('data', (data) => this.pushResponse(messageId, 'output', data.toString()));
122
+ child.on('close', (code) => {
123
+ this.pushResponse(messageId, 'done', '', { exitCode: code ?? 0 });
124
+ this.activeCommands.delete(messageId);
125
+ });
126
+
127
+ this.activeCommands.set(messageId, child);
128
+ }
129
+
130
+ async readFileCmd({ messageId, path: filePath }) {
131
+ const fullPath = getJailedPath(this.cwd, filePath);
132
+ try {
133
+ const content = await readFile(fullPath, 'utf-8');
134
+ this.pushResponse(messageId, 'output', content);
135
+ this.pushResponse(messageId, 'done', '');
136
+ } catch (err) {
137
+ this.pushResponse(messageId, 'error', err.message);
138
+ }
139
+ }
140
+
141
+ async writeFileCmd({ messageId, path: filePath, content }) {
142
+ const fullPath = getJailedPath(this.cwd, filePath);
143
+ try {
144
+ await mkdir(dirname(fullPath), { recursive: true });
145
+ await writeFile(fullPath, content, 'utf-8');
146
+ this.pushResponse(messageId, 'done', `Wrote ${fullPath}`);
147
+ } catch (err) {
148
+ this.pushResponse(messageId, 'error', err.message);
149
+ }
150
+ }
151
+
152
+ async listDir({ messageId, path: dirPath }) {
153
+ const fullPath = getJailedPath(this.cwd, dirPath || '.');
154
+ try {
155
+ const entries = await readdir(fullPath, { withFileTypes: true });
156
+ const listing = entries
157
+ .filter(e => !IGNORED_PATTERNS.includes(e.name))
158
+ .map(e => `${e.isDirectory() ? 'dir' : 'file'}\t${e.name}`)
159
+ .join('\n');
160
+ this.pushResponse(messageId, 'done', listing || '(empty)');
161
+ } catch (err) {
162
+ this.pushResponse(messageId, 'error', err.message);
163
+ }
164
+ }
165
+
166
+ async statFile({ messageId, path: filePath }) {
167
+ const fullPath = getJailedPath(this.cwd, filePath);
168
+ try {
169
+ const s = await stat(fullPath);
170
+ this.pushResponse(messageId, 'done', JSON.stringify({
171
+ size: s.size,
172
+ isFile: s.isFile(),
173
+ isDir: s.isDirectory(),
174
+ modified: s.mtime,
175
+ created: s.birthtime
176
+ }));
177
+ } catch (err) {
178
+ this.pushResponse(messageId, 'error', err.message);
179
+ }
180
+ }
181
+
182
+ async deleteFile({ messageId, path: filePath }) {
183
+ const fullPath = getJailedPath(this.cwd, filePath);
184
+ try {
185
+ if ((await stat(fullPath)).isDirectory()) {
186
+ await rm(fullPath, { recursive: true });
187
+ } else {
188
+ await unlink(fullPath);
189
+ }
190
+ this.pushResponse(messageId, 'done', `Deleted ${fullPath}`);
191
+ } catch (err) {
192
+ this.pushResponse(messageId, 'error', err.message);
193
+ }
194
+ }
195
+
196
+ async mkdirCmd({ messageId, path: dirPath }) {
197
+ const fullPath = getJailedPath(this.cwd, dirPath);
198
+ try {
199
+ await mkdir(fullPath, { recursive: true });
200
+ this.pushResponse(messageId, 'done', `Created ${fullPath}`);
201
+ } catch (err) {
202
+ this.pushResponse(messageId, 'error', err.message);
203
+ }
204
+ }
205
+
206
+ async searchCodebase({ messageId, query, directory }) {
207
+ const searchDir = getJailedPath(this.cwd, directory || '.');
208
+ try {
209
+ const child = spawn('grep', [
210
+ '-rn', '--include=*.{js,jsx,ts,tsx,json,md,css,html,py,rb,go,rs}',
211
+ query, searchDir
212
+ ], { cwd: this.cwd });
213
+
214
+ let output = '';
215
+ child.stdout.on('data', (d) => { output += d.toString(); });
216
+ child.stderr.on('data', (d) => { output += d.toString(); });
217
+
218
+ child.on('close', (code) => {
219
+ this.pushResponse(messageId, 'done', output || 'No matches found');
220
+ });
221
+ } catch (err) {
222
+ this.pushResponse(messageId, 'error', err.message);
223
+ }
224
+ }
225
+
226
+ startWatcher() {
227
+ this.watcher = watch(this.cwd, {
228
+ ignored: (path) => IGNORED_PATTERNS.some(p => path.includes(p)),
229
+ persistent: true,
230
+ ignoreInitial: true
231
+ });
232
+
233
+ this.watcher.on('all', (event, filePath) => {
234
+ const relPath = relative(this.cwd, filePath);
235
+
236
+ // Stop anything escaping local disk
237
+ if (!relPath || relPath.startsWith('..') || isAbsolute(relPath)) {
238
+ return;
239
+ }
240
+
241
+ const changesRef = ref(this.db, `tunnels/${this.tunnelId}/fileChanges`);
242
+ push(changesRef, {
243
+ event,
244
+ path: relPath,
245
+ timestamp: serverTimestamp()
246
+ });
247
+ });
248
+ }
249
+
250
+ async shutdown() {
251
+ for (const [messageId, child] of this.activeCommands) {
252
+ child.kill('SIGTERM');
253
+ this.pushResponse(messageId, 'error', 'Command killed: tunnel shutting down');
254
+ }
255
+ this.activeCommands.clear();
256
+
257
+ if (this.watcher) await this.watcher.close();
258
+ if (this.db) {
259
+ await set(ref(this.db, `tunnels/${this.tunnelId}/meta/online`), false);
260
+ await set(ref(this.db, `tunnels/${this.tunnelId}/meta/status`), 'disconnected');
261
+ }
262
+
263
+ this.connected = false;
264
+ }
265
+ }
package/src/config.js ADDED
@@ -0,0 +1,10 @@
1
+ // src/config.js — Realtime Relay
2
+ export const FIREBASE_CONFIG = {
3
+ apiKey: "AIzaSyBn8sdlbO2dJH1RQYqoQVfN5r0Wq3l6M8E",
4
+ authDomain: "poweredbyevok.firebaseapp.com",
5
+ databaseURL: "https://lucena-tunnel.firebaseio.com",
6
+ projectId: "poweredbyevok",
7
+ storageBucket: "poweredbyevok.appspot.com",
8
+ messagingSenderId: "726544861905",
9
+ appId: "1:726544861905:web:20e5e35a0d6069ee2913f0"
10
+ };
package/src/main.js ADDED
@@ -0,0 +1,84 @@
1
+ // src/main.js — CLI entry point for the Lucena agent
2
+ import { LucenaAgent } from './agent.js';
3
+ import { basename } from 'path';
4
+ import { exec } from 'child_process';
5
+
6
+ // Standard ANSI Terminal Colors
7
+ const c = {
8
+ cyan: '\x1b[36m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ dim: '\x1b[90m',
12
+ bold: '\x1b[1m',
13
+ reset: '\x1b[0m'
14
+ };
15
+
16
+ const BANNER = `
17
+ ${c.green}⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⡀
18
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠻⢿⣿⣿⣷⣄
19
+ ⠀⠀⠀⠀⠀⠀⣀⣤⣶⣶⣦⣄⠙⣿⣿⣿⣇⣠⣶⣾⣿⣷⣶⣶⠄
20
+ ⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣷⣼⣿⣿⣿⣿⣿⣿⣿⠟⠋
21
+ ⠀⠀⠀⠘⠛⠉⠉⠉⠁⠉⠉⠛⢿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣤⣀
22
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠿⠛⢿⣿⣿⣿⣿⣟⠛⠻⢿⣷⣦⡀
23
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⡿⠁⠀⠀⢸⣿⣿⡿⠻⣿⣷⡀⠀⠉⠻⢷
24
+ ⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⡿⠁⠀⠀⠀⠸⣿⡿⠁⠀⠈⢿⣇${c.reset}
25
+ ${c.dim}⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⠁${c.reset}${c.green}⠀⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠏${c.reset}
26
+ ${c.dim}⠀⠀⠀⠀⠀⠀⢠⣿⣿⠇${c.reset} ${c.bold}L U C E N A${c.reset}
27
+ ${c.dim}⠀⠀⠀⠀⠀⢀⣾⣿⡟${c.reset} ${c.bold} C O D E R${c.reset}
28
+ ${c.dim}⠀⠀⠀⠀⠀⣼⣿⣿⠃${c.reset} ${c.cyan}${c.bold} L O C A L${c.reset}
29
+ ${c.dim}⠀⠀⠀⠀⢠⣿⣿⡟${c.reset} ${c.cyan}${c.bold}T U N N E L${c.reset}
30
+ ${c.dim}⠀⠀⠀⠀⣼⣿⣿⠃${c.reset}
31
+ ${c.dim}⠀⠀⠀⠀⠈⠛⠉${c.reset}
32
+
33
+ ${c.dim} =========================================${c.reset}
34
+ Connect your LucenaCoder.com session
35
+ to your local folders.
36
+ `;
37
+
38
+ export async function main() {
39
+ const cwd = process.cwd();
40
+
41
+ console.log(BANNER);
42
+ console.log(` ${c.cyan}📍 Scoped to:${c.reset} ${cwd}`);
43
+ console.log(` ${c.yellow}🛡️ Safe Mode:${c.reset} ON by default (All edits require approval)`);
44
+ console.log(` ${c.dim}Optionally switch to YOLO on LucenaCoder.com${c.reset}\n`);
45
+ console.log(` Starting tunnel...`);
46
+
47
+ const agent = new LucenaAgent(cwd);
48
+
49
+ const shutdown = async () => {
50
+ console.log(`\n ${c.dim}Shutting down tunnel...${c.reset}`);
51
+ await agent.shutdown();
52
+ process.exit(0);
53
+ };
54
+
55
+ process.on('SIGINT', shutdown);
56
+ process.on('SIGTERM', shutdown);
57
+
58
+ try {
59
+ const tunnelId = await agent.start();
60
+ console.log(`\n ${c.green}✔ Tunnel active!${c.reset}\n`);
61
+
62
+ const idLabel = "Tunnel ID:";
63
+ const boxWidth = idLabel.length + tunnelId.length + 5;
64
+ const border = '─'.repeat(boxWidth);
65
+
66
+ console.log(` ${c.dim}┌${border}┐${c.reset}`);
67
+ console.log(` ${c.dim}│${c.reset} ${c.dim}${idLabel}${c.reset} ${c.bold}${c.cyan}${tunnelId}${c.reset} ${c.dim}│${c.reset}`);
68
+ console.log(` ${c.dim}└${border}┘${c.reset}`);
69
+
70
+ const webUrl = `https://lucenacoder.com/?tunnel=${tunnelId}`;
71
+ console.log(`\n ${c.dim}Opening browser to your workspace...${c.reset}`);
72
+
73
+ const startCmd = process.platform === 'darwin' ? 'open'
74
+ : process.platform === 'win32' ? 'start'
75
+ : 'xdg-open';
76
+
77
+ exec(`${startCmd} "${webUrl}"`);
78
+
79
+ console.log(`\n ${c.dim}Press Ctrl+C to disconnect${c.reset}\n`);
80
+ } catch (err) {
81
+ console.error(`\n ${c.yellow}✖ Failed to start tunnel: ${err.message}${c.reset}\n`);
82
+ process.exit(1);
83
+ }
84
+ }