@cccarv82/freya 1.0.15 → 1.0.17
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/.agent/rules/freya/agents/coach.mdc +72 -0
- package/.agent/rules/freya/agents/ingestor.mdc +183 -0
- package/.agent/rules/freya/agents/master.mdc +93 -0
- package/.agent/rules/freya/agents/oracle.mdc +102 -0
- package/.agent/rules/freya/freya.mdc +31 -0
- package/cli/web-ui.css +225 -0
- package/cli/web-ui.js +575 -0
- package/cli/web.js +581 -975
- package/package.json +4 -3
package/cli/web.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
6
7
|
const { spawn } = require('child_process');
|
|
7
8
|
|
|
8
9
|
function guessNpmCmd() {
|
|
@@ -120,6 +121,81 @@ function listReports(workspaceDir) {
|
|
|
120
121
|
}));
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
function splitForDiscord(text, limit = 1900) {
|
|
125
|
+
const t = String(text || '');
|
|
126
|
+
if (t.length <= limit) return [t];
|
|
127
|
+
|
|
128
|
+
const NL = String.fromCharCode(10);
|
|
129
|
+
const NL2 = NL + NL;
|
|
130
|
+
|
|
131
|
+
const parts = [];
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < t.length) {
|
|
134
|
+
let end = Math.min(t.length, i + limit);
|
|
135
|
+
const window = t.slice(i, end);
|
|
136
|
+
const cut = window.lastIndexOf(NL2);
|
|
137
|
+
const cut2 = window.lastIndexOf(NL);
|
|
138
|
+
if (cut > 400) end = i + cut;
|
|
139
|
+
else if (cut2 > 600) end = i + cut2;
|
|
140
|
+
const chunk = t.slice(i, end).trim();
|
|
141
|
+
if (chunk) parts.push(chunk);
|
|
142
|
+
i = end;
|
|
143
|
+
}
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function postJson(url, bodyObj) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const u = new URL(url);
|
|
150
|
+
const body = JSON.stringify(bodyObj);
|
|
151
|
+
const options = {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
hostname: u.hostname,
|
|
154
|
+
path: u.pathname + u.search,
|
|
155
|
+
headers: {
|
|
156
|
+
'Content-Type': 'application/json',
|
|
157
|
+
'Content-Length': Buffer.byteLength(body)
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const proto = u.protocol === 'https:' ? require('https') : require('http');
|
|
162
|
+
const req2 = proto.request(options, (r2) => {
|
|
163
|
+
const chunks = [];
|
|
164
|
+
r2.on('data', (c) => chunks.push(c));
|
|
165
|
+
r2.on('end', () => {
|
|
166
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
167
|
+
if (r2.statusCode >= 200 && r2.statusCode < 300) return resolve({ ok: true, status: r2.statusCode, body: raw });
|
|
168
|
+
return reject(new Error('Webhook error ' + r2.statusCode + ': ' + raw));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
req2.on('error', reject);
|
|
172
|
+
req2.write(body);
|
|
173
|
+
req2.end();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function postDiscordWebhook(url, content) {
|
|
178
|
+
return postJson(url, { content });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function postTeamsWebhook(url, text) {
|
|
182
|
+
return postJson(url, { text });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function publishRobust(webhookUrl, text, opts = {}) {
|
|
186
|
+
const u = new URL(webhookUrl);
|
|
187
|
+
const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
|
|
188
|
+
|
|
189
|
+
const chunks = isDiscord ? splitForDiscord(text, 1900) : splitForDiscord(text, 1800);
|
|
190
|
+
|
|
191
|
+
for (const chunk of chunks) {
|
|
192
|
+
if (isDiscord) await postDiscordWebhook(webhookUrl, chunk);
|
|
193
|
+
else await postTeamsWebhook(webhookUrl, chunk);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { ok: true, chunks: chunks.length, mode: 'chunks' };
|
|
197
|
+
}
|
|
198
|
+
|
|
123
199
|
function safeJson(res, code, obj) {
|
|
124
200
|
const body = JSON.stringify(obj);
|
|
125
201
|
res.writeHead(code, {
|
|
@@ -249,989 +325,175 @@ async function pickDirectoryNative() {
|
|
|
249
325
|
function html(defaultDir) {
|
|
250
326
|
// Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
|
|
251
327
|
const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
328
|
+
return buildHtml(safeDefault);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildHtml(safeDefault) {
|
|
252
332
|
return `<!doctype html>
|
|
253
333
|
<html>
|
|
254
334
|
<head>
|
|
255
335
|
<meta charset="utf-8" />
|
|
256
336
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
257
337
|
<title>FREYA Web</title>
|
|
258
|
-
<
|
|
259
|
-
/*
|
|
260
|
-
Design goals:
|
|
261
|
-
- Light theme by default (inspired by your reference screenshots)
|
|
262
|
-
- Dark mode toggle
|
|
263
|
-
- App-like layout: sidebar + main surface
|
|
264
|
-
- Clear onboarding and affordances
|
|
265
|
-
*/
|
|
266
|
-
|
|
267
|
-
:root {
|
|
268
|
-
--radius: 14px;
|
|
269
|
-
--shadow: 0 18px 55px rgba(16, 24, 40, .10);
|
|
270
|
-
--shadow2: 0 10px 20px rgba(16, 24, 40, .08);
|
|
271
|
-
--ring: 0 0 0 4px rgba(59, 130, 246, .18);
|
|
272
|
-
|
|
273
|
-
/* Light */
|
|
274
|
-
--bg: #f6f7fb;
|
|
275
|
-
--paper: #ffffff;
|
|
276
|
-
--paper2: #fbfbfd;
|
|
277
|
-
--line: rgba(16, 24, 40, .10);
|
|
278
|
-
--line2: rgba(16, 24, 40, .14);
|
|
279
|
-
--text: #0f172a;
|
|
280
|
-
--muted: rgba(15, 23, 42, .68);
|
|
281
|
-
--faint: rgba(15, 23, 42, .50);
|
|
282
|
-
|
|
283
|
-
--primary: #2563eb;
|
|
284
|
-
--primary2: #0ea5e9;
|
|
285
|
-
--accent: #f97316;
|
|
286
|
-
--ok: #16a34a;
|
|
287
|
-
--warn: #f59e0b;
|
|
288
|
-
--danger: #e11d48;
|
|
289
|
-
|
|
290
|
-
--chip: rgba(37, 99, 235, .08);
|
|
291
|
-
--chip2: rgba(249, 115, 22, .10);
|
|
292
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
293
|
-
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
[data-theme="dark"] {
|
|
297
|
-
--bg: #0b1020;
|
|
298
|
-
--paper: rgba(255,255,255,.06);
|
|
299
|
-
--paper2: rgba(255,255,255,.04);
|
|
300
|
-
--line: rgba(255,255,255,.12);
|
|
301
|
-
--line2: rgba(255,255,255,.18);
|
|
302
|
-
--text: #e9f0ff;
|
|
303
|
-
--muted: rgba(233,240,255,.72);
|
|
304
|
-
--faint: rgba(233,240,255,.54);
|
|
305
|
-
|
|
306
|
-
--primary: #60a5fa;
|
|
307
|
-
--primary2: #22c55e;
|
|
308
|
-
--accent: #fb923c;
|
|
309
|
-
--chip: rgba(96, 165, 250, .14);
|
|
310
|
-
--chip2: rgba(251, 146, 60, .14);
|
|
311
|
-
|
|
312
|
-
--shadow: 0 30px 70px rgba(0,0,0,.55);
|
|
313
|
-
--shadow2: 0 18px 40px rgba(0,0,0,.35);
|
|
314
|
-
--ring: 0 0 0 4px rgba(96, 165, 250, .22);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
* { box-sizing: border-box; }
|
|
318
|
-
html, body { height: 100%; }
|
|
319
|
-
body {
|
|
320
|
-
margin: 0;
|
|
321
|
-
background:
|
|
322
|
-
radial-gradient(1200px 800px at 20% -10%, rgba(37,99,235,.12), transparent 55%),
|
|
323
|
-
radial-gradient(900px 600px at 92% 10%, rgba(249,115,22,.12), transparent 55%),
|
|
324
|
-
radial-gradient(1100px 700px at 70% 105%, rgba(14,165,233,.10), transparent 55%),
|
|
325
|
-
var(--bg);
|
|
326
|
-
color: var(--text);
|
|
327
|
-
font-family: var(--sans);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/* subtle grain */
|
|
331
|
-
body:before {
|
|
332
|
-
content: "";
|
|
333
|
-
position: fixed;
|
|
334
|
-
inset: 0;
|
|
335
|
-
pointer-events: none;
|
|
336
|
-
background-image:
|
|
337
|
-
radial-gradient(circle at 15% 20%, rgba(255,255,255,.38), transparent 32%),
|
|
338
|
-
radial-gradient(circle at 80% 10%, rgba(255,255,255,.26), transparent 38%),
|
|
339
|
-
linear-gradient(transparent 0, transparent 3px, rgba(0,0,0,.02) 4px);
|
|
340
|
-
background-size: 900px 900px, 900px 900px, 100% 7px;
|
|
341
|
-
opacity: .08;
|
|
342
|
-
mix-blend-mode: overlay;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
.app {
|
|
346
|
-
max-width: 1260px;
|
|
347
|
-
margin: 18px auto;
|
|
348
|
-
padding: 0 18px;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
.frame {
|
|
352
|
-
display: grid;
|
|
353
|
-
grid-template-columns: 280px 1fr;
|
|
354
|
-
gap: 14px;
|
|
355
|
-
min-height: calc(100vh - 36px);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
@media (max-width: 980px) {
|
|
359
|
-
.frame { grid-template-columns: 1fr; }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
.sidebar {
|
|
363
|
-
background: var(--paper);
|
|
364
|
-
border: 1px solid var(--line);
|
|
365
|
-
border-radius: var(--radius);
|
|
366
|
-
box-shadow: var(--shadow2);
|
|
367
|
-
padding: 14px;
|
|
368
|
-
position: sticky;
|
|
369
|
-
top: 18px;
|
|
370
|
-
height: fit-content;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.main {
|
|
374
|
-
background: var(--paper);
|
|
375
|
-
border: 1px solid var(--line);
|
|
376
|
-
border-radius: var(--radius);
|
|
377
|
-
box-shadow: var(--shadow);
|
|
378
|
-
overflow: hidden;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.topbar {
|
|
382
|
-
display: flex;
|
|
383
|
-
align-items: center;
|
|
384
|
-
justify-content: space-between;
|
|
385
|
-
padding: 14px 16px;
|
|
386
|
-
border-bottom: 1px solid var(--line);
|
|
387
|
-
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
.brand {
|
|
391
|
-
display: flex;
|
|
392
|
-
align-items: center;
|
|
393
|
-
gap: 10px;
|
|
394
|
-
font-weight: 800;
|
|
395
|
-
letter-spacing: .08em;
|
|
396
|
-
text-transform: uppercase;
|
|
397
|
-
font-size: 12px;
|
|
398
|
-
color: var(--muted);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.spark {
|
|
402
|
-
width: 10px;
|
|
403
|
-
height: 10px;
|
|
404
|
-
border-radius: 4px;
|
|
405
|
-
background: linear-gradient(135deg, var(--accent), var(--primary));
|
|
406
|
-
box-shadow: 0 0 0 6px rgba(249,115,22,.12);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
.actions {
|
|
410
|
-
display: flex;
|
|
411
|
-
align-items: center;
|
|
412
|
-
gap: 10px;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.chip {
|
|
416
|
-
font-family: var(--mono);
|
|
417
|
-
font-size: 12px;
|
|
418
|
-
padding: 7px 10px;
|
|
419
|
-
border-radius: 999px;
|
|
420
|
-
border: 1px solid var(--line);
|
|
421
|
-
background: rgba(255,255,255,.55);
|
|
422
|
-
color: var(--faint);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
[data-theme="dark"] .chip { background: rgba(0,0,0,.20); }
|
|
426
|
-
|
|
427
|
-
.toggle {
|
|
428
|
-
border: 1px solid var(--line);
|
|
429
|
-
border-radius: 999px;
|
|
430
|
-
background: var(--paper2);
|
|
431
|
-
padding: 7px 10px;
|
|
432
|
-
cursor: pointer;
|
|
433
|
-
color: var(--muted);
|
|
434
|
-
font-weight: 700;
|
|
435
|
-
font-size: 12px;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
.section {
|
|
439
|
-
padding: 16px;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
h1 {
|
|
443
|
-
margin: 0;
|
|
444
|
-
font-size: 22px;
|
|
445
|
-
letter-spacing: -.02em;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.subtitle {
|
|
449
|
-
margin-top: 6px;
|
|
450
|
-
color: var(--muted);
|
|
451
|
-
font-size: 13px;
|
|
452
|
-
line-height: 1.4;
|
|
453
|
-
max-width: 860px;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.cards {
|
|
457
|
-
display: grid;
|
|
458
|
-
grid-template-columns: repeat(4, 1fr);
|
|
459
|
-
gap: 12px;
|
|
460
|
-
margin-top: 14px;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
@media (max-width: 1100px) { .cards { grid-template-columns: repeat(2, 1fr);} }
|
|
464
|
-
@media (max-width: 620px) { .cards { grid-template-columns: 1fr;} }
|
|
465
|
-
|
|
466
|
-
.card {
|
|
467
|
-
border: 1px solid var(--line);
|
|
468
|
-
background: var(--paper2);
|
|
469
|
-
border-radius: 14px;
|
|
470
|
-
padding: 12px;
|
|
471
|
-
display: grid;
|
|
472
|
-
gap: 6px;
|
|
473
|
-
cursor: pointer;
|
|
474
|
-
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
475
|
-
box-shadow: 0 1px 0 rgba(16,24,40,.04);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
.card:hover {
|
|
479
|
-
transform: translateY(-1px);
|
|
480
|
-
border-color: var(--line2);
|
|
481
|
-
box-shadow: 0 10px 22px rgba(16,24,40,.10);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.icon {
|
|
485
|
-
width: 34px;
|
|
486
|
-
height: 34px;
|
|
487
|
-
border-radius: 12px;
|
|
488
|
-
display: grid;
|
|
489
|
-
place-items: center;
|
|
490
|
-
background: var(--chip);
|
|
491
|
-
border: 1px solid var(--line);
|
|
492
|
-
color: var(--primary);
|
|
493
|
-
font-weight: 900;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.icon.orange { background: var(--chip2); color: var(--accent); }
|
|
497
|
-
|
|
498
|
-
.title {
|
|
499
|
-
font-weight: 800;
|
|
500
|
-
font-size: 13px;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
.desc {
|
|
504
|
-
color: var(--muted);
|
|
505
|
-
font-size: 12px;
|
|
506
|
-
line-height: 1.35;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
.grid2 {
|
|
510
|
-
display: grid;
|
|
511
|
-
grid-template-columns: 1fr 1fr;
|
|
512
|
-
gap: 14px;
|
|
513
|
-
margin-top: 14px;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
@media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
|
|
517
|
-
|
|
518
|
-
.panel {
|
|
519
|
-
border: 1px solid var(--line);
|
|
520
|
-
background: var(--paper);
|
|
521
|
-
border-radius: 14px;
|
|
522
|
-
overflow: hidden;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.panelHead {
|
|
526
|
-
display: flex;
|
|
527
|
-
align-items: center;
|
|
528
|
-
justify-content: space-between;
|
|
529
|
-
padding: 12px 12px;
|
|
530
|
-
border-bottom: 1px solid var(--line);
|
|
531
|
-
background: linear-gradient(180deg, var(--paper2), var(--paper));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
.panelHead b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
535
|
-
|
|
536
|
-
.panelBody { padding: 12px; }
|
|
537
|
-
|
|
538
|
-
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
|
539
|
-
|
|
540
|
-
input {
|
|
541
|
-
width: 100%;
|
|
542
|
-
padding: 11px 12px;
|
|
543
|
-
border-radius: 12px;
|
|
544
|
-
border: 1px solid var(--line);
|
|
545
|
-
background: rgba(255,255,255,.72);
|
|
546
|
-
color: var(--text);
|
|
547
|
-
outline: none;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
[data-theme="dark"] input { background: rgba(0,0,0,.16); }
|
|
551
|
-
|
|
552
|
-
input:focus { box-shadow: var(--ring); border-color: rgba(37,99,235,.35); }
|
|
553
|
-
|
|
554
|
-
.row {
|
|
555
|
-
display: grid;
|
|
556
|
-
grid-template-columns: 1fr auto;
|
|
557
|
-
gap: 10px;
|
|
558
|
-
align-items: center;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
.btn {
|
|
562
|
-
border: 1px solid var(--line);
|
|
563
|
-
border-radius: 12px;
|
|
564
|
-
background: var(--paper2);
|
|
565
|
-
color: var(--text);
|
|
566
|
-
padding: 10px 12px;
|
|
567
|
-
cursor: pointer;
|
|
568
|
-
font-weight: 800;
|
|
569
|
-
font-size: 12px;
|
|
570
|
-
transition: transform .10s ease, border-color .16s ease, box-shadow .16s ease;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
.btn:hover { transform: translateY(-1px); border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
|
|
574
|
-
.btn:active { transform: translateY(0); }
|
|
575
|
-
|
|
576
|
-
.btn.primary {
|
|
577
|
-
background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(14,165,233,.12));
|
|
578
|
-
border-color: rgba(37,99,235,.22);
|
|
579
|
-
color: var(--text);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
.btn.orange {
|
|
583
|
-
background: linear-gradient(135deg, rgba(249,115,22,.16), rgba(37,99,235,.08));
|
|
584
|
-
border-color: rgba(249,115,22,.24);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
.btn.danger {
|
|
588
|
-
background: rgba(225,29,72,.10);
|
|
589
|
-
border-color: rgba(225,29,72,.28);
|
|
590
|
-
color: var(--text);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
.btn.small { padding: 9px 10px; font-weight: 800; }
|
|
594
|
-
|
|
595
|
-
.stack { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
596
|
-
|
|
597
|
-
.help {
|
|
598
|
-
margin-top: 8px;
|
|
599
|
-
color: var(--faint);
|
|
600
|
-
font-size: 12px;
|
|
601
|
-
line-height: 1.35;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
.log {
|
|
605
|
-
border: 1px solid var(--line);
|
|
606
|
-
background: rgba(255,255,255,.65);
|
|
607
|
-
border-radius: 14px;
|
|
608
|
-
padding: 12px;
|
|
609
|
-
font-family: var(--mono);
|
|
610
|
-
font-size: 12px;
|
|
611
|
-
line-height: 1.35;
|
|
612
|
-
white-space: pre-wrap;
|
|
613
|
-
max-height: 420px;
|
|
614
|
-
overflow: auto;
|
|
615
|
-
color: rgba(15,23,42,.92);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
[data-theme="dark"] .log { background: rgba(0,0,0,.20); color: rgba(233,240,255,.84); }
|
|
619
|
-
|
|
620
|
-
.statusRow { display:flex; align-items:center; justify-content: space-between; gap: 10px; }
|
|
621
|
-
|
|
622
|
-
.pill {
|
|
623
|
-
display: inline-flex;
|
|
624
|
-
align-items: center;
|
|
625
|
-
gap: 8px;
|
|
626
|
-
padding: 7px 10px;
|
|
627
|
-
border-radius: 999px;
|
|
628
|
-
border: 1px solid var(--line);
|
|
629
|
-
background: rgba(255,255,255,.55);
|
|
630
|
-
font-size: 12px;
|
|
631
|
-
color: var(--muted);
|
|
632
|
-
font-family: var(--mono);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
[data-theme="dark"] .pill { background: rgba(0,0,0,.18); }
|
|
636
|
-
|
|
637
|
-
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 0 5px rgba(245,158,11,.15); }
|
|
638
|
-
.dot.ok { background: var(--ok); box-shadow: 0 0 0 5px rgba(22,163,74,.12); }
|
|
639
|
-
.dot.err { background: var(--danger); box-shadow: 0 0 0 5px rgba(225,29,72,.14); }
|
|
640
|
-
|
|
641
|
-
.small {
|
|
642
|
-
font-size: 12px;
|
|
643
|
-
color: var(--faint);
|
|
644
|
-
font-family: var(--mono);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.sidebar h3 {
|
|
648
|
-
margin: 0;
|
|
649
|
-
font-size: 12px;
|
|
650
|
-
letter-spacing: .10em;
|
|
651
|
-
text-transform: uppercase;
|
|
652
|
-
color: var(--muted);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
.sideBlock { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line); }
|
|
656
|
-
|
|
657
|
-
.sidePath {
|
|
658
|
-
margin-top: 8px;
|
|
659
|
-
border: 1px solid var(--line);
|
|
660
|
-
background: var(--paper2);
|
|
661
|
-
border-radius: 12px;
|
|
662
|
-
padding: 10px;
|
|
663
|
-
font-family: var(--mono);
|
|
664
|
-
font-size: 12px;
|
|
665
|
-
color: var(--muted);
|
|
666
|
-
word-break: break-word;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
.sideBtn { width: 100%; margin-top: 8px; }
|
|
670
|
-
|
|
671
|
-
.rep {
|
|
672
|
-
width: 100%;
|
|
673
|
-
text-align: left;
|
|
674
|
-
border: 1px solid var(--line);
|
|
675
|
-
border-radius: 12px;
|
|
676
|
-
background: var(--paper2);
|
|
677
|
-
padding: 10px 12px;
|
|
678
|
-
cursor: pointer;
|
|
679
|
-
font-family: var(--mono);
|
|
680
|
-
font-size: 12px;
|
|
681
|
-
color: var(--muted);
|
|
682
|
-
}
|
|
683
|
-
.rep:hover { border-color: var(--line2); box-shadow: 0 10px 22px rgba(16,24,40,.10); }
|
|
684
|
-
.repActive { border-color: rgba(59,130,246,.55); box-shadow: 0 0 0 4px rgba(59,130,246,.12); }
|
|
685
|
-
|
|
686
|
-
.md-h1{ font-size: 20px; margin: 10px 0 6px; }
|
|
687
|
-
.md-h2{ font-size: 16px; margin: 10px 0 6px; }
|
|
688
|
-
.md-h3{ font-size: 14px; margin: 10px 0 6px; }
|
|
689
|
-
.md-p{ margin: 6px 0; color: var(--muted); line-height: 1.5; }
|
|
690
|
-
.md-ul{ margin: 6px 0 6px 18px; color: var(--muted); }
|
|
691
|
-
.md-inline{ font-family: var(--mono); font-size: 12px; padding: 2px 6px; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,.55); }
|
|
692
|
-
[data-theme="dark"] .md-inline{ background: rgba(0,0,0,.18); }
|
|
693
|
-
.md-code{ background: rgba(0,0,0,.05); border: 1px solid var(--line); border-radius: 14px; padding: 12px; overflow:auto; }
|
|
694
|
-
[data-theme="dark"] .md-code{ background: rgba(0,0,0,.22); }
|
|
695
|
-
.md-sp{ height: 10px; }
|
|
696
|
-
|
|
697
|
-
.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); }
|
|
698
|
-
[data-theme="dark"] .k { background: rgba(0,0,0,.18); }
|
|
699
|
-
|
|
700
|
-
</style>
|
|
338
|
+
<link rel="stylesheet" href="/app.css" />
|
|
701
339
|
</head>
|
|
702
340
|
<body>
|
|
703
341
|
<div class="app">
|
|
704
342
|
<div class="frame">
|
|
343
|
+
<div class="shell">
|
|
705
344
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
345
|
+
<aside class="sidebar">
|
|
346
|
+
<div class="sideHeader">
|
|
347
|
+
<div class="logo">FREYA</div>
|
|
348
|
+
<div class="statusPill"><span class="dot" id="dot"></span><span id="pill">idle</span></div>
|
|
349
|
+
</div>
|
|
711
350
|
|
|
712
|
-
<div class="sideBlock">
|
|
713
|
-
<h3>Workspace</h3>
|
|
714
351
|
<div class="sidePath" id="sidePath">./freya</div>
|
|
715
|
-
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
<div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
|
|
726
|
-
<div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
|
|
727
|
-
</div>
|
|
728
|
-
</aside>
|
|
729
|
-
|
|
730
|
-
<main class="main">
|
|
731
|
-
<div class="topbar">
|
|
732
|
-
<div class="brand"><span class="spark"></span> Local-first status assistant</div>
|
|
733
|
-
<div class="actions">
|
|
734
|
-
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
735
|
-
<button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
|
|
352
|
+
|
|
353
|
+
<div class="sideGroup">
|
|
354
|
+
<div class="sideTitle">Workspace</div>
|
|
355
|
+
<button class="btn sideBtn" onclick="pickDir()">Select workspace…</button>
|
|
356
|
+
<button class="btn primary sideBtn" onclick="doInit()">Init workspace</button>
|
|
357
|
+
<button class="btn sideBtn" onclick="doUpdate()">Update (preserve data/logs)</button>
|
|
358
|
+
<button class="btn sideBtn" onclick="doHealth()">Health</button>
|
|
359
|
+
<button class="btn sideBtn" onclick="doMigrate()">Migrate</button>
|
|
360
|
+
<div style="height:10px"></div>
|
|
361
|
+
<div class="help">Dica: se você já tem uma workspace antiga, use Update. Por padrão, data/logs não são sobrescritos.</div>
|
|
736
362
|
</div>
|
|
737
|
-
</div>
|
|
738
363
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
364
|
+
<div class="sideGroup">
|
|
365
|
+
<div class="sideTitle">Atalhos</div>
|
|
366
|
+
<div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
|
|
367
|
+
<div style="height:8px"></div>
|
|
368
|
+
<div class="help"><span class="k">--port</span> muda a porta (default 3872).</div>
|
|
369
|
+
</div>
|
|
742
370
|
|
|
743
|
-
<div class="
|
|
744
|
-
<div class="
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<
|
|
751
|
-
<div class="title">SM weekly</div>
|
|
752
|
-
<div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
|
|
753
|
-
</div>
|
|
754
|
-
<div class="card" onclick="runReport('blockers')">
|
|
755
|
-
<div class="icon orange">B</div>
|
|
756
|
-
<div class="title">Blockers</div>
|
|
757
|
-
<div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
|
|
371
|
+
<div class="sideGroup">
|
|
372
|
+
<div class="sideTitle">Daily Input</div>
|
|
373
|
+
<textarea id="inboxText" rows="6" placeholder="Cole aqui updates do dia (status, blockers, decisões, ideias)…" style="width:100%; padding:10px 12px; border-radius:12px; border:1px solid var(--line); background: rgba(255,255,255,.72); color: var(--text); outline:none; resize: vertical;"></textarea>
|
|
374
|
+
<div style="height:10px"></div>
|
|
375
|
+
<div class="stack">
|
|
376
|
+
<button class="btn sideBtn" onclick="saveInbox()">Save to Daily Log</button>
|
|
377
|
+
<button class="btn primary sideBtn" onclick="saveAndPlan()">Save + Process (Agents)</button>
|
|
378
|
+
<button class="btn sideBtn" onclick="runSuggestedReports()">Run suggested reports</button>
|
|
758
379
|
</div>
|
|
759
|
-
<div class="
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
380
|
+
<div class="help">Save+Process gera um plano (draft). Apply plan cria tasks/blockers. Run suggested reports executa os reports recomendados (daily/status/sm-weekly/blockers).</div>
|
|
381
|
+
</div>
|
|
382
|
+
</aside>
|
|
383
|
+
|
|
384
|
+
<main class="main">
|
|
385
|
+
<div class="topbar">
|
|
386
|
+
<div class="brand"><span class="spark"></span> Local-first status assistant</div>
|
|
387
|
+
<div class="actions">
|
|
388
|
+
<span class="chip" id="chipPort">127.0.0.1:3872</span>
|
|
389
|
+
<button class="toggle" id="themeToggle" onclick="toggleTheme()">Theme</button>
|
|
763
390
|
</div>
|
|
764
391
|
</div>
|
|
765
392
|
|
|
766
|
-
<div class="
|
|
767
|
-
<
|
|
768
|
-
|
|
769
|
-
<div class="panelBody">
|
|
770
|
-
<label>Workspace dir</label>
|
|
771
|
-
<div class="row">
|
|
772
|
-
<input id="dir" placeholder="./freya" />
|
|
773
|
-
<button class="btn small" onclick="pickDir()">Browse</button>
|
|
774
|
-
</div>
|
|
775
|
-
<div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
|
|
776
|
-
|
|
777
|
-
<div style="height:12px"></div>
|
|
778
|
-
|
|
779
|
-
<label>Discord webhook URL</label>
|
|
780
|
-
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
781
|
-
<div style="height:10px"></div>
|
|
782
|
-
|
|
783
|
-
<label>Teams webhook URL</label>
|
|
784
|
-
<input id="teams" placeholder="https://..." />
|
|
785
|
-
<div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
|
|
393
|
+
<div class="section">
|
|
394
|
+
<h1>Morning, how can I help?</h1>
|
|
395
|
+
<div class="subtitle">Selecione uma workspace e gere relatórios (Executive / SM / Blockers / Daily). Você pode publicar no Discord/Teams com 1 clique.</div>
|
|
786
396
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
<div
|
|
795
|
-
|
|
796
|
-
<div class="
|
|
397
|
+
<div class="cards">
|
|
398
|
+
<div class="card" onclick="runReport('status')">
|
|
399
|
+
<div class="icon">E</div>
|
|
400
|
+
<div class="title">Executive report</div>
|
|
401
|
+
<div class="desc">Status pronto para stakeholders (entregas, projetos, blockers).</div>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="card" onclick="runReport('sm-weekly')">
|
|
404
|
+
<div class="icon">S</div>
|
|
405
|
+
<div class="title">SM weekly</div>
|
|
406
|
+
<div class="desc">Resumo, wins, riscos e foco da próxima semana.</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="card" onclick="runReport('blockers')">
|
|
409
|
+
<div class="icon orange">B</div>
|
|
410
|
+
<div class="title">Blockers</div>
|
|
411
|
+
<div class="desc">Lista priorizada por severidade + idade (pra destravar rápido).</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="card" onclick="runReport('daily')">
|
|
414
|
+
<div class="icon">D</div>
|
|
415
|
+
<div class="title">Daily</div>
|
|
416
|
+
<div class="desc">Ontem / Hoje / Bloqueios — pronto pra standup.</div>
|
|
797
417
|
</div>
|
|
798
418
|
</div>
|
|
799
419
|
|
|
800
|
-
<div class="
|
|
801
|
-
<div class="
|
|
802
|
-
<b>
|
|
803
|
-
<div class="
|
|
804
|
-
<
|
|
420
|
+
<div class="grid2">
|
|
421
|
+
<div class="panel">
|
|
422
|
+
<div class="panelHead"><b>Workspace & publish settings</b><span class="small" id="last"></span></div>
|
|
423
|
+
<div class="panelBody">
|
|
424
|
+
<label>Workspace dir</label>
|
|
425
|
+
<div class="row">
|
|
426
|
+
<input id="dir" placeholder="./freya" />
|
|
427
|
+
<button class="btn small" onclick="pickDir()">Browse</button>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="help">Escolha a pasta que contém <code>data/</code>, <code>logs/</code> e <code>scripts/</code>.</div>
|
|
430
|
+
|
|
431
|
+
<div style="height:12px"></div>
|
|
432
|
+
|
|
433
|
+
<label>Discord webhook URL</label>
|
|
434
|
+
<input id="discord" placeholder="https://discord.com/api/webhooks/..." />
|
|
435
|
+
<div style="height:10px"></div>
|
|
436
|
+
|
|
437
|
+
<label>Teams webhook URL</label>
|
|
438
|
+
<input id="teams" placeholder="https://..." />
|
|
439
|
+
<div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
|
|
440
|
+
|
|
441
|
+
<div style="height:10px"></div>
|
|
442
|
+
<div class="stack">
|
|
443
|
+
<button class="btn" onclick="saveSettings()">Save settings</button>
|
|
444
|
+
<button class="btn" onclick="publish('discord')">Publish selected → Discord</button>
|
|
445
|
+
<button class="btn" onclick="publish('teams')">Publish selected → Teams</button>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div style="height:14px"></div>
|
|
449
|
+
|
|
450
|
+
<div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
|
|
805
451
|
</div>
|
|
806
452
|
</div>
|
|
807
|
-
<div class="panelBody">
|
|
808
|
-
<input id="reportsFilter" placeholder="filter (ex: daily, executive, 2026-01-29)" style="width:100%; margin-bottom:10px" oninput="renderReportsList()" />
|
|
809
|
-
<div id="reportsList" style="display:grid; gap:8px"></div>
|
|
810
|
-
<div class="help">Últimos relatórios em <code>docs/reports</code>. Clique para abrir preview.</div>
|
|
811
|
-
</div>
|
|
812
|
-
</div>
|
|
813
453
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
454
|
+
<div class="panel">
|
|
455
|
+
<div class="panelHead">
|
|
456
|
+
<b>Reports</b>
|
|
457
|
+
<div class="stack">
|
|
458
|
+
<button class="btn small" onclick="refreshReports()">Refresh</button>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="panelBody">
|
|
462
|
+
<input id="reportsFilter" placeholder="filter (ex: daily, executive, 2026-01-29)" style="width:100%; margin-bottom:10px" oninput="renderReportsList()" />
|
|
463
|
+
<div id="reportsList" style="display:grid; gap:8px"></div>
|
|
464
|
+
<div class="help">Últimos relatórios em <code>docs/reports</code>. Clique para abrir preview.</div>
|
|
820
465
|
</div>
|
|
821
466
|
</div>
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
<div class="
|
|
467
|
+
|
|
468
|
+
<div class="panel">
|
|
469
|
+
<div class="panelHead">
|
|
470
|
+
<b>Preview</b>
|
|
471
|
+
<div class="stack">
|
|
472
|
+
<button class="btn small" onclick="copyOut()">Copy</button>
|
|
473
|
+
<button class="btn small" onclick="applyPlan()">Apply plan</button>
|
|
474
|
+
<button class="btn small" onclick="copyPath()">Copy path</button>
|
|
475
|
+
<button class="btn small" onclick="openSelected()">Open file</button>
|
|
476
|
+
<button class="btn small" onclick="downloadSelected()">Download .md</button>
|
|
477
|
+
<button class="btn small" onclick="clearOut()">Clear</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="panelBody">
|
|
481
|
+
<div id="reportPreview" class="log md" style="font-family: var(--sans);"></div>
|
|
482
|
+
<div class="help">O preview renderiza Markdown básico (headers, listas, code). O botão Copy copia o conteúdo completo.</div>
|
|
483
|
+
</div>
|
|
825
484
|
</div>
|
|
826
485
|
</div>
|
|
827
|
-
|
|
828
486
|
</div>
|
|
829
|
-
</
|
|
830
|
-
|
|
831
|
-
</main>
|
|
487
|
+
</main>
|
|
832
488
|
|
|
489
|
+
</div>
|
|
833
490
|
</div>
|
|
834
491
|
</div>
|
|
835
492
|
|
|
836
|
-
<script>
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
function applyTheme(theme) {
|
|
842
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
843
|
-
localStorage.setItem('freya.theme', theme);
|
|
844
|
-
$('themeToggle').textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function toggleTheme() {
|
|
848
|
-
const t = localStorage.getItem('freya.theme') || 'light';
|
|
849
|
-
applyTheme(t === 'dark' ? 'light' : 'dark');
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function setPill(kind, text) {
|
|
853
|
-
const dot = $('dot');
|
|
854
|
-
dot.classList.remove('ok','err');
|
|
855
|
-
if (kind === 'ok') dot.classList.add('ok');
|
|
856
|
-
if (kind === 'err') dot.classList.add('err');
|
|
857
|
-
$('pill').textContent = text;
|
|
858
|
-
$('status') && ($('status').textContent = text);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function escapeHtml(str) {
|
|
862
|
-
return String(str)
|
|
863
|
-
.replace(/&/g, '&')
|
|
864
|
-
.replace(/</g, '<')
|
|
865
|
-
.replace(/>/g, '>')
|
|
866
|
-
.replace(/\"/g, '"')
|
|
867
|
-
.replace(/'/g, ''');
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function renderMarkdown(md) {
|
|
871
|
-
const lines = String(md || '').split(/\\r?\\n/);
|
|
872
|
-
let html = '';
|
|
873
|
-
let inCode = false;
|
|
874
|
-
let inList = false;
|
|
875
|
-
|
|
876
|
-
const NL = String.fromCharCode(10);
|
|
877
|
-
const BT = String.fromCharCode(96); // backtick
|
|
878
|
-
const FENCE = BT + BT + BT;
|
|
879
|
-
const inlineCodeRe = /\x60([^\x60]+)\x60/g;
|
|
880
|
-
|
|
881
|
-
const closeList = () => {
|
|
882
|
-
if (inList) { html += '</ul>'; inList = false; }
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
for (const line of lines) {
|
|
886
|
-
if (line.trim().startsWith(FENCE)) {
|
|
887
|
-
if (!inCode) {
|
|
888
|
-
closeList();
|
|
889
|
-
inCode = true;
|
|
890
|
-
html += '<pre class="md-code"><code>';
|
|
891
|
-
} else {
|
|
892
|
-
inCode = false;
|
|
893
|
-
html += '</code></pre>';
|
|
894
|
-
}
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (inCode) {
|
|
899
|
-
html += escapeHtml(line) + NL;
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const h = line.match(/^(#{1,3})[ \t]+(.*)$/);
|
|
904
|
-
if (h) {
|
|
905
|
-
closeList();
|
|
906
|
-
const lvl = h[1].length;
|
|
907
|
-
html += '<h' + lvl + ' class="md-h' + lvl + '">' + escapeHtml(h[2]) + '</h' + lvl + '>';
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const li = line.match(/^[ \t]*[-*][ \t]+(.*)$/);
|
|
912
|
-
if (li) {
|
|
913
|
-
if (!inList) { html += '<ul class="md-ul">'; inList = true; }
|
|
914
|
-
const content = escapeHtml(li[1]).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
915
|
-
html += '<li>' + content + '</li>';
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (line.trim() === '') {
|
|
920
|
-
closeList();
|
|
921
|
-
html += '<div class="md-sp"></div>';
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
closeList();
|
|
926
|
-
const p = escapeHtml(line).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
|
|
927
|
-
html += '<p class="md-p">' + p + '</p>';
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
closeList();
|
|
931
|
-
if (inCode) html += '</code></pre>';
|
|
932
|
-
return html;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function setOut(text) {
|
|
936
|
-
state.lastText = text || '';
|
|
937
|
-
const el = $('reportPreview');
|
|
938
|
-
if (el) el.innerHTML = renderMarkdown(state.lastText);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function clearOut() {
|
|
942
|
-
state.lastText = '';
|
|
943
|
-
const el = $('reportPreview');
|
|
944
|
-
if (el) el.innerHTML = '';
|
|
945
|
-
setPill('ok', 'idle');
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
async function copyOut() {
|
|
949
|
-
try {
|
|
950
|
-
await navigator.clipboard.writeText(state.lastText || '');
|
|
951
|
-
setPill('ok','copied');
|
|
952
|
-
setTimeout(() => setPill('ok','idle'), 800);
|
|
953
|
-
} catch (e) {
|
|
954
|
-
setPill('err','copy failed');
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function setLast(p) {
|
|
959
|
-
state.lastReportPath = p;
|
|
960
|
-
$('last').textContent = p ? ('Last report: ' + p) : '';
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function saveLocal() {
|
|
964
|
-
localStorage.setItem('freya.dir', $('dir').value);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function loadLocal() {
|
|
968
|
-
$('dir').value = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__') ? window.__FREYA_DEFAULT_DIR : (localStorage.getItem('freya.dir') || './freya');
|
|
969
|
-
$('sidePath').textContent = $('dir').value || './freya';
|
|
970
|
-
// Always persist the current run's default dir to avoid stale values
|
|
971
|
-
localStorage.setItem('freya.dir', $('dir').value || './freya');
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
async function api(p, body) {
|
|
975
|
-
const res = await fetch(p, {
|
|
976
|
-
method: body ? 'POST' : 'GET',
|
|
977
|
-
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
978
|
-
body: body ? JSON.stringify(body) : undefined
|
|
979
|
-
});
|
|
980
|
-
const json = await res.json();
|
|
981
|
-
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
982
|
-
return json;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function dirOrDefault() {
|
|
986
|
-
const d = $('dir').value.trim();
|
|
987
|
-
return d || './freya';
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function fmtWhen(ms) {
|
|
991
|
-
try {
|
|
992
|
-
const d = new Date(ms);
|
|
993
|
-
const yy = String(d.getFullYear());
|
|
994
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
995
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
996
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
997
|
-
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
998
|
-
return yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mi;
|
|
999
|
-
} catch {
|
|
1000
|
-
return '';
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
async function selectReport(item) {
|
|
1005
|
-
const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
|
|
1006
|
-
state.selectedReport = item;
|
|
1007
|
-
setLast(item.name);
|
|
1008
|
-
setOut(rr.text || '');
|
|
1009
|
-
renderReportsList();
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function renderReportsList() {
|
|
1013
|
-
const list = $('reportsList');
|
|
1014
|
-
if (!list) return;
|
|
1015
|
-
const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
|
|
1016
|
-
const filtered = (state.reports || []).filter((it) => {
|
|
1017
|
-
if (!q) return true;
|
|
1018
|
-
return (it.name + ' ' + it.kind).toLowerCase().includes(q);
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
list.innerHTML = '';
|
|
1022
|
-
for (const item of filtered) {
|
|
1023
|
-
const btn = document.createElement('button');
|
|
1024
|
-
btn.className = 'rep' + (state.selectedReport && state.selectedReport.relPath === item.relPath ? ' repActive' : '');
|
|
1025
|
-
btn.type = 'button';
|
|
1026
|
-
const meta = fmtWhen(item.mtimeMs);
|
|
1027
|
-
btn.innerHTML =
|
|
1028
|
-
'<div style="display:flex; gap:10px; align-items:center; justify-content:space-between">'
|
|
1029
|
-
+ '<div style="min-width:0">'
|
|
1030
|
-
+ '<div><span style="font-weight:800">' + escapeHtml(item.kind) + '</span> <span style="opacity:.7">—</span> ' + escapeHtml(item.name) + '</div>'
|
|
1031
|
-
+ '<div style="opacity:.65; font-size:11px; margin-top:4px">' + escapeHtml(item.relPath) + '</div>'
|
|
1032
|
-
+ '</div>'
|
|
1033
|
-
+ '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(meta) + '</div>'
|
|
1034
|
-
+ '</div>';
|
|
1035
|
-
|
|
1036
|
-
btn.onclick = async () => {
|
|
1037
|
-
try {
|
|
1038
|
-
await selectReport(item);
|
|
1039
|
-
} catch (e) {
|
|
1040
|
-
setPill('err', 'open failed');
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
list.appendChild(btn);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async function refreshReports() {
|
|
1048
|
-
try {
|
|
1049
|
-
const r = await api('/api/reports/list', { dir: dirOrDefault() });
|
|
1050
|
-
state.reports = (r.reports || []).slice(0, 50);
|
|
1051
|
-
renderReportsList();
|
|
1052
|
-
|
|
1053
|
-
// Auto-select latest if nothing selected yet
|
|
1054
|
-
if (!state.selectedReport && state.reports && state.reports[0]) {
|
|
1055
|
-
await selectReport(state.reports[0]);
|
|
1056
|
-
}
|
|
1057
|
-
} catch (e) {
|
|
1058
|
-
// ignore
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
async function pickDir() {
|
|
1063
|
-
try {
|
|
1064
|
-
setPill('run','picker…');
|
|
1065
|
-
const r = await api('/api/pick-dir', {});
|
|
1066
|
-
if (r && r.dir) {
|
|
1067
|
-
$('dir').value = r.dir;
|
|
1068
|
-
$('sidePath').textContent = r.dir;
|
|
1069
|
-
}
|
|
1070
|
-
saveLocal();
|
|
1071
|
-
setPill('ok','ready');
|
|
1072
|
-
} catch (e) {
|
|
1073
|
-
setPill('err','picker failed');
|
|
1074
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
async function doInit() {
|
|
1079
|
-
try {
|
|
1080
|
-
saveLocal();
|
|
1081
|
-
$('sidePath').textContent = dirOrDefault();
|
|
1082
|
-
setPill('run','init…');
|
|
1083
|
-
setOut('');
|
|
1084
|
-
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
1085
|
-
setOut(r.output);
|
|
1086
|
-
setLast(null);
|
|
1087
|
-
await refreshReports();
|
|
1088
|
-
setPill('ok','init ok');
|
|
1089
|
-
} catch (e) {
|
|
1090
|
-
setPill('err','init failed');
|
|
1091
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async function doUpdate() {
|
|
1096
|
-
try {
|
|
1097
|
-
saveLocal();
|
|
1098
|
-
$('sidePath').textContent = dirOrDefault();
|
|
1099
|
-
setPill('run','update…');
|
|
1100
|
-
setOut('');
|
|
1101
|
-
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
1102
|
-
setOut(r.output);
|
|
1103
|
-
setLast(null);
|
|
1104
|
-
await refreshReports();
|
|
1105
|
-
setPill('ok','update ok');
|
|
1106
|
-
} catch (e) {
|
|
1107
|
-
setPill('err','update failed');
|
|
1108
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async function doHealth() {
|
|
1113
|
-
try {
|
|
1114
|
-
saveLocal();
|
|
1115
|
-
$('sidePath').textContent = dirOrDefault();
|
|
1116
|
-
setPill('run','health…');
|
|
1117
|
-
setOut('');
|
|
1118
|
-
const r = await api('/api/health', { dir: dirOrDefault() });
|
|
1119
|
-
setOut(r.output);
|
|
1120
|
-
setLast(null);
|
|
1121
|
-
setPill('ok','health ok');
|
|
1122
|
-
} catch (e) {
|
|
1123
|
-
setPill('err','health failed');
|
|
1124
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
async function doMigrate() {
|
|
1129
|
-
try {
|
|
1130
|
-
saveLocal();
|
|
1131
|
-
$('sidePath').textContent = dirOrDefault();
|
|
1132
|
-
setPill('run','migrate…');
|
|
1133
|
-
setOut('');
|
|
1134
|
-
const r = await api('/api/migrate', { dir: dirOrDefault() });
|
|
1135
|
-
setOut(r.output);
|
|
1136
|
-
setLast(null);
|
|
1137
|
-
setPill('ok','migrate ok');
|
|
1138
|
-
} catch (e) {
|
|
1139
|
-
setPill('err','migrate failed');
|
|
1140
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
async function runReport(name) {
|
|
1145
|
-
try {
|
|
1146
|
-
saveLocal();
|
|
1147
|
-
$('sidePath').textContent = dirOrDefault();
|
|
1148
|
-
setPill('run', name + '…');
|
|
1149
|
-
setOut('');
|
|
1150
|
-
const r = await api('/api/report', { dir: dirOrDefault(), script: name });
|
|
1151
|
-
setOut(r.output);
|
|
1152
|
-
setLast(r.reportPath || null);
|
|
1153
|
-
if (r.reportText) state.lastText = r.reportText;
|
|
1154
|
-
await refreshReports();
|
|
1155
|
-
setPill('ok', name + ' ok');
|
|
1156
|
-
} catch (e) {
|
|
1157
|
-
setPill('err', name + ' failed');
|
|
1158
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
async function saveSettings() {
|
|
1163
|
-
try {
|
|
1164
|
-
saveLocal();
|
|
1165
|
-
setPill('run','saving…');
|
|
1166
|
-
await api('/api/settings/save', {
|
|
1167
|
-
dir: dirOrDefault(),
|
|
1168
|
-
settings: {
|
|
1169
|
-
discordWebhookUrl: $('discord').value.trim(),
|
|
1170
|
-
teamsWebhookUrl: $('teams').value.trim()
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
setPill('ok','saved');
|
|
1174
|
-
setTimeout(() => setPill('ok','idle'), 800);
|
|
1175
|
-
} catch (e) {
|
|
1176
|
-
setPill('err','save failed');
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
async function publish(target) {
|
|
1181
|
-
try {
|
|
1182
|
-
saveLocal();
|
|
1183
|
-
if (!state.lastText) throw new Error('Gere um relatório primeiro.');
|
|
1184
|
-
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
1185
|
-
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
1186
|
-
setPill('run','publish…');
|
|
1187
|
-
await api('/api/publish', { webhookUrl, text: state.lastText });
|
|
1188
|
-
setPill('ok','published');
|
|
1189
|
-
} catch (e) {
|
|
1190
|
-
setPill('err','publish failed');
|
|
1191
|
-
setOut(String(e && e.message ? e.message : e));
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Expose handlers for inline onclick="..." attributes
|
|
1196
|
-
window.doInit = doInit;
|
|
1197
|
-
window.doUpdate = doUpdate;
|
|
1198
|
-
window.doHealth = doHealth;
|
|
1199
|
-
window.doMigrate = doMigrate;
|
|
1200
|
-
window.pickDir = pickDir;
|
|
1201
|
-
window.runReport = runReport;
|
|
1202
|
-
window.publish = publish;
|
|
1203
|
-
window.saveSettings = saveSettings;
|
|
1204
|
-
window.refreshReports = refreshReports;
|
|
1205
|
-
window.renderReportsList = renderReportsList;
|
|
1206
|
-
window.copyOut = copyOut;
|
|
1207
|
-
window.clearOut = clearOut;
|
|
1208
|
-
window.toggleTheme = toggleTheme;
|
|
1209
|
-
|
|
1210
|
-
// init
|
|
1211
|
-
applyTheme(localStorage.getItem('freya.theme') || 'light');
|
|
1212
|
-
$('chipPort').textContent = location.host;
|
|
1213
|
-
loadLocal();
|
|
1214
|
-
|
|
1215
|
-
// Load persisted settings from the workspace
|
|
1216
|
-
(async () => {
|
|
1217
|
-
try {
|
|
1218
|
-
const r = await api('/api/defaults', { dir: dirOrDefault() });
|
|
1219
|
-
if (r && r.workspaceDir) {
|
|
1220
|
-
$('dir').value = r.workspaceDir;
|
|
1221
|
-
$('sidePath').textContent = r.workspaceDir;
|
|
1222
|
-
}
|
|
1223
|
-
if (r && r.settings) {
|
|
1224
|
-
$('discord').value = r.settings.discordWebhookUrl || '';
|
|
1225
|
-
$('teams').value = r.settings.teamsWebhookUrl || '';
|
|
1226
|
-
}
|
|
1227
|
-
} catch (e) {
|
|
1228
|
-
// ignore
|
|
1229
|
-
}
|
|
1230
|
-
refreshReports();
|
|
1231
|
-
})();
|
|
1232
|
-
|
|
1233
|
-
setPill('ok','idle');
|
|
1234
|
-
</script>
|
|
493
|
+
<script>
|
|
494
|
+
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
495
|
+
</script>
|
|
496
|
+
<script src="/app.js"></script>
|
|
1235
497
|
</body>
|
|
1236
498
|
</html>`;
|
|
1237
499
|
}
|
|
@@ -1419,6 +681,20 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1419
681
|
return;
|
|
1420
682
|
}
|
|
1421
683
|
|
|
684
|
+
if (req.method === 'GET' && req.url === '/app.css') {
|
|
685
|
+
const css = fs.readFileSync(path.join(__dirname, 'web-ui.css'), 'utf8');
|
|
686
|
+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
687
|
+
res.end(css);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (req.method === 'GET' && req.url === '/app.js') {
|
|
692
|
+
const js = fs.readFileSync(path.join(__dirname, 'web-ui.js'), 'utf8');
|
|
693
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
694
|
+
res.end(js);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
1422
698
|
if (req.url.startsWith('/api/')) {
|
|
1423
699
|
const raw = await readBody(req);
|
|
1424
700
|
const payload = raw ? JSON.parse(raw) : {};
|
|
@@ -1457,6 +733,357 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1457
733
|
return safeJson(res, 200, { relPath: rel, text });
|
|
1458
734
|
}
|
|
1459
735
|
|
|
736
|
+
if (req.url === '/api/reports/resolve') {
|
|
737
|
+
const rel = payload.relPath;
|
|
738
|
+
if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
|
|
739
|
+
const full = path.join(workspaceDir, rel);
|
|
740
|
+
if (!exists(full)) return safeJson(res, 404, { error: 'Report not found' });
|
|
741
|
+
return safeJson(res, 200, { relPath: rel, fullPath: full });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (req.url === '/api/inbox/add') {
|
|
745
|
+
const text = String(payload.text || '').trim();
|
|
746
|
+
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
747
|
+
|
|
748
|
+
const d = isoDate();
|
|
749
|
+
const file = path.join(workspaceDir, 'logs', 'daily', `${d}.md`);
|
|
750
|
+
ensureDir(path.dirname(file));
|
|
751
|
+
|
|
752
|
+
const stamp = new Date();
|
|
753
|
+
const hh = String(stamp.getHours()).padStart(2, '0');
|
|
754
|
+
const mm = String(stamp.getMinutes()).padStart(2, '0');
|
|
755
|
+
|
|
756
|
+
const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}\n`;
|
|
757
|
+
fs.appendFileSync(file, block, 'utf8');
|
|
758
|
+
|
|
759
|
+
return safeJson(res, 200, { ok: true, file: path.relative(workspaceDir, file).replace(/\\/g, '/'), appended: true });
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (req.url === '/api/agents/plan') {
|
|
763
|
+
const text = String(payload.text || '').trim();
|
|
764
|
+
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
765
|
+
|
|
766
|
+
// Build planner prompt from agent rules.
|
|
767
|
+
// Prefer rules inside the selected workspace, but fallback to packaged rules.
|
|
768
|
+
const workspaceRulesBase = path.join(workspaceDir, '.agent', 'rules', 'freya');
|
|
769
|
+
const packagedRulesBase = path.join(__dirname, '..', '.agent', 'rules', 'freya');
|
|
770
|
+
const rulesBase = exists(workspaceRulesBase) ? workspaceRulesBase : packagedRulesBase;
|
|
771
|
+
|
|
772
|
+
const files = [
|
|
773
|
+
path.join(rulesBase, 'freya.mdc'),
|
|
774
|
+
path.join(rulesBase, 'agents', 'master.mdc'),
|
|
775
|
+
path.join(rulesBase, 'agents', 'ingestor.mdc'),
|
|
776
|
+
path.join(rulesBase, 'agents', 'oracle.mdc'),
|
|
777
|
+
path.join(rulesBase, 'agents', 'coach.mdc')
|
|
778
|
+
].filter(exists);
|
|
779
|
+
|
|
780
|
+
const rulesText = files.map((p) => {
|
|
781
|
+
const rel = path.relative(workspaceDir, p).replace(/\\/g, '/');
|
|
782
|
+
return `\n\n---\nFILE: ${rel}\n---\n` + fs.readFileSync(p, 'utf8');
|
|
783
|
+
}).join('');
|
|
784
|
+
|
|
785
|
+
const schema = {
|
|
786
|
+
actions: [
|
|
787
|
+
{ type: 'append_daily_log', text: '<string>' },
|
|
788
|
+
{ type: 'create_task', description: '<string>', priority: 'HIGH|MEDIUM|LOW', category: 'DO_NOW|SCHEDULE|DELEGATE|IGNORE' },
|
|
789
|
+
{ type: 'create_blocker', title: '<string>', severity: 'CRITICAL|HIGH|MEDIUM|LOW', notes: '<string>' },
|
|
790
|
+
{ type: 'suggest_report', name: 'daily|status|sm-weekly|blockers' },
|
|
791
|
+
{ type: 'oracle_query', query: '<string>' }
|
|
792
|
+
]
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
|
|
796
|
+
|
|
797
|
+
// Prefer COPILOT_CMD if provided, otherwise try 'copilot'
|
|
798
|
+
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
799
|
+
|
|
800
|
+
// Best-effort: if Copilot CLI isn't available, return 200 with an explanatory plan
|
|
801
|
+
// so the UI can show actionable next steps instead of hard-failing.
|
|
802
|
+
try {
|
|
803
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir);
|
|
804
|
+
const out = (r.stdout + r.stderr).trim();
|
|
805
|
+
if (r.code !== 0) {
|
|
806
|
+
return safeJson(res, 200, {
|
|
807
|
+
ok: false,
|
|
808
|
+
plan: out || 'Copilot returned non-zero exit code.',
|
|
809
|
+
hint: 'Copilot CLI needs to be installed and authenticated.'
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return safeJson(res, 200, { ok: true, plan: out });
|
|
813
|
+
} catch (e) {
|
|
814
|
+
return safeJson(res, 200, {
|
|
815
|
+
ok: false,
|
|
816
|
+
plan: `Copilot CLI não disponível (cmd: ${cmd}).\n\nPara habilitar:\n- Windows (winget): winget install GitHub.Copilot\n- npm: npm i -g @github/copilot\n\nDepois rode \"copilot\" uma vez e faça /login.`,
|
|
817
|
+
details: e.message || String(e)
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (req.url === '/api/agents/preview') {
|
|
823
|
+
const planRaw = String(payload.plan || '').trim();
|
|
824
|
+
if (!planRaw) return safeJson(res, 400, { error: 'Missing plan' });
|
|
825
|
+
|
|
826
|
+
function extractJson(text) {
|
|
827
|
+
const start = text.indexOf('{');
|
|
828
|
+
const end = text.lastIndexOf('}');
|
|
829
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
830
|
+
return text.slice(start, end + 1);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const jsonText = extractJson(planRaw) || planRaw;
|
|
834
|
+
let plan;
|
|
835
|
+
try {
|
|
836
|
+
plan = JSON.parse(jsonText);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
return safeJson(res, 400, { error: 'Plan is not valid JSON', details: e.message || String(e) });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const actions = Array.isArray(plan.actions) ? plan.actions : [];
|
|
842
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
843
|
+
return safeJson(res, 400, { error: 'Plan has no actions[]' });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Validate + normalize to a safe preview shape
|
|
847
|
+
const validTaskCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
|
|
848
|
+
const validSev = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']);
|
|
849
|
+
|
|
850
|
+
const preview = { tasks: [], blockers: [], reportsSuggested: [], oracleQueries: [], ignored: 0, errors: [] };
|
|
851
|
+
|
|
852
|
+
for (const a of actions) {
|
|
853
|
+
if (!a || typeof a !== 'object') { preview.ignored++; continue; }
|
|
854
|
+
const type = String(a.type || '').trim();
|
|
855
|
+
|
|
856
|
+
if (type === 'create_task') {
|
|
857
|
+
const description = String(a.description || '').trim();
|
|
858
|
+
const category = String(a.category || '').trim();
|
|
859
|
+
const priorityRaw = String(a.priority || '').trim().toLowerCase();
|
|
860
|
+
const priority = (priorityRaw === 'high' || priorityRaw === 'medium' || priorityRaw === 'low') ? priorityRaw : undefined;
|
|
861
|
+
if (!description) { preview.errors.push('Task missing description'); continue; }
|
|
862
|
+
preview.tasks.push({ description, category: validTaskCats.has(category) ? category : 'DO_NOW', priority });
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (type === 'create_blocker') {
|
|
867
|
+
const title = String(a.title || '').trim();
|
|
868
|
+
const notes = String(a.notes || '').trim();
|
|
869
|
+
let severity = String(a.severity || '').trim().toUpperCase();
|
|
870
|
+
if (!validSev.has(severity)) {
|
|
871
|
+
if (severity.includes('CRIT')) severity = 'CRITICAL';
|
|
872
|
+
else if (severity.includes('HIGH')) severity = 'HIGH';
|
|
873
|
+
else if (severity.includes('MED')) severity = 'MEDIUM';
|
|
874
|
+
else if (severity.includes('LOW')) severity = 'LOW';
|
|
875
|
+
else severity = 'MEDIUM';
|
|
876
|
+
}
|
|
877
|
+
if (!title) { preview.errors.push('Blocker missing title'); continue; }
|
|
878
|
+
preview.blockers.push({ title, notes, severity });
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (type === 'suggest_report') {
|
|
883
|
+
const name = String(a.name || '').trim();
|
|
884
|
+
if (name) preview.reportsSuggested.push(name);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (type === 'oracle_query') {
|
|
889
|
+
const query = String(a.query || '').trim();
|
|
890
|
+
if (query) preview.oracleQueries.push(query);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
preview.ignored++;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Dedup suggested reports
|
|
898
|
+
preview.reportsSuggested = Array.from(new Set(preview.reportsSuggested));
|
|
899
|
+
preview.oracleQueries = Array.from(new Set(preview.oracleQueries));
|
|
900
|
+
|
|
901
|
+
return safeJson(res, 200, { ok: true, preview });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (req.url === '/api/agents/apply') {
|
|
905
|
+
const planRaw = String(payload.plan || '').trim();
|
|
906
|
+
if (!planRaw) return safeJson(res, 400, { error: 'Missing plan' });
|
|
907
|
+
|
|
908
|
+
function extractJson(text) {
|
|
909
|
+
const start = text.indexOf('{');
|
|
910
|
+
const end = text.lastIndexOf('}');
|
|
911
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
912
|
+
return text.slice(start, end + 1);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const jsonText = extractJson(planRaw) || planRaw;
|
|
916
|
+
let plan;
|
|
917
|
+
try {
|
|
918
|
+
plan = JSON.parse(jsonText);
|
|
919
|
+
} catch (e) {
|
|
920
|
+
return safeJson(res, 400, { error: 'Plan is not valid JSON', details: e.message || String(e) });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const actions = Array.isArray(plan.actions) ? plan.actions : [];
|
|
924
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
925
|
+
return safeJson(res, 400, { error: 'Plan has no actions[]' });
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
|
|
929
|
+
const blockerFile = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
|
|
930
|
+
|
|
931
|
+
const taskLog = readJsonOrNull(taskFile) || { schemaVersion: 1, tasks: [] };
|
|
932
|
+
if (!Array.isArray(taskLog.tasks)) taskLog.tasks = [];
|
|
933
|
+
if (typeof taskLog.schemaVersion !== 'number') taskLog.schemaVersion = 1;
|
|
934
|
+
|
|
935
|
+
const blockerLog = readJsonOrNull(blockerFile) || { schemaVersion: 1, blockers: [] };
|
|
936
|
+
if (!Array.isArray(blockerLog.blockers)) blockerLog.blockers = [];
|
|
937
|
+
if (typeof blockerLog.schemaVersion !== 'number') blockerLog.schemaVersion = 1;
|
|
938
|
+
|
|
939
|
+
function normalizeTextForKey(t) {
|
|
940
|
+
return String(t || '').toLowerCase().replace(/s+/g, ' ').trim();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function sha1(text) {
|
|
944
|
+
return crypto.createHash('sha1').update(String(text || ''), 'utf8').digest('hex');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function within24h(iso) {
|
|
948
|
+
try {
|
|
949
|
+
const ms = Date.parse(iso);
|
|
950
|
+
if (!Number.isFinite(ms)) return false;
|
|
951
|
+
return (Date.now() - ms) <= 24 * 60 * 60 * 1000;
|
|
952
|
+
} catch {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const existingTaskKeys24h = new Set(
|
|
958
|
+
taskLog.tasks
|
|
959
|
+
.filter((t) => t && within24h(t.createdAt))
|
|
960
|
+
.map((t) => sha1(normalizeTextForKey(t.description)))
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
const existingBlockerKeys24h = new Set(
|
|
964
|
+
blockerLog.blockers
|
|
965
|
+
.filter((b) => b && within24h(b.createdAt))
|
|
966
|
+
.map((b) => sha1(normalizeTextForKey(b.title)))
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
const now = new Date().toISOString();
|
|
970
|
+
const applyMode = String(payload.mode || 'all').trim();
|
|
971
|
+
undefined
|
|
972
|
+
|
|
973
|
+
function makeId(prefix) {
|
|
974
|
+
const rand = Math.random().toString(16).slice(2, 8);
|
|
975
|
+
return `${prefix}-${Date.now()}-${rand}`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function normPriority(p) {
|
|
979
|
+
const v = String(p || '').trim().toLowerCase();
|
|
980
|
+
if (v === 'high') return 'high';
|
|
981
|
+
if (v === 'medium') return 'medium';
|
|
982
|
+
if (v === 'low') return 'low';
|
|
983
|
+
if (v === 'critical') return 'high';
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function normSeverity(s) {
|
|
988
|
+
const v = String(s || '').trim().toUpperCase();
|
|
989
|
+
if (v.includes('CRIT')) return 'CRITICAL';
|
|
990
|
+
if (v.includes('HIGH')) return 'HIGH';
|
|
991
|
+
if (v.includes('MED')) return 'MEDIUM';
|
|
992
|
+
if (v.includes('LOW')) return 'LOW';
|
|
993
|
+
return 'MEDIUM';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const validTaskCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
|
|
997
|
+
|
|
998
|
+
for (const a of actions) {
|
|
999
|
+
if (!a || typeof a !== 'object') continue;
|
|
1000
|
+
const type = String(a.type || '').trim();
|
|
1001
|
+
|
|
1002
|
+
if (type === 'create_task') {
|
|
1003
|
+
if (applyMode !== 'all' && applyMode !== 'tasks') continue;
|
|
1004
|
+
const description = String(a.description || '').trim();
|
|
1005
|
+
if (!description) continue;
|
|
1006
|
+
const key = sha1(normalizeTextForKey(description));
|
|
1007
|
+
if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
|
|
1008
|
+
const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
|
|
1009
|
+
const priority = normPriority(a.priority);
|
|
1010
|
+
const task = {
|
|
1011
|
+
id: makeId('t'),
|
|
1012
|
+
description,
|
|
1013
|
+
category,
|
|
1014
|
+
status: 'PENDING',
|
|
1015
|
+
createdAt: now,
|
|
1016
|
+
};
|
|
1017
|
+
if (priority) task.priority = priority;
|
|
1018
|
+
taskLog.tasks.push(task);
|
|
1019
|
+
applied.tasks++;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (type === 'create_blocker') {
|
|
1024
|
+
if (applyMode !== 'all' && applyMode !== 'blockers') continue;
|
|
1025
|
+
const title = String(a.title || '').trim();
|
|
1026
|
+
const key = sha1(normalizeTextForKey(title));
|
|
1027
|
+
if (existingBlockerKeys24h.has(key)) { applied.blockersSkipped++; continue; }
|
|
1028
|
+
const notes = String(a.notes || '').trim();
|
|
1029
|
+
if (!title) continue;
|
|
1030
|
+
const severity = normSeverity(a.severity);
|
|
1031
|
+
const blocker = {
|
|
1032
|
+
id: makeId('b'),
|
|
1033
|
+
title,
|
|
1034
|
+
description: notes || title,
|
|
1035
|
+
createdAt: now,
|
|
1036
|
+
status: 'OPEN',
|
|
1037
|
+
severity,
|
|
1038
|
+
};
|
|
1039
|
+
blockerLog.blockers.push(blocker);
|
|
1040
|
+
applied.blockers++;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (type === 'suggest_report') {
|
|
1045
|
+
const name = String(a.name || '').trim();
|
|
1046
|
+
if (name) applied.reportsSuggested.push(name);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (type === 'oracle_query') {
|
|
1051
|
+
const query = String(a.query || '').trim();
|
|
1052
|
+
if (query) applied.oracleQueries.push(query);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Persist
|
|
1058
|
+
writeJson(taskFile, taskLog);
|
|
1059
|
+
writeJson(blockerFile, blockerLog);
|
|
1060
|
+
|
|
1061
|
+
return safeJson(res, 200, { ok: true, applied });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (req.url === '/api/reports/open') {
|
|
1065
|
+
const rel = payload.relPath;
|
|
1066
|
+
if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
|
|
1067
|
+
const full = path.join(workspaceDir, rel);
|
|
1068
|
+
if (!exists(full)) return safeJson(res, 404, { error: 'Report not found' });
|
|
1069
|
+
|
|
1070
|
+
// Best-effort: open the file with OS default app
|
|
1071
|
+
try {
|
|
1072
|
+
const platform = process.platform;
|
|
1073
|
+
if (platform === 'win32') {
|
|
1074
|
+
await run('cmd', ['/c', 'start', '', full], workspaceDir);
|
|
1075
|
+
} else if (platform === 'darwin') {
|
|
1076
|
+
await run('open', [full], workspaceDir);
|
|
1077
|
+
} else {
|
|
1078
|
+
await run('xdg-open', [full], workspaceDir);
|
|
1079
|
+
}
|
|
1080
|
+
} catch {
|
|
1081
|
+
// ignore; still return ok
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return safeJson(res, 200, { ok: true, relPath: rel, fullPath: full });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1460
1087
|
if (req.url === '/api/init') {
|
|
1461
1088
|
const pkg = '@cccarv82/freya';
|
|
1462
1089
|
const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', workspaceDir], process.cwd());
|
|
@@ -1513,37 +1140,16 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1513
1140
|
if (req.url === '/api/publish') {
|
|
1514
1141
|
const webhookUrl = payload.webhookUrl;
|
|
1515
1142
|
const text = payload.text;
|
|
1143
|
+
const mode = payload.mode || 'chunks';
|
|
1516
1144
|
if (!webhookUrl) return safeJson(res, 400, { error: 'Missing webhookUrl' });
|
|
1517
1145
|
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
1518
1146
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
method: 'POST',
|
|
1526
|
-
hostname: u.hostname,
|
|
1527
|
-
path: u.pathname + u.search,
|
|
1528
|
-
headers: {
|
|
1529
|
-
'Content-Type': 'application/json',
|
|
1530
|
-
'Content-Length': Buffer.byteLength(body)
|
|
1531
|
-
}
|
|
1532
|
-
};
|
|
1533
|
-
|
|
1534
|
-
const proto = u.protocol === 'https:' ? require('https') : require('http');
|
|
1535
|
-
const req2 = proto.request(options, (r2) => {
|
|
1536
|
-
const chunks = [];
|
|
1537
|
-
r2.on('data', (c) => chunks.push(c));
|
|
1538
|
-
r2.on('end', () => {
|
|
1539
|
-
if (r2.statusCode >= 200 && r2.statusCode < 300) return safeJson(res, 200, { ok: true });
|
|
1540
|
-
return safeJson(res, 400, { error: `Webhook error ${r2.statusCode}: ${Buffer.concat(chunks).toString('utf8')}` });
|
|
1541
|
-
});
|
|
1542
|
-
});
|
|
1543
|
-
req2.on('error', (e) => safeJson(res, 400, { error: e.message }));
|
|
1544
|
-
req2.write(body);
|
|
1545
|
-
req2.end();
|
|
1546
|
-
return;
|
|
1147
|
+
try {
|
|
1148
|
+
const result = await publishRobust(webhookUrl, text, { mode });
|
|
1149
|
+
return safeJson(res, 200, result);
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
return safeJson(res, 400, { error: e.message || String(e) });
|
|
1152
|
+
}
|
|
1547
1153
|
}
|
|
1548
1154
|
|
|
1549
1155
|
return safeJson(res, 404, { error: 'Not found' });
|