@cccarv82/freya 1.0.30 → 1.0.31

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 +52 -2
  2. package/cli/web.js +110 -2
  3. package/package.json +1 -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__')
@@ -519,6 +526,39 @@
519
526
  }
520
527
  }
521
528
 
529
+ async function reloadSlugRules() {
530
+ try {
531
+ const r = await api('/api/project-slug-map/get', { dir: dirOrDefault() });
532
+ const el = $('slugRules');
533
+ if (el) el.value = JSON.stringify(r.map || { rules: [] }, null, 2);
534
+ setPill('ok', 'rules loaded');
535
+ setTimeout(() => setPill('ok', 'idle'), 800);
536
+ } catch (e) {
537
+ setPill('err', 'rules load failed');
538
+ setOut(String(e && e.message ? e.message : e));
539
+ }
540
+ }
541
+
542
+ async function saveSlugRules() {
543
+ try {
544
+ const el = $('slugRules');
545
+ if (!el) return;
546
+ const raw = String(el.value || '').trim();
547
+ if (!raw) throw new Error('Rules JSON is empty');
548
+ let map;
549
+ try { map = JSON.parse(raw); } catch (e) { throw new Error('Invalid JSON: ' + (e.message || e)); }
550
+
551
+ setPill('run', 'saving rules…');
552
+ const r = await api('/api/project-slug-map/save', { dir: dirOrDefault(), map });
553
+ if (el) el.value = JSON.stringify(r.map || map, null, 2);
554
+ setPill('ok', 'rules saved');
555
+ setTimeout(() => setPill('ok', 'idle'), 800);
556
+ } catch (e) {
557
+ setPill('err', 'rules save failed');
558
+ setOut(String(e && e.message ? e.message : e));
559
+ }
560
+ }
561
+
522
562
  async function saveSettings() {
523
563
  try {
524
564
  saveLocal();
@@ -560,7 +600,8 @@
560
600
  const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
561
601
  if (!webhookUrl) throw new Error('Configure o webhook antes.');
562
602
  setPill('run', 'publish…');
563
- await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks', allowSecrets: true });
603
+ const mode = state.prettyPublish ? 'pretty' : 'chunks';
604
+ await api('/api/publish', { webhookUrl, text: state.lastText, mode, allowSecrets: true });
564
605
  setPill('ok', 'published');
565
606
  } catch (e) {
566
607
  setPill('err', 'publish failed');
@@ -587,6 +628,12 @@
587
628
  }
588
629
  }
589
630
 
631
+ function togglePrettyPublish() {
632
+ const cb = $('prettyPublish');
633
+ state.prettyPublish = cb ? !!cb.checked : true;
634
+ try { localStorage.setItem('freya.prettyPublish', state.prettyPublish ? '1' : '0'); } catch {}
635
+ }
636
+
590
637
  function toggleAutoRunReports() {
591
638
  const cb = $('autoRunReports');
592
639
  state.autoRunReports = cb ? !!cb.checked : false;
@@ -758,6 +805,8 @@
758
805
  window.saveSettings = saveSettings;
759
806
  window.refreshReports = refreshReports;
760
807
  window.refreshToday = refreshToday;
808
+ window.reloadSlugRules = reloadSlugRules;
809
+ window.saveSlugRules = saveSlugRules;
761
810
  window.renderReportsList = renderReportsList;
762
811
  window.copyOut = copyOut;
763
812
  window.copyPath = copyPath;
@@ -769,6 +818,7 @@
769
818
  window.saveAndPlan = saveAndPlan;
770
819
  window.toggleAutoApply = toggleAutoApply;
771
820
  window.toggleAutoRunReports = toggleAutoRunReports;
821
+ window.togglePrettyPublish = togglePrettyPublish;
772
822
  window.applyPlan = applyPlan;
773
823
  window.runSuggestedReports = runSuggestedReports;
774
824
  })();
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) {
@@ -682,6 +753,11 @@ function buildHtml(safeDefault) {
682
753
  <div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
683
754
 
684
755
  <div style="height:10px"></div>
756
+ <label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
757
+ <input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
758
+ Pretty publish (cards/embeds)
759
+ </label>
760
+
685
761
  <div class="stack">
686
762
  <button class="btn" onclick="saveSettings()">Save settings</button>
687
763
  <button class="btn" onclick="publish('discord')">Publish selected → Discord</button>
@@ -691,6 +767,15 @@ function buildHtml(safeDefault) {
691
767
  <div style="height:14px"></div>
692
768
 
693
769
  <div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
770
+
771
+ <div style="height:14px"></div>
772
+ <label>Project slug rules</label>
773
+ <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>
774
+ <div class="help">Regras usadas pra inferir <code>projectSlug</code>. Formato JSON (objeto com <code>rules</code>). Editável no estilo Obsidian-friendly.</div>
775
+ <div class="stack" style="margin-top:10px">
776
+ <button class="btn" onclick="reloadSlugRules()">Reload rules</button>
777
+ <button class="btn" onclick="saveSlugRules()">Save rules</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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",