@cccarv82/freya 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/cli/index.js +4 -2
  2. package/cli/web.js +867 -118
  3. package/package.json +1 -1
package/cli/index.js CHANGED
@@ -11,7 +11,7 @@ freya - F.R.E.Y.A. CLI
11
11
 
12
12
  Usage:
13
13
  freya init [dir] [--force] [--here|--in-place] [--force-data] [--force-logs]
14
- freya web [--port <n>] [--dir <path>] [--no-open]
14
+ freya web [--port <n>] [--dir <path>] [--no-open] [--dev]
15
15
 
16
16
  Defaults:
17
17
  - If no [dir] is provided, creates ./freya
@@ -27,6 +27,7 @@ Examples:
27
27
 
28
28
  freya web
29
29
  freya web --dir ./freya --port 3872
30
+ freya web --dev # seeds demo data/logs for quick testing
30
31
  `;
31
32
  }
32
33
 
@@ -81,6 +82,7 @@ async function run(argv) {
81
82
  const port = Number(kv['--port'] || 3872);
82
83
  const dir = kv['--dir'] ? path.resolve(process.cwd(), kv['--dir']) : null;
83
84
  const open = !flags.has('--no-open');
85
+ const dev = flags.has('--dev');
84
86
 
85
87
  if (!Number.isFinite(port) || port <= 0) {
86
88
  process.stderr.write('Invalid --port\n');
@@ -88,7 +90,7 @@ async function run(argv) {
88
90
  return;
89
91
  }
90
92
 
91
- await cmdWeb({ port, dir, open });
93
+ await cmdWeb({ port, dir, open, dev });
92
94
  return;
93
95
  }
94
96
 
package/cli/web.js CHANGED
@@ -9,6 +9,10 @@ function guessNpmCmd() {
9
9
  return process.platform === 'win32' ? 'npm.cmd' : 'npm';
10
10
  }
11
11
 
12
+ function guessNpxCmd() {
13
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
14
+ }
15
+
12
16
  function guessOpenCmd() {
13
17
  // Minimal cross-platform opener without extra deps
14
18
  if (process.platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', ''] };
@@ -17,19 +21,33 @@ function guessOpenCmd() {
17
21
  }
18
22
 
19
23
  function exists(p) {
20
- try { fs.accessSync(p); return true; } catch { return false; }
24
+ try {
25
+ fs.accessSync(p);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
21
30
  }
22
31
 
23
32
  function newestFile(dir, prefix) {
24
33
  if (!exists(dir)) return null;
25
- const files = fs.readdirSync(dir)
34
+ const files = fs
35
+ .readdirSync(dir)
26
36
  .filter((f) => f.startsWith(prefix) && f.endsWith('.md'))
27
37
  .map((f) => ({ f, p: path.join(dir, f) }))
28
38
  .filter((x) => {
29
- try { return fs.statSync(x.p).isFile(); } catch { return false; }
39
+ try {
40
+ return fs.statSync(x.p).isFile();
41
+ } catch {
42
+ return false;
43
+ }
30
44
  })
31
45
  .sort((a, b) => {
32
- try { return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs; } catch { return 0; }
46
+ try {
47
+ return fs.statSync(b.p).mtimeMs - fs.statSync(a.p).mtimeMs;
48
+ } catch {
49
+ return 0;
50
+ }
33
51
  });
34
52
  return files[0]?.p || null;
35
53
  }
@@ -55,11 +73,29 @@ function readBody(req) {
55
73
 
56
74
  function run(cmd, args, cwd) {
57
75
  return new Promise((resolve) => {
58
- const child = spawn(cmd, args, { cwd, shell: false, env: process.env });
76
+ let child;
77
+ try {
78
+ child = spawn(cmd, args, { cwd, shell: false, env: process.env });
79
+ } catch (e) {
80
+ return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
81
+ }
82
+
59
83
  let stdout = '';
60
84
  let stderr = '';
61
- child.stdout.on('data', (d) => { stdout += d.toString(); });
62
- child.stderr.on('data', (d) => { stderr += d.toString(); });
85
+
86
+ child.stdout && child.stdout.on('data', (d) => {
87
+ stdout += d.toString();
88
+ });
89
+ child.stderr && child.stderr.on('data', (d) => {
90
+ stderr += d.toString();
91
+ });
92
+
93
+ // Prevent unhandled error event (e.g., ENOENT on Windows when cmd not found)
94
+ child.on('error', (e) => {
95
+ stderr += `\n${e.message || String(e)}`;
96
+ resolve({ code: 1, stdout, stderr });
97
+ });
98
+
63
99
  child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
64
100
  });
65
101
  }
@@ -73,109 +109,652 @@ function openBrowser(url) {
73
109
  }
74
110
  }
75
111
 
112
+ async function pickDirectoryNative() {
113
+ if (process.platform === 'win32') {
114
+ // PowerShell FolderBrowserDialog
115
+ const ps = [
116
+ '-NoProfile',
117
+ '-Command',
118
+ [
119
+ 'Add-Type -AssemblyName System.Windows.Forms;',
120
+ '$f = New-Object System.Windows.Forms.FolderBrowserDialog;',
121
+ "$f.Description = 'Select your FREYA workspace folder';",
122
+ '$f.ShowNewFolderButton = $true;',
123
+ 'if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }'
124
+ ].join(' ')
125
+ ];
126
+ const r = await run('powershell', ps, process.cwd());
127
+ if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
128
+ const out = (r.stdout || '').trim();
129
+ return out || null;
130
+ }
131
+
132
+ if (process.platform === 'darwin') {
133
+ const r = await run('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select your FREYA workspace folder")'], process.cwd());
134
+ if (r.code !== 0) throw new Error((r.stderr || r.stdout || '').trim() || 'Failed to open folder picker');
135
+ const out = (r.stdout || '').trim();
136
+ return out || null;
137
+ }
138
+
139
+ // Linux: prefer zenity, then kdialog
140
+ if (exists('/usr/bin/zenity') || exists('/bin/zenity') || exists('/usr/local/bin/zenity')) {
141
+ const r = await run('zenity', ['--file-selection', '--directory', '--title=Select your FREYA workspace folder'], process.cwd());
142
+ if (r.code !== 0) return null;
143
+ return (r.stdout || '').trim() || null;
144
+ }
145
+ if (exists('/usr/bin/kdialog') || exists('/bin/kdialog') || exists('/usr/local/bin/kdialog')) {
146
+ const r = await run('kdialog', ['--getexistingdirectory', '.', 'Select your FREYA workspace folder'], process.cwd());
147
+ if (r.code !== 0) return null;
148
+ return (r.stdout || '').trim() || null;
149
+ }
150
+
151
+ return null;
152
+ }
153
+
76
154
  function html() {
155
+ // Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
77
156
  return `<!doctype html>
78
157
  <html>
79
158
  <head>
80
159
  <meta charset="utf-8" />
81
160
  <meta name="viewport" content="width=device-width, initial-scale=1" />
82
- <title>FREYA</title>
161
+ <title>FREYA Web</title>
83
162
  <style>
84
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Helvetica, Arial; margin: 0; background: #0b0f14; color: #e6edf3; }
85
- header { padding: 16px 18px; border-bottom: 1px solid #1f2a37; display:flex; gap:12px; align-items:center; }
86
- header h1 { font-size: 14px; margin:0; letter-spacing: .08em; text-transform: uppercase; color:#9fb2c7; }
87
- .wrap { max-width: 980px; margin: 0 auto; padding: 18px; }
88
- .card { border: 1px solid #1f2a37; border-radius: 12px; background: #0f1620; padding: 14px; }
89
- .grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
90
- @media (min-width: 920px) { .grid { grid-template-columns: 1.2fr .8fr; } }
91
- label { font-size: 12px; color:#9fb2c7; display:block; margin-bottom: 6px; }
92
- input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#0b1220; color:#e6edf3; }
93
- .btns { display:flex; flex-wrap:wrap; gap: 10px; margin-top: 10px; }
94
- button { padding: 10px 12px; border-radius: 10px; border: 1px solid #223041; background:#1f6feb; color:white; cursor:pointer; }
95
- button.secondary { background: transparent; color:#e6edf3; }
96
- button.danger { background: #d73a49; }
97
- .row { display:grid; grid-template-columns: 1fr; gap: 10px; }
98
- .small { font-size: 12px; color:#9fb2c7; }
99
- pre { white-space: pre-wrap; background:#0b1220; border: 1px solid #223041; border-radius: 12px; padding: 12px; overflow:auto; }
100
- a { color:#58a6ff; }
163
+ /*
164
+ Design goals:
165
+ - Light theme by default (inspired by your reference screenshots)
166
+ - Dark mode toggle
167
+ - App-like layout: sidebar + main surface
168
+ - Clear onboarding and affordances
169
+ */
170
+
171
+ :root {
172
+ --radius: 14px;
173
+ --shadow: 0 18px 55px rgba(16, 24, 40, .10);
174
+ --shadow2: 0 10px 20px rgba(16, 24, 40, .08);
175
+ --ring: 0 0 0 4px rgba(59, 130, 246, .18);
176
+
177
+ /* Light */
178
+ --bg: #f6f7fb;
179
+ --paper: #ffffff;
180
+ --paper2: #fbfbfd;
181
+ --line: rgba(16, 24, 40, .10);
182
+ --line2: rgba(16, 24, 40, .14);
183
+ --text: #0f172a;
184
+ --muted: rgba(15, 23, 42, .68);
185
+ --faint: rgba(15, 23, 42, .50);
186
+
187
+ --primary: #2563eb;
188
+ --primary2: #0ea5e9;
189
+ --accent: #f97316;
190
+ --ok: #16a34a;
191
+ --warn: #f59e0b;
192
+ --danger: #e11d48;
193
+
194
+ --chip: rgba(37, 99, 235, .08);
195
+ --chip2: rgba(249, 115, 22, .10);
196
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
197
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
198
+ }
199
+
200
+ [data-theme="dark"] {
201
+ --bg: #0b1020;
202
+ --paper: rgba(255,255,255,.06);
203
+ --paper2: rgba(255,255,255,.04);
204
+ --line: rgba(255,255,255,.12);
205
+ --line2: rgba(255,255,255,.18);
206
+ --text: #e9f0ff;
207
+ --muted: rgba(233,240,255,.72);
208
+ --faint: rgba(233,240,255,.54);
209
+
210
+ --primary: #60a5fa;
211
+ --primary2: #22c55e;
212
+ --accent: #fb923c;
213
+ --chip: rgba(96, 165, 250, .14);
214
+ --chip2: rgba(251, 146, 60, .14);
215
+
216
+ --shadow: 0 30px 70px rgba(0,0,0,.55);
217
+ --shadow2: 0 18px 40px rgba(0,0,0,.35);
218
+ --ring: 0 0 0 4px rgba(96, 165, 250, .22);
219
+ }
220
+
221
+ * { box-sizing: border-box; }
222
+ html, body { height: 100%; }
223
+ body {
224
+ margin: 0;
225
+ background:
226
+ radial-gradient(1200px 800px at 20% -10%, rgba(37,99,235,.12), transparent 55%),
227
+ radial-gradient(900px 600px at 92% 10%, rgba(249,115,22,.12), transparent 55%),
228
+ radial-gradient(1100px 700px at 70% 105%, rgba(14,165,233,.10), transparent 55%),
229
+ var(--bg);
230
+ color: var(--text);
231
+ font-family: var(--sans);
232
+ }
233
+
234
+ /* subtle grain */
235
+ body:before {
236
+ content: "";
237
+ position: fixed;
238
+ inset: 0;
239
+ pointer-events: none;
240
+ background-image:
241
+ radial-gradient(circle at 15% 20%, rgba(255,255,255,.38), transparent 32%),
242
+ radial-gradient(circle at 80% 10%, rgba(255,255,255,.26), transparent 38%),
243
+ linear-gradient(transparent 0, transparent 3px, rgba(0,0,0,.02) 4px);
244
+ background-size: 900px 900px, 900px 900px, 100% 7px;
245
+ opacity: .08;
246
+ mix-blend-mode: overlay;
247
+ }
248
+
249
+ .app {
250
+ max-width: 1260px;
251
+ margin: 18px auto;
252
+ padding: 0 18px;
253
+ }
254
+
255
+ .frame {
256
+ display: grid;
257
+ grid-template-columns: 280px 1fr;
258
+ gap: 14px;
259
+ min-height: calc(100vh - 36px);
260
+ }
261
+
262
+ @media (max-width: 980px) {
263
+ .frame { grid-template-columns: 1fr; }
264
+ }
265
+
266
+ .sidebar {
267
+ background: var(--paper);
268
+ border: 1px solid var(--line);
269
+ border-radius: var(--radius);
270
+ box-shadow: var(--shadow2);
271
+ padding: 14px;
272
+ position: sticky;
273
+ top: 18px;
274
+ height: fit-content;
275
+ }
276
+
277
+ .main {
278
+ background: var(--paper);
279
+ border: 1px solid var(--line);
280
+ border-radius: var(--radius);
281
+ box-shadow: var(--shadow);
282
+ overflow: hidden;
283
+ }
284
+
285
+ .topbar {
286
+ display: flex;
287
+ align-items: center;
288
+ justify-content: space-between;
289
+ padding: 14px 16px;
290
+ border-bottom: 1px solid var(--line);
291
+ background: linear-gradient(180deg, var(--paper2), var(--paper));
292
+ }
293
+
294
+ .brand {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 10px;
298
+ font-weight: 800;
299
+ letter-spacing: .08em;
300
+ text-transform: uppercase;
301
+ font-size: 12px;
302
+ color: var(--muted);
303
+ }
304
+
305
+ .spark {
306
+ width: 10px;
307
+ height: 10px;
308
+ border-radius: 4px;
309
+ background: linear-gradient(135deg, var(--accent), var(--primary));
310
+ box-shadow: 0 0 0 6px rgba(249,115,22,.12);
311
+ }
312
+
313
+ .actions {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 10px;
317
+ }
318
+
319
+ .chip {
320
+ font-family: var(--mono);
321
+ font-size: 12px;
322
+ padding: 7px 10px;
323
+ border-radius: 999px;
324
+ border: 1px solid var(--line);
325
+ background: rgba(255,255,255,.55);
326
+ color: var(--faint);
327
+ }
328
+
329
+ [data-theme="dark"] .chip { background: rgba(0,0,0,.20); }
330
+
331
+ .toggle {
332
+ border: 1px solid var(--line);
333
+ border-radius: 999px;
334
+ background: var(--paper2);
335
+ padding: 7px 10px;
336
+ cursor: pointer;
337
+ color: var(--muted);
338
+ font-weight: 700;
339
+ font-size: 12px;
340
+ }
341
+
342
+ .section {
343
+ padding: 16px;
344
+ }
345
+
346
+ h1 {
347
+ margin: 0;
348
+ font-size: 22px;
349
+ letter-spacing: -.02em;
350
+ }
351
+
352
+ .subtitle {
353
+ margin-top: 6px;
354
+ color: var(--muted);
355
+ font-size: 13px;
356
+ line-height: 1.4;
357
+ max-width: 860px;
358
+ }
359
+
360
+ .cards {
361
+ display: grid;
362
+ grid-template-columns: repeat(4, 1fr);
363
+ gap: 12px;
364
+ margin-top: 14px;
365
+ }
366
+
367
+ @media (max-width: 1100px) { .cards { grid-template-columns: repeat(2, 1fr);} }
368
+ @media (max-width: 620px) { .cards { grid-template-columns: 1fr;} }
369
+
370
+ .card {
371
+ border: 1px solid var(--line);
372
+ background: var(--paper2);
373
+ border-radius: 14px;
374
+ padding: 12px;
375
+ display: grid;
376
+ gap: 6px;
377
+ cursor: pointer;
378
+ transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
379
+ box-shadow: 0 1px 0 rgba(16,24,40,.04);
380
+ }
381
+
382
+ .card:hover {
383
+ transform: translateY(-1px);
384
+ border-color: var(--line2);
385
+ box-shadow: 0 10px 22px rgba(16,24,40,.10);
386
+ }
387
+
388
+ .icon {
389
+ width: 34px;
390
+ height: 34px;
391
+ border-radius: 12px;
392
+ display: grid;
393
+ place-items: center;
394
+ background: var(--chip);
395
+ border: 1px solid var(--line);
396
+ color: var(--primary);
397
+ font-weight: 900;
398
+ }
399
+
400
+ .icon.orange { background: var(--chip2); color: var(--accent); }
401
+
402
+ .title {
403
+ font-weight: 800;
404
+ font-size: 13px;
405
+ }
406
+
407
+ .desc {
408
+ color: var(--muted);
409
+ font-size: 12px;
410
+ line-height: 1.35;
411
+ }
412
+
413
+ .grid2 {
414
+ display: grid;
415
+ grid-template-columns: 1fr 1fr;
416
+ gap: 14px;
417
+ margin-top: 14px;
418
+ }
419
+
420
+ @media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
421
+
422
+ .panel {
423
+ border: 1px solid var(--line);
424
+ background: var(--paper);
425
+ border-radius: 14px;
426
+ overflow: hidden;
427
+ }
428
+
429
+ .panelHead {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: space-between;
433
+ padding: 12px 12px;
434
+ border-bottom: 1px solid var(--line);
435
+ background: linear-gradient(180deg, var(--paper2), var(--paper));
436
+ }
437
+
438
+ .panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
439
+
440
+ .panelBody { padding: 12px; }
441
+
442
+ label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
443
+
444
+ input {
445
+ width: 100%;
446
+ padding: 11px 12px;
447
+ border-radius: 12px;
448
+ border: 1px solid var(--line);
449
+ background: rgba(255,255,255,.72);
450
+ color: var(--text);
451
+ outline: none;
452
+ }
453
+
454
+ [data-theme="dark"] input { background: rgba(0,0,0,.16); }
455
+
456
+ input:focus { box-shadow: var(--ring); border-color: rgba(37,99,235,.35); }
457
+
458
+ .row {
459
+ display: grid;
460
+ grid-template-columns: 1fr auto;
461
+ gap: 10px;
462
+ align-items: center;
463
+ }
464
+
465
+ .btn {
466
+ border: 1px solid var(--line);
467
+ border-radius: 12px;
468
+ background: var(--paper2);
469
+ color: var(--text);
470
+ padding: 10px 12px;
471
+ cursor: pointer;
472
+ font-weight: 800;
473
+ font-size: 12px;
474
+ transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
475
+ }
476
+
477
+ .btn:hover { transform: translateY(-1px); border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
478
+ .btn:active { transform: translateY(0); }
479
+
480
+ .btn.primary {
481
+ background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(14,165,233,.12));
482
+ border-color: rgba(37,99,235,.22);
483
+ color: var(--text);
484
+ }
485
+
486
+ .btn.orange {
487
+ background: linear-gradient(135deg, rgba(249,115,22,.16), rgba(37,99,235,.08));
488
+ border-color: rgba(249,115,22,.24);
489
+ }
490
+
491
+ .btn.danger {
492
+ background: rgba(225,29,72,.10);
493
+ border-color: rgba(225,29,72,.28);
494
+ color: var(--text);
495
+ }
496
+
497
+ .btn.small { padding: 9px 10px; font-weight: 800; }
498
+
499
+ .stack { display: flex; flex-wrap: wrap; gap: 10px; }
500
+
501
+ .help {
502
+ margin-top: 8px;
503
+ color: var(--faint);
504
+ font-size: 12px;
505
+ line-height: 1.35;
506
+ }
507
+
508
+ .log {
509
+ border: 1px solid var(--line);
510
+ background: rgba(255,255,255,.65);
511
+ border-radius: 14px;
512
+ padding: 12px;
513
+ font-family: var(--mono);
514
+ font-size: 12px;
515
+ line-height: 1.35;
516
+ white-space: pre-wrap;
517
+ max-height: 420px;
518
+ overflow: auto;
519
+ color: rgba(15,23,42,.92);
520
+ }
521
+
522
+ [data-theme="dark"] .log { background: rgba(0,0,0,.20); color: rgba(233,240,255,.84); }
523
+
524
+ .statusRow { display:flex; align-items:center; justify-content: space-between; gap: 10px; }
525
+
526
+ .pill {
527
+ display: inline-flex;
528
+ align-items: center;
529
+ gap: 8px;
530
+ padding: 7px 10px;
531
+ border-radius: 999px;
532
+ border: 1px solid var(--line);
533
+ background: rgba(255,255,255,.55);
534
+ font-size: 12px;
535
+ color: var(--muted);
536
+ font-family: var(--mono);
537
+ }
538
+
539
+ [data-theme="dark"] .pill { background: rgba(0,0,0,.18); }
540
+
541
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(245,158,11,.15); }
542
+ .dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(22,163,74,.12); }
543
+ .dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(225,29,72,.14); }
544
+
545
+ .small {
546
+ font-size: 12px;
547
+ color: var(--faint);
548
+ font-family: var(--mono);
549
+ }
550
+
551
+ .sidebar h3 {
552
+ margin: 0;
553
+ font-size: 12px;
554
+ letter-spacing: .10em;
555
+ text-transform: uppercase;
556
+ color: var(--muted);
557
+ }
558
+
559
+ .sideBlock { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
560
+
561
+ .sidePath {
562
+ margin-top: 8px;
563
+ border: 1px solid var(--line);
564
+ background: var(--paper2);
565
+ border-radius: 12px;
566
+ padding: 10px;
567
+ font-family: var(--mono);
568
+ font-size: 12px;
569
+ color: var(--muted);
570
+ word-break: break-word;
571
+ }
572
+
573
+ .sideBtn { width: 100%; margin-top: 8px; }
574
+
575
+ .k { display: inline-block; padding: 2px 6px; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,.65); font-family: var(--mono); font-size: 12px; color: var(--muted); }
576
+ [data-theme="dark"] .k { background: rgba(0,0,0,.18); }
577
+
101
578
  </style>
102
579
  </head>
103
580
  <body>
104
- <header>
105
- <h1>FREYA • Local-first Status Assistant</h1>
106
- <span class="small" id="status"></span>
107
- </header>
108
- <div class="wrap">
109
- <div class="grid">
110
- <div class="card">
111
- <div class="row">
112
- <div>
113
- <label>Workspace dir</label>
114
- <input id="dir" placeholder="/path/to/freya (or ./freya)" />
115
- <div class="small">Dica: a workspace é a pasta que contém <code>data/</code>, <code>logs/</code>, <code>scripts/</code>.</div>
116
- </div>
117
- <div class="btns">
118
- <button onclick="doInit()">Init (preserva data/logs)</button>
119
- <button class="secondary" onclick="doUpdate()">Update (init --here)</button>
120
- <button class="secondary" onclick="doHealth()">Health</button>
121
- <button class="secondary" onclick="doMigrate()">Migrate</button>
122
- </div>
581
+ <div class="app">
582
+ <div class="frame">
583
+
584
+ <aside class="sidebar">
585
+ <div style="display:flex; align-items:center; justify-content: space-between; gap:10px;">
586
+ <h3>FREYA</h3>
587
+ <span class="pill"><span class="dot" id="dot"></span><span id="pill">idle</span></span>
123
588
  </div>
124
589
 
125
- <hr style="border:0;border-top:1px solid #1f2a37;margin:14px 0" />
590
+ <div class="sideBlock">
591
+ <h3>Workspace</h3>
592
+ <div class="sidePath" id="sidePath">./freya</div>
593
+ <button class="btn sideBtn" onclick="pickDir()">Select workspace…</button>
594
+ <button class="btn primary sideBtn" onclick="doInit()">Init workspace</button>
595
+ <button class="btn sideBtn" onclick="doUpdate()">Update (preserve data/logs)</button>
596
+ <button class="btn sideBtn" onclick="doHealth()">Health</button>
597
+ <button class="btn sideBtn" onclick="doMigrate()">Migrate</button>
598
+ <div class="help">Dica: se você já tem uma workspace antiga, use <b>Update</b>. Por padrão, <b>data/</b> e <b>logs/</b> não são sobrescritos.</div>
599
+ </div>
126
600
 
127
- <div class="btns">
128
- <button onclick="runReport('status')">Generate Executive Report</button>
129
- <button onclick="runReport('sm-weekly')">Generate SM Weekly</button>
130
- <button onclick="runReport('blockers')">Generate Blockers</button>
131
- <button onclick="runReport('daily')">Generate Daily</button>
601
+ <div class="sideBlock">
602
+ <h3>Publish</h3>
603
+ <button class="btn sideBtn" onclick="publish('discord')">Publish Discord</button>
604
+ <button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
605
+ <div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
132
606
  </div>
133
607
 
134
- <div style="margin-top:12px">
135
- <label>Output</label>
136
- <pre id="out"></pre>
137
- <div class="small" id="last"></div>
608
+ <div class="sideBlock">
609
+ <h3>Atalhos</h3>
610
+ <div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
611
+ <div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
138
612
  </div>
139
- </div>
613
+ </aside>
140
614
 
141
- <div class="card">
142
- <div class="row">
143
- <div>
144
- <label>Discord Webhook URL (optional)</label>
145
- <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
146
- </div>
147
- <div>
148
- <label>Teams Webhook URL (optional)</label>
149
- <input id="teams" placeholder="https://..." />
615
+ <main class="main">
616
+ <div class="topbar">
617
+ <div class="brand"><span class="spark"></span> Local-first status assistant</div>
618
+ <div class="actions">
619
+ <span class="chip" id="chipPort">127.0.0.1:3872</span>
620
+ <button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
150
621
  </div>
151
- <div class="btns">
152
- <button class="secondary" onclick="publish('discord')">Publish last → Discord</button>
153
- <button class="secondary" onclick="publish('teams')">Publish last → Teams</button>
622
+ </div>
623
+
624
+ <div class="section">
625
+ <h1>Morning, how can I help?</h1>
626
+ <div class="subtitle">Selecione uma workspace e gere relatórios (Executive / SM / Blockers / Daily). Você pode publicar no Discord/Teams com 1 clique.</div>
627
+
628
+ <div class="cards">
629
+ <div class="card" onclick="runReport('status')">
630
+ <div class="icon">E</div>
631
+ <div class="title">Executive report</div>
632
+ <div class="desc">Status pronto para stakeholders (entregas, projetos, blockers).</div>
633
+ </div>
634
+ <div class="card" onclick="runReport('sm-weekly')">
635
+ <div class="icon">S</div>
636
+ <div class="title">SM weekly</div>
637
+ <div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
638
+ </div>
639
+ <div class="card" onclick="runReport('blockers')">
640
+ <div class="icon orange">B</div>
641
+ <div class="title">Blockers</div>
642
+ <div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
643
+ </div>
644
+ <div class="card" onclick="runReport('daily')">
645
+ <div class="icon">D</div>
646
+ <div class="title">Daily</div>
647
+ <div class="desc">Ontem / Hoje / Bloqueios — pronto pra standup.</div>
648
+ </div>
154
649
  </div>
155
- <div class="small">
156
- Publica o último relatório gerado (cache local). Limite: ~1800 chars (pra evitar limites de webhook).
650
+
651
+ <div class="grid2">
652
+ <div class="panel">
653
+ <div class="panelHead"><b>Workspace & publish settings</b><span class="small" id="last"></span></div>
654
+ <div class="panelBody">
655
+ <label>Workspace dir</label>
656
+ <div class="row">
657
+ <input id="dir" placeholder="./freya" />
658
+ <button class="btn small" onclick="pickDir()">Browse</button>
659
+ </div>
660
+ <div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
661
+
662
+ <div style="height:12px"></div>
663
+
664
+ <div class="stack">
665
+ <button class="btn primary" onclick="doInit()">Init</button>
666
+ <button class="btn" onclick="doUpdate()">Update</button>
667
+ <button class="btn" onclick="doHealth()">Health</button>
668
+ <button class="btn" onclick="doMigrate()">Migrate</button>
669
+ </div>
670
+
671
+ <div style="height:16px"></div>
672
+
673
+ <label>Discord webhook URL</label>
674
+ <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
675
+ <div style="height:10px"></div>
676
+
677
+ <label>Teams webhook URL</label>
678
+ <input id="teams" placeholder="https://..." />
679
+ <div class="help">O publish usa incoming webhooks. (Depois a gente evolui para anexos/chunks.)</div>
680
+
681
+ <div style="height:10px"></div>
682
+ <div class="stack">
683
+ <button class="btn" onclick="publish('discord')">Publish last → Discord</button>
684
+ <button class="btn" onclick="publish('teams')">Publish last → Teams</button>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <div class="panel">
690
+ <div class="panelHead">
691
+ <b>Output</b>
692
+ <div class="stack">
693
+ <button class="btn small" onclick="copyOut()">Copy</button>
694
+ <button class="btn small" onclick="clearOut()">Clear</button>
695
+ </div>
696
+ </div>
697
+ <div class="panelBody">
698
+ <div class="log" id="out"></div>
699
+ <div class="help">Dica: quando um report gera arquivo, mostramos o conteúdo real do report aqui (melhor que stdout).</div>
700
+ </div>
701
+ </div>
702
+
157
703
  </div>
158
704
  </div>
159
- </div>
705
+
706
+ </main>
160
707
 
161
708
  </div>
162
709
  </div>
163
710
 
164
711
  <script>
165
712
  const $ = (id) => document.getElementById(id);
166
- const state = {
167
- lastReportPath: null,
168
- lastText: ''
169
- };
713
+ const state = { lastReportPath: null, lastText: '' };
714
+
715
+ function applyTheme(theme) {
716
+ document.documentElement.setAttribute('data-theme', theme);
717
+ localStorage.setItem('freya.theme', theme);
718
+ $('themeToggle').textContent = theme === 'dark' ? 'Light' : 'Dark';
719
+ }
720
+
721
+ function toggleTheme() {
722
+ const t = localStorage.getItem('freya.theme') || 'light';
723
+ applyTheme(t === 'dark' ? 'light' : 'dark');
724
+ }
725
+
726
+ function setPill(kind, text) {
727
+ const dot = $('dot');
728
+ dot.classList.remove('ok','err');
729
+ if (kind === 'ok') dot.classList.add('ok');
730
+ if (kind === 'err') dot.classList.add('err');
731
+ $('pill').textContent = text;
732
+ $('status') && ($('status').textContent = text);
733
+ }
170
734
 
171
735
  function setOut(text) {
172
- state.lastText = text;
736
+ state.lastText = text || '';
173
737
  $('out').textContent = text || '';
174
738
  }
175
739
 
176
- function setLast(path) {
177
- state.lastReportPath = path;
178
- $('last').textContent = path ? ('Last report: ' + path) : '';
740
+ function clearOut() {
741
+ setOut('');
742
+ setPill('ok', 'idle');
743
+ }
744
+
745
+ async function copyOut() {
746
+ try {
747
+ await navigator.clipboard.writeText(state.lastText || '');
748
+ setPill('ok','copied');
749
+ setTimeout(() => setPill('ok','idle'), 800);
750
+ } catch (e) {
751
+ setPill('err','copy failed');
752
+ }
753
+ }
754
+
755
+ function setLast(p) {
756
+ state.lastReportPath = p;
757
+ $('last').textContent = p ? ('Last report: ' + p) : '';
179
758
  }
180
759
 
181
760
  function saveLocal() {
@@ -185,13 +764,14 @@ function html() {
185
764
  }
186
765
 
187
766
  function loadLocal() {
188
- $('dir').value = localStorage.getItem('freya.dir') || '';
767
+ $('dir').value = localStorage.getItem('freya.dir') || './freya';
189
768
  $('discord').value = localStorage.getItem('freya.discord') || '';
190
769
  $('teams').value = localStorage.getItem('freya.teams') || '';
770
+ $('sidePath').textContent = $('dir').value || './freya';
191
771
  }
192
772
 
193
- async function api(path, body) {
194
- const res = await fetch(path, {
773
+ async function api(p, body) {
774
+ const res = await fetch(p, {
195
775
  method: body ? 'POST' : 'GET',
196
776
  headers: body ? { 'Content-Type': 'application/json' } : {},
197
777
  body: body ? JSON.stringify(body) : undefined
@@ -206,63 +786,210 @@ function html() {
206
786
  return d || './freya';
207
787
  }
208
788
 
789
+ async function pickDir() {
790
+ try {
791
+ setPill('run','picker…');
792
+ const r = await api('/api/pick-dir', {});
793
+ if (r && r.dir) {
794
+ $('dir').value = r.dir;
795
+ $('sidePath').textContent = r.dir;
796
+ }
797
+ saveLocal();
798
+ setPill('ok','ready');
799
+ } catch (e) {
800
+ setPill('err','picker failed');
801
+ setOut(String(e && e.message ? e.message : e));
802
+ }
803
+ }
804
+
209
805
  async function doInit() {
210
- saveLocal();
211
- setOut('Running init...');
212
- const r = await api('/api/init', { dir: dirOrDefault() });
213
- setOut(r.output);
214
- setLast(null);
806
+ try {
807
+ saveLocal();
808
+ $('sidePath').textContent = dirOrDefault();
809
+ setPill('run','init…');
810
+ setOut('');
811
+ const r = await api('/api/init', { dir: dirOrDefault() });
812
+ setOut(r.output);
813
+ setLast(null);
814
+ setPill('ok','init ok');
815
+ } catch (e) {
816
+ setPill('err','init failed');
817
+ setOut(String(e && e.message ? e.message : e));
818
+ }
215
819
  }
216
820
 
217
821
  async function doUpdate() {
218
- saveLocal();
219
- setOut('Running update...');
220
- const r = await api('/api/update', { dir: dirOrDefault() });
221
- setOut(r.output);
222
- setLast(null);
822
+ try {
823
+ saveLocal();
824
+ $('sidePath').textContent = dirOrDefault();
825
+ setPill('run','update…');
826
+ setOut('');
827
+ const r = await api('/api/update', { dir: dirOrDefault() });
828
+ setOut(r.output);
829
+ setLast(null);
830
+ setPill('ok','update ok');
831
+ } catch (e) {
832
+ setPill('err','update failed');
833
+ setOut(String(e && e.message ? e.message : e));
834
+ }
223
835
  }
224
836
 
225
837
  async function doHealth() {
226
- saveLocal();
227
- setOut('Running health...');
228
- const r = await api('/api/health', { dir: dirOrDefault() });
229
- setOut(r.output);
230
- setLast(null);
838
+ try {
839
+ saveLocal();
840
+ $('sidePath').textContent = dirOrDefault();
841
+ setPill('run','health…');
842
+ setOut('');
843
+ const r = await api('/api/health', { dir: dirOrDefault() });
844
+ setOut(r.output);
845
+ setLast(null);
846
+ setPill('ok','health ok');
847
+ } catch (e) {
848
+ setPill('err','health failed');
849
+ setOut(String(e && e.message ? e.message : e));
850
+ }
231
851
  }
232
852
 
233
853
  async function doMigrate() {
234
- saveLocal();
235
- setOut('Running migrate...');
236
- const r = await api('/api/migrate', { dir: dirOrDefault() });
237
- setOut(r.output);
238
- setLast(null);
854
+ try {
855
+ saveLocal();
856
+ $('sidePath').textContent = dirOrDefault();
857
+ setPill('run','migrate…');
858
+ setOut('');
859
+ const r = await api('/api/migrate', { dir: dirOrDefault() });
860
+ setOut(r.output);
861
+ setLast(null);
862
+ setPill('ok','migrate ok');
863
+ } catch (e) {
864
+ setPill('err','migrate failed');
865
+ setOut(String(e && e.message ? e.message : e));
866
+ }
239
867
  }
240
868
 
241
869
  async function runReport(name) {
242
- saveLocal();
243
- setOut('Running ' + name + '...');
244
- const r = await api('/api/report', { dir: dirOrDefault(), script: name });
245
- setOut(r.output);
246
- setLast(r.reportPath || null);
870
+ try {
871
+ saveLocal();
872
+ $('sidePath').textContent = dirOrDefault();
873
+ setPill('run', name + '…');
874
+ setOut('');
875
+ const r = await api('/api/report', { dir: dirOrDefault(), script: name });
876
+ setOut(r.output);
877
+ setLast(r.reportPath || null);
878
+ if (r.reportText) state.lastText = r.reportText;
879
+ setPill('ok', name + ' ok');
880
+ } catch (e) {
881
+ setPill('err', name + ' failed');
882
+ setOut(String(e && e.message ? e.message : e));
883
+ }
247
884
  }
248
885
 
249
886
  async function publish(target) {
250
- saveLocal();
251
- if (!state.lastText) throw new Error('No cached output. Generate a report first.');
252
- const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
253
- setOut('Publishing to ' + target + '...');
254
- const r = await api('/api/publish', { webhookUrl, text: state.lastText });
255
- setOut('Published.');
887
+ try {
888
+ saveLocal();
889
+ if (!state.lastText) throw new Error('Gere um relatório primeiro.');
890
+ const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
891
+ if (!webhookUrl) throw new Error('Configure o webhook antes.');
892
+ setPill('run','publish…');
893
+ await api('/api/publish', { webhookUrl, text: state.lastText });
894
+ setPill('ok','published');
895
+ } catch (e) {
896
+ setPill('err','publish failed');
897
+ setOut(String(e && e.message ? e.message : e));
898
+ }
256
899
  }
257
900
 
901
+ // init
902
+ applyTheme(localStorage.getItem('freya.theme') || 'light');
903
+ $('chipPort').textContent = location.host;
258
904
  loadLocal();
259
- $('status').textContent = 'ready';
905
+ setPill('ok','idle');
260
906
  </script>
261
907
  </body>
262
908
  </html>`;
263
909
  }
264
910
 
265
- async function cmdWeb({ port, dir, open }) {
911
+ function ensureDir(p) {
912
+ fs.mkdirSync(p, { recursive: true });
913
+ }
914
+
915
+ function isoDate(d = new Date()) {
916
+ return d.toISOString().slice(0, 10);
917
+ }
918
+
919
+ function isoNow() {
920
+ return new Date().toISOString();
921
+ }
922
+
923
+ function seedDevWorkspace(workspaceDir) {
924
+ // Only create if missing; never overwrite user content.
925
+ ensureDir(path.join(workspaceDir, 'data', 'tasks'));
926
+ ensureDir(path.join(workspaceDir, 'data', 'career'));
927
+ ensureDir(path.join(workspaceDir, 'data', 'blockers'));
928
+ ensureDir(path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket'));
929
+ ensureDir(path.join(workspaceDir, 'logs', 'daily'));
930
+
931
+ const taskLog = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
932
+ if (!exists(taskLog)) {
933
+ fs.writeFileSync(taskLog, JSON.stringify({
934
+ schemaVersion: 1,
935
+ tasks: [
936
+ { id: 't-demo-1', description: 'Preparar update executivo', category: 'DO_NOW', status: 'PENDING', createdAt: isoNow(), priority: 'high' },
937
+ { id: 't-demo-2', description: 'Revisar PR de integração Teams', category: 'SCHEDULE', status: 'PENDING', createdAt: isoNow(), priority: 'medium' },
938
+ { id: 't-demo-3', description: 'Rodar retro e registrar aprendizados', category: 'DO_NOW', status: 'COMPLETED', createdAt: isoNow(), completedAt: isoNow(), priority: 'low' }
939
+ ]
940
+ }, null, 2) + '\n', 'utf8');
941
+ }
942
+
943
+ const careerLog = path.join(workspaceDir, 'data', 'career', 'career-log.json');
944
+ if (!exists(careerLog)) {
945
+ fs.writeFileSync(careerLog, JSON.stringify({
946
+ schemaVersion: 1,
947
+ entries: [
948
+ { id: 'c-demo-1', date: isoDate(), type: 'Achievement', description: 'Publicou o CLI @cccarv82/freya com init/web', tags: ['shipping', 'tooling'], source: 'dev-seed' },
949
+ { id: 'c-demo-2', date: isoDate(), type: 'Feedback', description: 'Feedback: UX do painel web está “muito promissor”', tags: ['product'], source: 'dev-seed' }
950
+ ]
951
+ }, null, 2) + '\n', 'utf8');
952
+ }
953
+
954
+ const blockerLog = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
955
+ if (!exists(blockerLog)) {
956
+ fs.writeFileSync(blockerLog, JSON.stringify({
957
+ schemaVersion: 1,
958
+ blockers: [
959
+ { id: 'b-demo-1', title: 'Webhook do Teams falhando em ambientes com 2FA', description: 'Ajustar token / payload', createdAt: isoNow(), status: 'OPEN', severity: 'HIGH', nextAction: 'Validar payload e limites' },
960
+ { id: 'b-demo-2', title: 'Definir template de status report por cliente', description: 'Padronizar headings', createdAt: isoNow(), status: 'MITIGATING', severity: 'MEDIUM' }
961
+ ]
962
+ }, null, 2) + '\n', 'utf8');
963
+ }
964
+
965
+ const projectStatus = path.join(workspaceDir, 'data', 'Clients', 'acme', 'rocket', 'status.json');
966
+ if (!exists(projectStatus)) {
967
+ fs.writeFileSync(projectStatus, JSON.stringify({
968
+ schemaVersion: 1,
969
+ client: 'Acme',
970
+ project: 'Rocket',
971
+ currentStatus: 'Green — progressing as planned',
972
+ lastUpdated: isoNow(),
973
+ active: true,
974
+ history: [
975
+ { date: isoNow(), type: 'Status', content: 'Launched stage 1', source: 'dev-seed' },
976
+ { date: isoNow(), type: 'Risk', content: 'Potential delay on vendor dependency', source: 'dev-seed' }
977
+ ]
978
+ }, null, 2) + '\n', 'utf8');
979
+ }
980
+
981
+ const dailyLog = path.join(workspaceDir, 'logs', 'daily', `${isoDate()}.md`);
982
+ if (!exists(dailyLog)) {
983
+ fs.writeFileSync(dailyLog, `# Daily Log ${isoDate()}\n\n## [09:15] Raw Input\nReunião com a Acme. Tudo verde, mas preciso alinhar com fornecedor.\n\n## [16:40] Raw Input\nTerminei o relatório SM e publiquei no Discord.\n`, 'utf8');
984
+ }
985
+
986
+ return {
987
+ seeded: true,
988
+ paths: { taskLog, careerLog, blockerLog, projectStatus, dailyLog }
989
+ };
990
+ }
991
+
992
+ async function cmdWeb({ port, dir, open, dev }) {
266
993
  const host = '127.0.0.1';
267
994
 
268
995
  const server = http.createServer(async (req, res) => {
@@ -282,16 +1009,21 @@ async function cmdWeb({ port, dir, open }) {
282
1009
 
283
1010
  const workspaceDir = path.resolve(process.cwd(), payload.dir || dir || './freya');
284
1011
 
1012
+ if (req.url === '/api/pick-dir') {
1013
+ const picked = await pickDirectoryNative();
1014
+ return safeJson(res, 200, { dir: picked });
1015
+ }
1016
+
285
1017
  if (req.url === '/api/init') {
286
1018
  const pkg = '@cccarv82/freya';
287
- const r = await run('npx', [pkg, 'init', workspaceDir], process.cwd());
1019
+ const r = await run(guessNpxCmd(), [pkg, 'init', workspaceDir], process.cwd());
288
1020
  return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
289
1021
  }
290
1022
 
291
1023
  if (req.url === '/api/update') {
292
1024
  const pkg = '@cccarv82/freya';
293
1025
  fs.mkdirSync(workspaceDir, { recursive: true });
294
- const r = await run('npx', [pkg, 'init', '--here'], workspaceDir);
1026
+ const r = await run(guessNpxCmd(), [pkg, 'init', '--here'], workspaceDir);
295
1027
  return safeJson(res, r.code === 0 ? 200 : 400, { output: (r.stdout + r.stderr).trim() });
296
1028
  }
297
1029
 
@@ -323,8 +1055,12 @@ async function cmdWeb({ port, dir, open }) {
323
1055
  };
324
1056
  const prefix = prefixMap[script] || null;
325
1057
  const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
1058
+ const reportText = reportPath && exists(reportPath) ? fs.readFileSync(reportPath, 'utf8') : null;
1059
+
1060
+ // Prefer showing the actual report content when available.
1061
+ const output = reportText ? reportText : out;
326
1062
 
327
- return safeJson(res, r.code === 0 ? 200 : 400, { output: out, reportPath });
1063
+ return safeJson(res, r.code === 0 ? 200 : 400, { output, reportPath, reportText });
328
1064
  }
329
1065
 
330
1066
  if (req.url === '/api/publish') {
@@ -348,7 +1084,8 @@ async function cmdWeb({ port, dir, open }) {
348
1084
  }
349
1085
  };
350
1086
 
351
- const req2 = require(u.protocol === 'https:' ? 'https' : 'http').request(options, (r2) => {
1087
+ const proto = u.protocol === 'https:' ? require('https') : require('http');
1088
+ const req2 = proto.request(options, (r2) => {
352
1089
  const chunks = [];
353
1090
  r2.on('data', (c) => chunks.push(c));
354
1091
  r2.on('end', () => {
@@ -374,6 +1111,18 @@ async function cmdWeb({ port, dir, open }) {
374
1111
  await new Promise((resolve) => server.listen(port, host, resolve));
375
1112
 
376
1113
  const url = `http://${host}:${port}/`;
1114
+
1115
+ // Optional dev seed (safe: only creates files if missing)
1116
+ if (dev) {
1117
+ const target = dir ? path.resolve(process.cwd(), dir) : path.join(process.cwd(), 'freya');
1118
+ try {
1119
+ seedDevWorkspace(target);
1120
+ process.stdout.write(`Dev seed: created demo files in ${target}\n`);
1121
+ } catch (e) {
1122
+ process.stdout.write(`Dev seed failed: ${e.message || String(e)}\n`);
1123
+ }
1124
+ }
1125
+
377
1126
  process.stdout.write(`FREYA web running at ${url}\n`);
378
1127
  if (open) openBrowser(url);
379
1128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",