@lucenaone/coder 1.1.3 → 1.1.6

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,17 +1,25 @@
1
1
  {
2
2
  "name": "@lucenaone/coder",
3
- "version": "1.1.3",
3
+ "version": "1.1.6",
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": {
7
7
  "lucenacoder": "./bin/lucena.js"
8
8
  },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "grammars",
13
+ "README.md"
14
+ ],
9
15
  "scripts": {
10
16
  "start": "node bin/lucena.js"
11
17
  },
12
18
  "dependencies": {
13
19
  "chokidar": "^4.0.0",
20
+ "execa": "^9.6.1",
14
21
  "firebase": "^11.0.0",
22
+ "shell-quote": "^1.8.4",
15
23
  "web-tree-sitter": "^0.26.9"
16
24
  },
17
25
  "keywords": [
package/src/agent.js CHANGED
@@ -1,18 +1,21 @@
1
1
  import { initializeApp } from 'firebase/app';
2
2
  import { getDatabase, ref, push, set, onChildAdded, onDisconnect, serverTimestamp, remove, get } from 'firebase/database';
3
- import { spawn } from 'child_process';
3
+ import { spawn, spawnSync } from 'child_process';
4
4
  import { watch } from 'chokidar';
5
5
  import { readFile, writeFile, mkdir, readdir, stat, unlink, rm } from 'fs/promises';
6
6
  import { join, resolve, dirname, basename, relative, isAbsolute, extname } from 'path';
7
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
+ import { LucenaShell } from './lucena-shell.js';
10
11
 
11
12
  const IGNORED_PATTERNS = [
12
13
  'node_modules', '.git', '.next', '.wrangler', '.DS_Store',
13
14
  'dist', 'build', '.cache', '.turbo', '.vercel', '.firebase'
14
15
  ];
15
16
 
17
+ const SEARCH_GLOB = '*.{js,jsx,ts,tsx,json,md,css,html,py,rb,go,rs}';
18
+
16
19
  // ── The CLI Jailer ──
17
20
  function getJailedPath(baseDir, rawPath) {
18
21
  let p = rawPath.replace(/\\/g, '/');
@@ -59,8 +62,15 @@ export class LucenaAgent {
59
62
  this.watcher = null;
60
63
  this.activeCommands = new Map();
61
64
  this.connected = false;
65
+ this.stripCwd = true; // Default: strip absolute paths (Browser Mode safety)
62
66
  this.indexData = null; // Pre-built index from CLI-side parsing
63
67
  this.indexPromise = null; // The in-flight indexing promise
68
+ this.shell = new LucenaShell(this.cwd);
69
+ }
70
+
71
+ /** Conditionally strip cwd — only in Browser Mode */
72
+ _sanitize(text) {
73
+ return this.stripCwd ? stripCwd(this.cwd, text) : text;
64
74
  }
65
75
 
66
76
  _scaffoldWorkspaceBrain() {
@@ -129,6 +139,11 @@ export class LucenaAgent {
129
139
  if (!data) return;
130
140
  remove(snapshot.ref);
131
141
 
142
+ // Browser tells us whether to strip cwd from output
143
+ if (typeof data.stripCwd === 'boolean') {
144
+ this.stripCwd = data.stripCwd;
145
+ }
146
+
132
147
  // Index is guaranteed ready (awaited in start()), push immediately
133
148
  if (this.indexData) {
134
149
  this.pushIndexSnapshot(this.indexData);
@@ -144,7 +159,7 @@ export class LucenaAgent {
144
159
  try {
145
160
  await this.handleCommand(command);
146
161
  } catch (err) {
147
- this.pushResponse(command.messageId, 'error', stripCwd(this.cwd, err.message));
162
+ this.pushResponse(command.messageId, 'error', this._sanitize(err.message));
148
163
  }
149
164
  });
150
165
 
@@ -216,15 +231,26 @@ export class LucenaAgent {
216
231
  });
217
232
  }
218
233
 
219
- async executeCommand({ messageId, command }) {
220
- const child = spawn('sh', ['-c', command], { cwd: this.cwd });
234
+ async executeCommand({ messageId, command, mode = 'safe', approved = false, outsideWorkspaceApproved = false }) {
235
+ let child;
221
236
 
222
- child.stdout.on('data', (data) => {
223
- this.pushResponse(messageId, 'output', stripCwd(this.cwd, data.toString()));
237
+ try {
238
+ child = this.shell.execute(command, { mode, approved, outsideWorkspaceApproved });
239
+ } catch (err) {
240
+ return this.pushResponse(messageId, 'error', this._sanitize(err.message));
241
+ }
242
+
243
+ child.stdout?.on('data', (data) => {
244
+ this.pushResponse(messageId, 'output', this._sanitize(data.toString()));
224
245
  });
225
- child.stderr.on('data', (data) => {
226
- this.pushResponse(messageId, 'output', stripCwd(this.cwd, data.toString()));
246
+ child.stderr?.on('data', (data) => {
247
+ this.pushResponse(messageId, 'output', this._sanitize(data.toString()));
227
248
  });
249
+
250
+ // Immediately close stdin so commands that try to read it get EOF
251
+ // instead of hanging until the 60s timeout
252
+ try { child.stdin?.end(); } catch { /* already closed */ }
253
+
228
254
  child.on('close', (code) => {
229
255
  this.pushResponse(messageId, 'done', '', { exitCode: code ?? 0 });
230
256
  this.activeCommands.delete(messageId);
@@ -240,7 +266,7 @@ export class LucenaAgent {
240
266
  this.pushResponse(messageId, 'output', content);
241
267
  this.pushResponse(messageId, 'done', '');
242
268
  } catch (err) {
243
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
269
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
244
270
  }
245
271
  }
246
272
 
@@ -252,7 +278,7 @@ export class LucenaAgent {
252
278
  await writeFile(fullPath, content, 'utf-8');
253
279
  this.pushResponse(messageId, 'done', `Wrote ${relPath}`);
254
280
  } catch (err) {
255
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
281
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
256
282
  }
257
283
  }
258
284
 
@@ -266,7 +292,7 @@ export class LucenaAgent {
266
292
  .join('\n');
267
293
  this.pushResponse(messageId, 'done', listing || '(empty)');
268
294
  } catch (err) {
269
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
295
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
270
296
  }
271
297
  }
272
298
 
@@ -282,7 +308,7 @@ export class LucenaAgent {
282
308
  created: s.birthtime
283
309
  }));
284
310
  } catch (err) {
285
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
311
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
286
312
  }
287
313
  }
288
314
 
@@ -297,7 +323,7 @@ export class LucenaAgent {
297
323
  }
298
324
  this.pushResponse(messageId, 'done', `Deleted ${relPath}`);
299
325
  } catch (err) {
300
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
326
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
301
327
  }
302
328
  }
303
329
 
@@ -308,30 +334,53 @@ export class LucenaAgent {
308
334
  await mkdir(fullPath, { recursive: true });
309
335
  this.pushResponse(messageId, 'done', `Created ${relPath}`);
310
336
  } catch (err) {
311
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
337
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
312
338
  }
313
339
  }
314
340
 
315
341
  async searchCodebase({ messageId, query, directory }) {
316
342
  const searchDir = getJailedPath(this.cwd, directory || '.');
317
343
  try {
318
- const child = spawn('grep', [
319
- '-rn', '--include=*.{js,jsx,ts,tsx,json,md,css,html,py,rb,go,rs}',
320
- query, searchDir
321
- ], { cwd: this.cwd });
344
+ const child = this._createSearchProcess(query, searchDir);
322
345
 
323
346
  let output = '';
324
347
  child.stdout.on('data', (d) => { output += d.toString(); });
325
348
  child.stderr.on('data', (d) => { output += d.toString(); });
326
349
 
327
350
  child.on('close', (code) => {
328
- this.pushResponse(messageId, 'done', stripCwd(this.cwd, output) || 'No matches found');
351
+ this.pushResponse(messageId, 'done', this._sanitize(output) || 'No matches found');
352
+ });
353
+ child.on('error', (err) => {
354
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
329
355
  });
330
356
  } catch (err) {
331
- this.pushResponse(messageId, 'error', stripCwd(this.cwd, err.message));
357
+ this.pushResponse(messageId, 'error', this._sanitize(err.message));
332
358
  }
333
359
  }
334
360
 
361
+ _createSearchProcess(query, searchDir) {
362
+ try {
363
+ const rg = spawnSync('rg', ['--version'], { cwd: this.cwd, encoding: 'utf8' });
364
+ if (rg.status === 0) {
365
+ return spawn('rg', ['-n', '--glob', SEARCH_GLOB, query, searchDir], { cwd: this.cwd });
366
+ }
367
+ } catch {
368
+ // Fall back to the OS-native search tool below.
369
+ }
370
+
371
+ if (process.platform === 'win32') {
372
+ return spawn('findstr', ['/s', '/n', `/c:${query}`, join(searchDir, '*')], {
373
+ cwd: this.cwd,
374
+ shell: true,
375
+ });
376
+ }
377
+
378
+ return spawn('grep', [
379
+ '-rn', `--include=${SEARCH_GLOB}`,
380
+ query, searchDir
381
+ ], { cwd: this.cwd });
382
+ }
383
+
335
384
  startWatcher() {
336
385
  this.watcher = watch(this.cwd, {
337
386
  ignored: (path) => IGNORED_PATTERNS.some(p => path.includes(p)),
@@ -376,4 +425,4 @@ export class LucenaAgent {
376
425
 
377
426
  this.connected = false;
378
427
  }
379
- }
428
+ }
@@ -0,0 +1,303 @@
1
+ import { execaCommand } from 'execa';
2
+ import { parse } from 'shell-quote';
3
+ import { isAbsolute, resolve, relative } from 'path';
4
+
5
+ const READ_ONLY_COMMANDS = new Set([
6
+ 'awk', 'basename', 'cat', 'cd', 'curl', 'cut', 'dirname', 'echo', 'false', 'find',
7
+ 'grep', 'head', 'ls', 'pwd', 'rg', 'sed', 'sort', 'tail', 'test', 'true',
8
+ 'uniq', 'wc', 'which',
9
+ 'dir', 'type', 'findstr', 'where', 'tree', 'more', 'clip', 'ver', 'vol',
10
+ 'hostname', 'systeminfo',
11
+ 'get-childitem', 'get-content', 'select-string', 'get-location',
12
+ 'get-command', 'get-process', 'get-service', 'get-item', 'get-itemproperty',
13
+ 'test-path', 'get-help', 'write-output', 'write-host',
14
+ ]);
15
+
16
+ const READ_ONLY_GIT_SUBCOMMANDS = new Set([
17
+ 'branch', 'diff', 'grep', 'log', 'ls-files', 'rev-parse', 'show', 'status',
18
+ ]);
19
+
20
+ const CHAIN_OPERATORS = new Set(['&&', '||', ';', '|']);
21
+ const WRITE_OPERATORS = new Set(['>', '>>', '<>', '>|', '<<', '<<-', '<<<']);
22
+ const COMMANDS_WITH_PATH_OPERANDS = new Set([
23
+ 'cat', 'chmod', 'chown', 'cp', 'find', 'grep', 'head', 'ls', 'mkdir', 'mv',
24
+ 'rm', 'rmdir', 'sed', 'tail', 'touch', 'wc',
25
+ ]);
26
+
27
+ export class LucenaShell {
28
+ constructor(workspaceRoot) {
29
+ this.workspaceRoot = resolve(workspaceRoot);
30
+ this.cwd = this.workspaceRoot;
31
+ }
32
+
33
+ analyze(rawCommand) {
34
+ const sanitized = sanitizeCommand(rawCommand);
35
+ const analysis = {
36
+ command: sanitized.command,
37
+ rejected: false,
38
+ rejectReason: '',
39
+ isReadOnly: true,
40
+ needsApproval: false,
41
+ touchesOutsideWorkspace: false,
42
+ reasons: [],
43
+ segments: [],
44
+ };
45
+
46
+ if (!sanitized.ok) {
47
+ return {
48
+ ...analysis,
49
+ rejected: true,
50
+ rejectReason: sanitized.reason,
51
+ isReadOnly: false,
52
+ needsApproval: true,
53
+ };
54
+ }
55
+
56
+ if (!sanitized.command) return analysis;
57
+
58
+ if (/`|\$\(/.test(sanitized.command)) {
59
+ analysis.isReadOnly = false;
60
+ analysis.needsApproval = true;
61
+ analysis.reasons.push('uses command substitution');
62
+ }
63
+
64
+ let tokens;
65
+ try {
66
+ tokens = parse(sanitized.command);
67
+ } catch (err) {
68
+ return {
69
+ ...analysis,
70
+ rejected: true,
71
+ rejectReason: err.message || 'Command could not be parsed.',
72
+ isReadOnly: false,
73
+ needsApproval: true,
74
+ };
75
+ }
76
+
77
+ let current = [];
78
+ for (const token of tokens) {
79
+ if (isOperator(token) && CHAIN_OPERATORS.has(token.op)) {
80
+ this._addSegmentAnalysis(analysis, current);
81
+ current = [];
82
+ continue;
83
+ }
84
+
85
+ if (isOperator(token) && WRITE_OPERATORS.has(token.op)) {
86
+ analysis.isReadOnly = false;
87
+ analysis.needsApproval = true;
88
+ analysis.reasons.push(`uses shell redirection (${token.op})`);
89
+ }
90
+
91
+ if (isOperator(token) && token.op === '&') {
92
+ analysis.isReadOnly = false;
93
+ analysis.needsApproval = true;
94
+ analysis.reasons.push('runs a background process');
95
+ }
96
+
97
+ current.push(token);
98
+ }
99
+ this._addSegmentAnalysis(analysis, current);
100
+
101
+ analysis.reasons = [...new Set(analysis.reasons)];
102
+ return analysis;
103
+ }
104
+
105
+ canExecute(rawCommand, options = {}) {
106
+ const mode = options.mode === 'yolo' ? 'yolo' : 'safe';
107
+ const approved = options.approved === true;
108
+ const outsideWorkspaceApproved = options.outsideWorkspaceApproved === true;
109
+ const analysis = this.analyze(rawCommand);
110
+
111
+ if (analysis.rejected) {
112
+ return { ok: false, analysis, reason: analysis.rejectReason };
113
+ }
114
+
115
+ if (analysis.isReadOnly) {
116
+ return { ok: true, analysis };
117
+ }
118
+
119
+ if (mode === 'safe' && !approved) {
120
+ return {
121
+ ok: false,
122
+ analysis,
123
+ reason: `Safe Mode blocked a mutating command. Approval is required. ${formatReasons(analysis.reasons)}`,
124
+ };
125
+ }
126
+
127
+ if (mode === 'yolo' && analysis.touchesOutsideWorkspace && !outsideWorkspaceApproved) {
128
+ return {
129
+ ok: false,
130
+ analysis,
131
+ reason: `YOLO Mode blocked an outside-workspace mutation. Approval is required. ${formatReasons(analysis.reasons)}`,
132
+ };
133
+ }
134
+
135
+ return { ok: true, analysis };
136
+ }
137
+
138
+ execute(rawCommand, options = {}) {
139
+ const decision = this.canExecute(rawCommand, options);
140
+ if (!decision.ok) {
141
+ const err = new Error(decision.reason);
142
+ err.analysis = decision.analysis;
143
+ throw err;
144
+ }
145
+
146
+ return execaCommand(decision.analysis.command, {
147
+ cwd: this.cwd,
148
+ shell: true,
149
+ reject: false,
150
+ preferLocal: true,
151
+ stdin: 'pipe',
152
+ env: {
153
+ LUCENA_WORKSPACE_ROOT: this.workspaceRoot,
154
+ },
155
+ });
156
+ }
157
+
158
+ _addSegmentAnalysis(analysis, tokens) {
159
+ const words = tokens.filter((token) => typeof token === 'string');
160
+ if (words.length === 0) return;
161
+
162
+ let index = 0;
163
+ while (words[index] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index])) {
164
+ index++;
165
+ }
166
+
167
+ const command = stripQuotes(words[index] || '').toLowerCase();
168
+ if (!command) return;
169
+
170
+ const segment = {
171
+ command,
172
+ words,
173
+ isReadOnly: true,
174
+ touchesOutsideWorkspace: false,
175
+ };
176
+ analysis.segments.push(segment);
177
+
178
+ if (command === 'cd') {
179
+ const target = words[index + 1] ? stripQuotes(words[index + 1]) : this.workspaceRoot;
180
+ const nextCwd = resolvePath(this.cwd, target);
181
+ if (!isInside(this.workspaceRoot, nextCwd)) {
182
+ segment.touchesOutsideWorkspace = true;
183
+ analysis.touchesOutsideWorkspace = true;
184
+ analysis.reasons.push(`changes directory outside workspace (${displayPath(this.workspaceRoot, nextCwd)})`);
185
+ }
186
+ return;
187
+ }
188
+
189
+ if (command === 'git') {
190
+ const subcommand = stripQuotes(words[index + 1] || '').toLowerCase();
191
+ if (!READ_ONLY_GIT_SUBCOMMANDS.has(subcommand)) {
192
+ markMutating(analysis, segment, subcommand ? `git ${subcommand} may modify state` : 'git command is incomplete');
193
+ }
194
+ } else if (command === 'curl' && words.slice(index + 1).some(isCurlWriteOption)) {
195
+ markMutating(analysis, segment, 'curl output options may write files');
196
+ } else if (command === 'sed' && words.some((word) => /^-.*i/.test(stripQuotes(word)))) {
197
+ markMutating(analysis, segment, 'sed in-place editing may modify files');
198
+ } else if (!READ_ONLY_COMMANDS.has(command)) {
199
+ markMutating(analysis, segment, `${command} is not classified as read-only`);
200
+ }
201
+
202
+ if (segment.isReadOnly) return;
203
+
204
+ const pathOperands = collectPathOperands(command, words.slice(index + 1));
205
+ if (pathOperands.length === 0 && !isInside(this.workspaceRoot, this.cwd)) {
206
+ segment.touchesOutsideWorkspace = true;
207
+ analysis.touchesOutsideWorkspace = true;
208
+ analysis.reasons.push('mutates from a working directory outside the workspace');
209
+ return;
210
+ }
211
+
212
+ for (const operand of pathOperands) {
213
+ const absolutePath = resolvePath(this.cwd, operand);
214
+ if (!isInside(this.workspaceRoot, absolutePath)) {
215
+ segment.touchesOutsideWorkspace = true;
216
+ analysis.touchesOutsideWorkspace = true;
217
+ analysis.reasons.push(`references outside-workspace path (${displayPath(this.workspaceRoot, absolutePath)})`);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ function sanitizeCommand(rawCommand) {
224
+ const command = String(rawCommand || '').replace(/\r\n?/g, '\n').trim();
225
+
226
+ if (!command) return { ok: true, command };
227
+ if (command.length > 8000) {
228
+ return { ok: false, command, reason: 'Command is too long.' };
229
+ }
230
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(command)) {
231
+ return { ok: false, command, reason: 'Command contains unsupported control characters.' };
232
+ }
233
+
234
+ return { ok: true, command };
235
+ }
236
+
237
+ function isOperator(token) {
238
+ return token && typeof token === 'object' && typeof token.op === 'string';
239
+ }
240
+
241
+ function markMutating(analysis, segment, reason) {
242
+ segment.isReadOnly = false;
243
+ analysis.isReadOnly = false;
244
+ analysis.needsApproval = true;
245
+ analysis.reasons.push(reason);
246
+ }
247
+
248
+ function collectPathOperands(command, args) {
249
+ if (!COMMANDS_WITH_PATH_OPERANDS.has(command)) return [];
250
+ const operands = [];
251
+
252
+ for (let i = 0; i < args.length; i++) {
253
+ const arg = stripQuotes(args[i]);
254
+ if (!arg || arg === '--') continue;
255
+
256
+ if (arg.startsWith('-')) {
257
+ if (optionConsumesNext(command, arg)) i++;
258
+ continue;
259
+ }
260
+
261
+ if (looksLikePath(arg)) operands.push(arg);
262
+ }
263
+
264
+ return operands;
265
+ }
266
+
267
+ function optionConsumesNext(command, option) {
268
+ if (command === 'find' && ['-name', '-path', '-type', '-maxdepth', '-mindepth'].includes(option)) return true;
269
+ if (command === 'grep' && ['-e', '-f', '--exclude', '--include'].includes(option)) return true;
270
+ return false;
271
+ }
272
+
273
+ function isCurlWriteOption(word) {
274
+ const option = stripQuotes(word);
275
+ return option === '-o' || option === '-O' || option === '-J' || option === '--output' || option === '--remote-name' || option === '--remote-header-name' || option.startsWith('--output=');
276
+ }
277
+
278
+ function looksLikePath(value) {
279
+ if (!value) return false;
280
+ if (value.includes('*') || value.includes('?') || value.includes('[')) return true;
281
+ return value === '.' || value === '..' || value.startsWith('/') || value.startsWith('./') || value.startsWith('../') || value.includes('/');
282
+ }
283
+
284
+ function resolvePath(cwd, value) {
285
+ return isAbsolute(value) ? resolve(value) : resolve(cwd, value);
286
+ }
287
+
288
+ function isInside(root, candidate) {
289
+ const rel = relative(root, candidate);
290
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
291
+ }
292
+
293
+ function displayPath(root, absolutePath) {
294
+ return isInside(root, absolutePath) ? `/${relative(root, absolutePath)}` : absolutePath;
295
+ }
296
+
297
+ function stripQuotes(value) {
298
+ return String(value).replace(/^["']|["']$/g, '');
299
+ }
300
+
301
+ function formatReasons(reasons) {
302
+ return reasons.length ? `Reasons: ${reasons.join('; ')}.` : '';
303
+ }
package/src/main.js CHANGED
@@ -63,7 +63,7 @@ export async function main() {
63
63
  const border = '─'.repeat(boxWidth);
64
64
 
65
65
  console.log(` ${c.dim}┌${border}┐${c.reset}`);
66
- console.log(` ${c.dim}│${c.reset} ${c.dim}${idLabel}${c.reset} ${c.bold}${c.cyan}${tunnelId}${c.reset} ${c.dim}│${c.reset}`);
66
+ console.log(` ${c.dim}│${c.reset} ${idLabel}${c.reset} ${c.bold}${tunnelId}${c.reset} ${c.dim}│${c.reset}`);
67
67
  console.log(` ${c.dim}└${border}┘${c.reset}`);
68
68
 
69
69
  const webUrl = `https://lucenacoder.com/?tunnel=${tunnelId}`;
@@ -73,7 +73,7 @@ export async function main() {
73
73
  const urlBorder = '─'.repeat(urlBoxWidth);
74
74
 
75
75
  console.log(`\n ${c.dim}┌${urlBorder}┐${c.reset}`);
76
- console.log(` ${c.dim}│${c.reset} ${c.dim}${urlLabel}${c.reset} ${c.cyan}${webUrl}${c.reset} ${c.dim}│${c.reset}`);
76
+ console.log(` ${c.dim}│${c.reset} ${urlLabel}${c.reset} ${c.bold}${webUrl}${c.reset} ${c.dim}│${c.reset}`);
77
77
  console.log(` ${c.dim}└${urlBorder}┘${c.reset}`);
78
78
 
79
79
  console.log(`\n ${c.dim}Open the URL above in your browser to connect.${c.reset}`);
@@ -82,4 +82,4 @@ export async function main() {
82
82
  console.error(`\n ${c.yellow}✖ Failed to start tunnel: ${err.message}${c.reset}\n`);
83
83
  process.exit(1);
84
84
  }
85
- }
85
+ }
@@ -1,4 +0,0 @@
1
- {
2
- "name": "lucenacoder",
3
- "createdAt": "2026-05-24T01:17:13.939Z"
4
- }