@cccarv82/freya 1.0.30 → 1.0.32

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/web-ui.js +118 -4
  2. package/cli/web.js +165 -3
  3. package/package.json +2 -1
package/cli/web-ui.js CHANGED
@@ -12,7 +12,8 @@
12
12
  lastPlan: '',
13
13
  lastApplied: null,
14
14
  autoApply: true,
15
- autoRunReports: false
15
+ autoRunReports: false,
16
+ prettyPublish: true
16
17
  };
17
18
 
18
19
  function applyTheme(theme) {
@@ -205,6 +206,7 @@
205
206
  localStorage.setItem('freya.dir', $('dir').value);
206
207
  try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
207
208
  try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
209
+ try { localStorage.setItem('freya.prettyPublish', state.prettyPublish ? '1' : '0'); } catch {}
208
210
  }
209
211
 
210
212
  function loadLocal() {
@@ -218,6 +220,11 @@
218
220
  if (v2 !== null) state.autoRunReports = v2 === '1';
219
221
  const cb2 = $('autoRunReports');
220
222
  if (cb2) cb2.checked = !!state.autoRunReports;
223
+
224
+ const v3 = localStorage.getItem('freya.prettyPublish');
225
+ if (v3 !== null) state.prettyPublish = v3 === '1';
226
+ const cb3 = $('prettyPublish');
227
+ if (cb3) cb3.checked = !!state.prettyPublish;
221
228
  } catch {}
222
229
 
223
230
  const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
@@ -318,7 +325,7 @@
318
325
  async function refreshReports(opts = {}) {
319
326
  try {
320
327
  const r = await api('/api/reports/list', { dir: dirOrDefault() });
321
- state.reports = (r.reports || []).slice(0, 50);
328
+ state.reports = (r.reports || []).slice(0, 10);
322
329
  renderReportsList();
323
330
 
324
331
  const latest = state.reports && state.reports[0] ? state.reports[0] : null;
@@ -337,6 +344,44 @@
337
344
  }
338
345
  }
339
346
 
347
+ async function editTask(t) {
348
+ try {
349
+ const currentSlug = t.projectSlug ? String(t.projectSlug) : '';
350
+ const slug = prompt('projectSlug (ex: vivo/fidelizacao/chg0178682):', currentSlug);
351
+ if (slug === null) return;
352
+
353
+ const currentCat = String(t.category || 'DO_NOW');
354
+ const cat = prompt('category (DO_NOW|SCHEDULE|DELEGATE|IGNORE):', currentCat);
355
+ if (cat === null) return;
356
+
357
+ setPill('run', 'updating…');
358
+ await api('/api/tasks/update', { dir: dirOrDefault(), id: t.id, patch: { projectSlug: slug, category: cat } });
359
+ await refreshToday();
360
+ setPill('ok', 'updated');
361
+ setTimeout(() => setPill('ok', 'idle'), 800);
362
+ } catch (e) {
363
+ setPill('err', 'update failed');
364
+ setOut(String(e && e.message ? e.message : e));
365
+ }
366
+ }
367
+
368
+ async function editBlocker(b) {
369
+ try {
370
+ const currentSlug = b.projectSlug ? String(b.projectSlug) : '';
371
+ const slug = prompt('projectSlug (ex: vivo/bnpl/dpgc):', currentSlug);
372
+ if (slug === null) return;
373
+
374
+ setPill('run', 'updating…');
375
+ await api('/api/blockers/update', { dir: dirOrDefault(), id: b.id, patch: { projectSlug: slug } });
376
+ await refreshToday();
377
+ setPill('ok', 'updated');
378
+ setTimeout(() => setPill('ok', 'idle'), 800);
379
+ } catch (e) {
380
+ setPill('err', 'update failed');
381
+ setOut(String(e && e.message ? e.message : e));
382
+ }
383
+ }
384
+
340
385
  function renderTasks(list) {
341
386
  const el = $('tasksList');
342
387
  if (!el) return;
@@ -350,9 +395,14 @@
350
395
  + '<div style="opacity:.7; font-size:11px; margin-top:4px">' + escapeHtml(String(t.category || ''))
351
396
  + (t.projectSlug ? (' · <span style="font-family:var(--mono); opacity:.9">[' + escapeHtml(String(t.projectSlug)) + ']</span>') : '')
352
397
  + (pri ? (' · ' + escapeHtml(pri)) : '') + '</div></div>'
398
+ + '<div style="display:flex; gap:8px">'
353
399
  + '<button class="btn small" type="button">Complete</button>'
400
+ + '<button class="btn small" type="button">Edit</button>'
401
+ + '</div>'
354
402
  + '</div>';
355
- const btn = row.querySelector('button');
403
+ const btns = row.querySelectorAll('button');
404
+ const btn = btns[0];
405
+ if (btns[1]) btns[1].onclick = () => editTask(t);
356
406
  btn.onclick = async () => {
357
407
  try {
358
408
  setPill('run', 'completing…');
@@ -389,8 +439,13 @@
389
439
  + (b.projectSlug ? (' <span style="font-family:var(--mono); opacity:.8">[' + escapeHtml(String(b.projectSlug)) + ']</span>') : '')
390
440
  + '</div>'
391
441
  + '</div>'
442
+ + '<div style="display:flex; gap:8px; align-items:center">'
392
443
  + '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
444
+ + '<button class="btn small" type="button">Edit</button>'
445
+ + '</div>'
393
446
  + '</div>';
447
+ const ebtn = row.querySelector('button');
448
+ if (ebtn) ebtn.onclick = () => editBlocker(b);
394
449
  el.appendChild(row);
395
450
  }
396
451
  if (!el.childElementCount) {
@@ -519,6 +574,54 @@
519
574
  }
520
575
  }
521
576
 
577
+ async function exportObsidian() {
578
+ try {
579
+ setPill('run', 'exporting…');
580
+ const r = await api('/api/obsidian/export', { dir: dirOrDefault() });
581
+ setOut('## Obsidian export
582
+
583
+ ' + (r.output || 'ok'));
584
+ setPill('ok', 'exported');
585
+ setTimeout(() => setPill('ok', 'idle'), 800);
586
+ } catch (e) {
587
+ setPill('err', 'export failed');
588
+ setOut(String(e && e.message ? e.message : e));
589
+ }
590
+ }
591
+
592
+ async function reloadSlugRules() {
593
+ try {
594
+ const r = await api('/api/project-slug-map/get', { dir: dirOrDefault() });
595
+ const el = $('slugRules');
596
+ if (el) el.value = JSON.stringify(r.map || { rules: [] }, null, 2);
597
+ setPill('ok', 'rules loaded');
598
+ setTimeout(() => setPill('ok', 'idle'), 800);
599
+ } catch (e) {
600
+ setPill('err', 'rules load failed');
601
+ setOut(String(e && e.message ? e.message : e));
602
+ }
603
+ }
604
+
605
+ async function saveSlugRules() {
606
+ try {
607
+ const el = $('slugRules');
608
+ if (!el) return;
609
+ const raw = String(el.value || '').trim();
610
+ if (!raw) throw new Error('Rules JSON is empty');
611
+ let map;
612
+ try { map = JSON.parse(raw); } catch (e) { throw new Error('Invalid JSON: ' + (e.message || e)); }
613
+
614
+ setPill('run', 'saving rules…');
615
+ const r = await api('/api/project-slug-map/save', { dir: dirOrDefault(), map });
616
+ if (el) el.value = JSON.stringify(r.map || map, null, 2);
617
+ setPill('ok', 'rules saved');
618
+ setTimeout(() => setPill('ok', 'idle'), 800);
619
+ } catch (e) {
620
+ setPill('err', 'rules save failed');
621
+ setOut(String(e && e.message ? e.message : e));
622
+ }
623
+ }
624
+
522
625
  async function saveSettings() {
523
626
  try {
524
627
  saveLocal();
@@ -560,7 +663,8 @@
560
663
  const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
561
664
  if (!webhookUrl) throw new Error('Configure o webhook antes.');
562
665
  setPill('run', 'publish…');
563
- await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks', allowSecrets: true });
666
+ const mode = state.prettyPublish ? 'pretty' : 'chunks';
667
+ await api('/api/publish', { webhookUrl, text: state.lastText, mode, allowSecrets: true });
564
668
  setPill('ok', 'published');
565
669
  } catch (e) {
566
670
  setPill('err', 'publish failed');
@@ -587,6 +691,12 @@
587
691
  }
588
692
  }
589
693
 
694
+ function togglePrettyPublish() {
695
+ const cb = $('prettyPublish');
696
+ state.prettyPublish = cb ? !!cb.checked : true;
697
+ try { localStorage.setItem('freya.prettyPublish', state.prettyPublish ? '1' : '0'); } catch {}
698
+ }
699
+
590
700
  function toggleAutoRunReports() {
591
701
  const cb = $('autoRunReports');
592
702
  state.autoRunReports = cb ? !!cb.checked : false;
@@ -758,6 +868,9 @@
758
868
  window.saveSettings = saveSettings;
759
869
  window.refreshReports = refreshReports;
760
870
  window.refreshToday = refreshToday;
871
+ window.reloadSlugRules = reloadSlugRules;
872
+ window.saveSlugRules = saveSlugRules;
873
+ window.exportObsidian = exportObsidian;
761
874
  window.renderReportsList = renderReportsList;
762
875
  window.copyOut = copyOut;
763
876
  window.copyPath = copyPath;
@@ -769,6 +882,7 @@
769
882
  window.saveAndPlan = saveAndPlan;
770
883
  window.toggleAutoApply = toggleAutoApply;
771
884
  window.toggleAutoRunReports = toggleAutoRunReports;
885
+ window.togglePrettyPublish = togglePrettyPublish;
772
886
  window.applyPlan = applyPlan;
773
887
  window.runSuggestedReports = runSuggestedReports;
774
888
  })();
package/cli/web.js CHANGED
@@ -181,6 +181,36 @@ function listReports(workspaceDir) {
181
181
  }));
182
182
  }
183
183
 
184
+ function extractTitleFromMarkdown(md) {
185
+ const t = String(md || '');
186
+ const m = /^#\s+(.+)$/m.exec(t);
187
+ if (m && m[1]) return m[1].trim();
188
+ // fallback: first non-empty line
189
+ const line = t.split(/\r?\n/).map((l) => l.trim()).find((l) => l);
190
+ return line ? line.slice(0, 80) : 'Freya report';
191
+ }
192
+
193
+ function stripFirstH1(md) {
194
+ const t = String(md || '');
195
+ return t.replace(/^#\s+.+\r?\n/, '').trim();
196
+ }
197
+
198
+ function splitForEmbed(text, limit = 3900) {
199
+ const t = String(text || '');
200
+ if (t.length <= limit) return [t];
201
+ const chunks = [];
202
+ let i = 0;
203
+ while (i < t.length) {
204
+ let end = Math.min(t.length, i + limit);
205
+ // prefer splitting at newline
206
+ const nl = t.lastIndexOf('\n', end);
207
+ if (nl > i + 200) end = nl;
208
+ chunks.push(t.slice(i, end));
209
+ i = end;
210
+ }
211
+ return chunks;
212
+ }
213
+
184
214
  function splitForDiscord(text, limit = 1900) {
185
215
  const t = String(text || '');
186
216
  if (t.length <= limit) return [t];
@@ -234,14 +264,19 @@ function postJson(url, bodyObj) {
234
264
  });
235
265
  }
236
266
 
237
- function postDiscordWebhook(url, content) {
238
- return postJson(url, { content });
267
+ function postDiscordWebhook(url, payload) {
268
+ if (typeof payload === 'string') return postJson(url, { content: payload });
269
+ return postJson(url, payload);
239
270
  }
240
271
 
241
272
  function postTeamsWebhook(url, text) {
242
273
  return postJson(url, { text });
243
274
  }
244
275
 
276
+ function postTeamsCard(url, card) {
277
+ return postJson(url, card);
278
+ }
279
+
245
280
  function escapeJsonControlChars(jsonText) {
246
281
  // Replace unescaped control chars inside JSON string literals with safe escapes.
247
282
  // Handles Copilot outputs where newlines/tabs leak into string values.
@@ -389,6 +424,42 @@ async function publishRobust(webhookUrl, text, opts = {}) {
389
424
  const u = new URL(webhookUrl);
390
425
  const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
391
426
 
427
+ const mode = String(opts.mode || 'chunks');
428
+
429
+ if (mode === 'pretty') {
430
+ const title = extractTitleFromMarkdown(text);
431
+ const body = stripFirstH1(text);
432
+
433
+ if (isDiscord) {
434
+ const parts = splitForEmbed(body, 3900);
435
+ for (let i = 0; i < parts.length; i++) {
436
+ const payload = {
437
+ embeds: [
438
+ {
439
+ title: i === 0 ? title : undefined,
440
+ description: parts[i],
441
+ color: 0x5865F2
442
+ }
443
+ ]
444
+ };
445
+ await postDiscordWebhook(webhookUrl, payload);
446
+ }
447
+ return { ok: true, chunks: parts.length, mode: 'pretty' };
448
+ }
449
+
450
+ // Teams (MessageCard)
451
+ const card = {
452
+ '@type': 'MessageCard',
453
+ '@context': 'http://schema.org/extensions',
454
+ summary: title,
455
+ themeColor: '0078D7',
456
+ title,
457
+ text: body
458
+ };
459
+ await postTeamsCard(webhookUrl, card);
460
+ return { ok: true, chunks: 1, mode: 'pretty' };
461
+ }
462
+
392
463
  const chunks = isDiscord ? splitForDiscord(text, 1900) : splitForDiscord(text, 1800);
393
464
 
394
465
  for (const chunk of chunks) {
@@ -604,7 +675,6 @@ function buildHtml(safeDefault) {
604
675
  <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>
605
676
  <div style="height:10px"></div>
606
677
  <div class="stack">
607
- <button class="btn sideBtn" onclick="saveInbox()">Save to Daily Log</button>
608
678
  <button class="btn primary sideBtn" onclick="saveAndPlan()">Save + Process (Agents)</button>
609
679
  <button class="btn sideBtn" onclick="runSuggestedReports()">Run suggested reports</button>
610
680
  </div>
@@ -682,6 +752,11 @@ function buildHtml(safeDefault) {
682
752
  <div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
683
753
 
684
754
  <div style="height:10px"></div>
755
+ <label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
756
+ <input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
757
+ Pretty publish (cards/embeds)
758
+ </label>
759
+
685
760
  <div class="stack">
686
761
  <button class="btn" onclick="saveSettings()">Save settings</button>
687
762
  <button class="btn" onclick="publish('discord')">Publish selected → Discord</button>
@@ -691,6 +766,16 @@ function buildHtml(safeDefault) {
691
766
  <div style="height:14px"></div>
692
767
 
693
768
  <div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
769
+
770
+ <div style="height:14px"></div>
771
+ <label>Project slug rules</label>
772
+ <textarea id="slugRules" rows="8" placeholder="{ \"rules\": [ { \"contains\": \"fideliza\", \"slug\": \"vivo/fidelizacao\" } ] }" 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; font-family: var(--mono);"></textarea>
773
+ <div class="help">Regras usadas pra inferir <code>projectSlug</code>. Formato JSON (objeto com <code>rules</code>). Editável no estilo Obsidian-friendly.</div>
774
+ <div class="stack" style="margin-top:10px">
775
+ <button class="btn" onclick="reloadSlugRules()">Reload rules</button>
776
+ <button class="btn" onclick="saveSlugRules()">Save rules</button>
777
+ <button class="btn" onclick="exportObsidian()">Export Obsidian notes</button>
778
+ </div>
694
779
  </div>
695
780
  </div>
696
781
 
@@ -994,6 +1079,29 @@ async function cmdWeb({ port, dir, open, dev }) {
994
1079
  return safeJson(res, 200, { ok: true, settings: { discordWebhookUrl: saved.discordWebhookUrl, teamsWebhookUrl: saved.teamsWebhookUrl } });
995
1080
  }
996
1081
 
1082
+
1083
+ if (req.url === '/api/project-slug-map/get') {
1084
+ const map = readProjectSlugMap(workspaceDir);
1085
+ return safeJson(res, 200, { ok: true, map });
1086
+ }
1087
+
1088
+ if (req.url === '/api/project-slug-map/save') {
1089
+ const map = payload.map;
1090
+ if (!map || typeof map !== 'object') return safeJson(res, 400, { error: 'Missing map' });
1091
+ if (!Array.isArray(map.rules)) return safeJson(res, 400, { error: 'map.rules must be an array' });
1092
+
1093
+ // normalize + basic validation
1094
+ const rules = map.rules
1095
+ .map((r) => ({ contains: String(r.contains || '').trim(), slug: String(r.slug || '').trim() }))
1096
+ .filter((r) => r.contains && r.slug);
1097
+
1098
+ const out = { schemaVersion: 1, updatedAt: new Date().toISOString(), rules };
1099
+ const p = projectSlugMapPath(workspaceDir);
1100
+ ensureDir(require('path').dirname(p));
1101
+ fs.writeFileSync(p, JSON.stringify(out, null, 2) + '\n', 'utf8');
1102
+ return safeJson(res, 200, { ok: true, map: out });
1103
+ }
1104
+
997
1105
  if (req.url === '/api/reports/list') {
998
1106
  const reports = listReports(workspaceDir);
999
1107
  return safeJson(res, 200, { reports });
@@ -1445,6 +1553,13 @@ async function cmdWeb({ port, dir, open, dev }) {
1445
1553
  }
1446
1554
 
1447
1555
 
1556
+
1557
+ if (req.url === '/api/obsidian/export') {
1558
+ const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir);
1559
+ const out = (r.stdout + r.stderr).trim();
1560
+ return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
1561
+ }
1562
+
1448
1563
  if (req.url === '/api/tasks/list') {
1449
1564
  const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1450
1565
  const cat = payload.category ? String(payload.category).trim() : null;
@@ -1498,6 +1613,30 @@ async function cmdWeb({ port, dir, open, dev }) {
1498
1613
  return safeJson(res, 200, { ok: true, task: updated });
1499
1614
  }
1500
1615
 
1616
+
1617
+ if (req.url === '/api/tasks/update') {
1618
+ const id = String(payload.id || '').trim();
1619
+ if (!id) return safeJson(res, 400, { error: 'Missing id' });
1620
+ const patch = payload.patch && typeof payload.patch === 'object' ? payload.patch : {};
1621
+
1622
+ const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
1623
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, tasks: [] };
1624
+ const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
1625
+
1626
+ let updated = null;
1627
+ for (const t of tasks) {
1628
+ if (t && t.id === id) {
1629
+ if (typeof patch.projectSlug === 'string') t.projectSlug = patch.projectSlug.trim() || undefined;
1630
+ if (typeof patch.category === 'string') t.category = patch.category.trim();
1631
+ updated = t;
1632
+ break;
1633
+ }
1634
+ }
1635
+ if (!updated) return safeJson(res, 404, { error: 'Task not found' });
1636
+ writeJson(file, doc);
1637
+ return safeJson(res, 200, { ok: true, task: updated });
1638
+ }
1639
+
1501
1640
  if (req.url === '/api/blockers/list') {
1502
1641
  const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1503
1642
  const status = payload.status ? String(payload.status).trim() : 'OPEN';
@@ -1531,6 +1670,29 @@ async function cmdWeb({ port, dir, open, dev }) {
1531
1670
 
1532
1671
  return safeJson(res, 200, { ok: true, blockers: filtered });
1533
1672
  }
1673
+
1674
+ if (req.url === '/api/blockers/update') {
1675
+ const id = String(payload.id || '').trim();
1676
+ if (!id) return safeJson(res, 400, { error: 'Missing id' });
1677
+ const patch = payload.patch && typeof payload.patch === 'object' ? payload.patch : {};
1678
+
1679
+ const file = path.join(workspaceDir, 'data', 'blockers', 'blocker-log.json');
1680
+ const doc = readJsonOrNull(file) || { schemaVersion: 1, blockers: [] };
1681
+ const blockers = Array.isArray(doc.blockers) ? doc.blockers : [];
1682
+
1683
+ let updated = null;
1684
+ for (const b of blockers) {
1685
+ if (b && b.id === id) {
1686
+ if (typeof patch.projectSlug === 'string') b.projectSlug = patch.projectSlug.trim() || undefined;
1687
+ updated = b;
1688
+ break;
1689
+ }
1690
+ }
1691
+ if (!updated) return safeJson(res, 404, { error: 'Blocker not found' });
1692
+ writeJson(file, doc);
1693
+ return safeJson(res, 200, { ok: true, blocker: updated });
1694
+ }
1695
+
1534
1696
  if (req.url === '/api/report') {
1535
1697
  const script = payload.script;
1536
1698
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",
@@ -10,6 +10,7 @@
10
10
  "daily": "node scripts/generate-daily-summary.js",
11
11
  "status": "node scripts/generate-executive-report.js",
12
12
  "blockers": "node scripts/generate-blockers-report.js",
13
+ "export-obsidian": "node scripts/export-obsidian.js",
13
14
  "test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-task-schema.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-validation.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js"
14
15
  },
15
16
  "keywords": [],