@cccarv82/freya 1.0.10 → 1.0.12

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/cli/web.js CHANGED
@@ -59,6 +59,67 @@ function newestFile(dir, prefix) {
59
59
  return files[0]?.p || null;
60
60
  }
61
61
 
62
+ function settingsPath(workspaceDir) {
63
+ return path.join(workspaceDir, 'data', 'settings', 'settings.json');
64
+ }
65
+
66
+ function readSettings(workspaceDir) {
67
+ const p = settingsPath(workspaceDir);
68
+ try {
69
+ if (!exists(p)) return { discordWebhookUrl: '', teamsWebhookUrl: '' };
70
+ const json = JSON.parse(fs.readFileSync(p, 'utf8'));
71
+ return {
72
+ discordWebhookUrl: json.discordWebhookUrl || '',
73
+ teamsWebhookUrl: json.teamsWebhookUrl || ''
74
+ };
75
+ } catch {
76
+ return { discordWebhookUrl: '', teamsWebhookUrl: '' };
77
+ }
78
+ }
79
+
80
+ function writeSettings(workspaceDir, settings) {
81
+ const p = settingsPath(workspaceDir);
82
+ ensureDir(path.dirname(p));
83
+ const out = {
84
+ schemaVersion: 1,
85
+ updatedAt: new Date().toISOString(),
86
+ discordWebhookUrl: settings.discordWebhookUrl || '',
87
+ teamsWebhookUrl: settings.teamsWebhookUrl || ''
88
+ };
89
+ fs.writeFileSync(p, JSON.stringify(out, null, 2) + '\n', 'utf8');
90
+ return out;
91
+ }
92
+
93
+ function listReports(workspaceDir) {
94
+ const dir = path.join(workspaceDir, 'docs', 'reports');
95
+ if (!exists(dir)) return [];
96
+
97
+ const files = fs.readdirSync(dir)
98
+ .filter((f) => f.endsWith('.md'))
99
+ .map((name) => {
100
+ const full = path.join(dir, name);
101
+ const st = fs.statSync(full);
102
+ return { name, full, mtimeMs: st.mtimeMs, size: st.size };
103
+ })
104
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
105
+
106
+ function kind(name) {
107
+ if (name.startsWith('executive-')) return 'executive';
108
+ if (name.startsWith('sm-weekly-')) return 'sm-weekly';
109
+ if (name.startsWith('blockers-')) return 'blockers';
110
+ if (name.startsWith('daily-')) return 'daily';
111
+ return 'other';
112
+ }
113
+
114
+ return files.map((f) => ({
115
+ kind: kind(f.name),
116
+ name: f.name,
117
+ relPath: path.relative(workspaceDir, f.full).replace(/\\/g, '/'),
118
+ mtimeMs: f.mtimeMs,
119
+ size: f.size
120
+ }));
121
+ }
122
+
62
123
  function safeJson(res, code, obj) {
63
124
  const body = JSON.stringify(obj);
64
125
  res.writeHead(code, {
@@ -185,8 +246,9 @@ async function pickDirectoryNative() {
185
246
  return null;
186
247
  }
187
248
 
188
- function html() {
249
+ function html(defaultDir) {
189
250
  // Aesthetic: “clean workstation” — light-first UI inspired by modern productivity apps.
251
+ const safeDefault = String(defaultDir || './freya').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
190
252
  return `<!doctype html>
191
253
  <html>
192
254
  <head>
@@ -606,6 +668,32 @@ function html() {
606
668
 
607
669
  .sideBtn { width: 100%; margin-top: 8px; }
608
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
+
609
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); }
610
698
  [data-theme="dark"] .k { background: rgba(0,0,0,.18); }
611
699
 
@@ -632,13 +720,6 @@ function html() {
632
720
  <div class="help">Dica: se você já tem uma workspace antiga, use <b>Update</b>. Por padrão, <b>data/</b> e <b>logs/</b> não são sobrescritos.</div>
633
721
  </div>
634
722
 
635
- <div class="sideBlock">
636
- <h3>Publish</h3>
637
- <button class="btn sideBtn" onclick="publish('discord')">Publish → Discord</button>
638
- <button class="btn sideBtn" onclick="publish('teams')">Publish → Teams</button>
639
- <div class="help">Configure os webhooks no painel principal. O publish envia o último relatório gerado.</div>
640
- </div>
641
-
642
723
  <div class="sideBlock">
643
724
  <h3>Atalhos</h3>
644
725
  <div class="help"><span class="k">--dev</span> cria dados de exemplo para testar rápido.</div>
@@ -695,42 +776,52 @@ function html() {
695
776
 
696
777
  <div style="height:12px"></div>
697
778
 
698
- <div class="stack">
699
- <button class="btn primary" onclick="doInit()">Init</button>
700
- <button class="btn" onclick="doUpdate()">Update</button>
701
- <button class="btn" onclick="doHealth()">Health</button>
702
- <button class="btn" onclick="doMigrate()">Migrate</button>
703
- </div>
704
-
705
- <div style="height:16px"></div>
706
-
707
779
  <label>Discord webhook URL</label>
708
780
  <input id="discord" placeholder="https://discord.com/api/webhooks/..." />
709
781
  <div style="height:10px"></div>
710
782
 
711
783
  <label>Teams webhook URL</label>
712
784
  <input id="teams" placeholder="https://..." />
713
- <div class="help">O publish usa incoming webhooks. (Depois a gente evolui para anexos/chunks.)</div>
785
+ <div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
714
786
 
715
787
  <div style="height:10px"></div>
716
788
  <div class="stack">
717
- <button class="btn" onclick="publish('discord')">Publish last → Discord</button>
718
- <button class="btn" onclick="publish('teams')">Publish lastTeams</button>
789
+ <button class="btn" onclick="saveSettings()">Save settings</button>
790
+ <button class="btn" onclick="publish('discord')">Publish selectedDiscord</button>
791
+ <button class="btn" onclick="publish('teams')">Publish selected → Teams</button>
792
+ </div>
793
+
794
+ <div style="height:14px"></div>
795
+
796
+ <div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
797
+ </div>
798
+ </div>
799
+
800
+ <div class="panel">
801
+ <div class="panelHead">
802
+ <b>Reports</b>
803
+ <div class="stack">
804
+ <button class="btn small" onclick="refreshReports()">Refresh</button>
719
805
  </div>
720
806
  </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>
721
812
  </div>
722
813
 
723
814
  <div class="panel">
724
815
  <div class="panelHead">
725
- <b>Output</b>
816
+ <b>Preview</b>
726
817
  <div class="stack">
727
818
  <button class="btn small" onclick="copyOut()">Copy</button>
728
819
  <button class="btn small" onclick="clearOut()">Clear</button>
729
820
  </div>
730
821
  </div>
731
822
  <div class="panelBody">
732
- <div class="log" id="out"></div>
733
- <div class="help">Dica: quando um report gera arquivo, mostramos o conteúdo real do report aqui (melhor que stdout).</div>
823
+ <div id="reportPreview" class="log md" style="font-family: var(--sans);"></div>
824
+ <div class="help">O preview renderiza Markdown básico (headers, listas, code). O botão Copy copia o conteúdo completo.</div>
734
825
  </div>
735
826
  </div>
736
827
 
@@ -743,8 +834,9 @@ function html() {
743
834
  </div>
744
835
 
745
836
  <script>
837
+ window.__FREYA_DEFAULT_DIR = "${safeDefault}";
746
838
  const $ = (id) => document.getElementById(id);
747
- const state = { lastReportPath: null, lastText: '' };
839
+ const state = { lastReportPath: null, lastText: '', reports: [], selectedReport: null };
748
840
 
749
841
  function applyTheme(theme) {
750
842
  document.documentElement.setAttribute('data-theme', theme);
@@ -766,13 +858,89 @@ function html() {
766
858
  $('status') && ($('status').textContent = text);
767
859
  }
768
860
 
861
+ function escapeHtml(str) {
862
+ return String(str)
863
+ .replace(/&/g, '&amp;')
864
+ .replace(/</g, '&lt;')
865
+ .replace(/>/g, '&gt;')
866
+ .replace(/\"/g, '&quot;')
867
+ .replace(/'/g, '&#39;');
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 BT = String.fromCharCode(96); // backtick
877
+ const FENCE = BT + BT + BT;
878
+ const inlineCodeRe = /\x60([^\x60]+)\x60/g;
879
+
880
+ const closeList = () => {
881
+ if (inList) { html += '</ul>'; inList = false; }
882
+ };
883
+
884
+ for (const line of lines) {
885
+ if (line.trim().startsWith(FENCE)) {
886
+ if (!inCode) {
887
+ closeList();
888
+ inCode = true;
889
+ html += '<pre class="md-code"><code>';
890
+ } else {
891
+ inCode = false;
892
+ html += '</code></pre>';
893
+ }
894
+ continue;
895
+ }
896
+
897
+ if (inCode) {
898
+ html += escapeHtml(line) + '\n';
899
+ continue;
900
+ }
901
+
902
+ const h = line.match(/^(#{1,3})\s+(.*)$/);
903
+ if (h) {
904
+ closeList();
905
+ const lvl = h[1].length;
906
+ html += '<h' + lvl + ' class="md-h' + lvl + '">' + escapeHtml(h[2]) + '</h' + lvl + '>';
907
+ continue;
908
+ }
909
+
910
+ const li = line.match(/^\s*[-*]\s+(.*)$/);
911
+ if (li) {
912
+ if (!inList) { html += '<ul class="md-ul">'; inList = true; }
913
+ const content = escapeHtml(li[1]).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
914
+ html += '<li>' + content + '</li>';
915
+ continue;
916
+ }
917
+
918
+ if (line.trim() === '') {
919
+ closeList();
920
+ html += '<div class="md-sp"></div>';
921
+ continue;
922
+ }
923
+
924
+ closeList();
925
+ const p = escapeHtml(line).replace(inlineCodeRe, '<code class="md-inline">$1</code>');
926
+ html += '<p class="md-p">' + p + '</p>';
927
+ }
928
+
929
+ closeList();
930
+ if (inCode) html += '</code></pre>';
931
+ return html;
932
+ }
933
+
769
934
  function setOut(text) {
770
935
  state.lastText = text || '';
771
- $('out').textContent = text || '';
936
+ const el = $('reportPreview');
937
+ if (el) el.innerHTML = renderMarkdown(state.lastText);
772
938
  }
773
939
 
774
940
  function clearOut() {
775
- setOut('');
941
+ state.lastText = '';
942
+ const el = $('reportPreview');
943
+ if (el) el.innerHTML = '';
776
944
  setPill('ok', 'idle');
777
945
  }
778
946
 
@@ -793,15 +961,13 @@ function html() {
793
961
 
794
962
  function saveLocal() {
795
963
  localStorage.setItem('freya.dir', $('dir').value);
796
- localStorage.setItem('freya.discord', $('discord').value);
797
- localStorage.setItem('freya.teams', $('teams').value);
798
964
  }
799
965
 
800
966
  function loadLocal() {
801
- $('dir').value = localStorage.getItem('freya.dir') || './freya';
802
- $('discord').value = localStorage.getItem('freya.discord') || '';
803
- $('teams').value = localStorage.getItem('freya.teams') || '';
967
+ $('dir').value = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__') ? window.__FREYA_DEFAULT_DIR : (localStorage.getItem('freya.dir') || './freya');
804
968
  $('sidePath').textContent = $('dir').value || './freya';
969
+ // Always persist the current run's default dir to avoid stale values
970
+ localStorage.setItem('freya.dir', $('dir').value || './freya');
805
971
  }
806
972
 
807
973
  async function api(p, body) {
@@ -820,6 +986,78 @@ function html() {
820
986
  return d || './freya';
821
987
  }
822
988
 
989
+ function fmtWhen(ms) {
990
+ try {
991
+ const d = new Date(ms);
992
+ const yy = String(d.getFullYear());
993
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
994
+ const dd = String(d.getDate()).padStart(2, '0');
995
+ const hh = String(d.getHours()).padStart(2, '0');
996
+ const mi = String(d.getMinutes()).padStart(2, '0');
997
+ return yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mi;
998
+ } catch {
999
+ return '';
1000
+ }
1001
+ }
1002
+
1003
+ async function selectReport(item) {
1004
+ const rr = await api('/api/reports/read', { dir: dirOrDefault(), relPath: item.relPath });
1005
+ state.selectedReport = item;
1006
+ setLast(item.name);
1007
+ setOut(rr.text || '');
1008
+ renderReportsList();
1009
+ }
1010
+
1011
+ function renderReportsList() {
1012
+ const list = $('reportsList');
1013
+ if (!list) return;
1014
+ const q = ($('reportsFilter') ? $('reportsFilter').value : '').trim().toLowerCase();
1015
+ const filtered = (state.reports || []).filter((it) => {
1016
+ if (!q) return true;
1017
+ return (it.name + ' ' + it.kind).toLowerCase().includes(q);
1018
+ });
1019
+
1020
+ list.innerHTML = '';
1021
+ for (const item of filtered) {
1022
+ const btn = document.createElement('button');
1023
+ btn.className = 'rep' + (state.selectedReport && state.selectedReport.relPath === item.relPath ? ' repActive' : '');
1024
+ btn.type = 'button';
1025
+ const meta = fmtWhen(item.mtimeMs);
1026
+ btn.innerHTML =
1027
+ '<div style="display:flex; gap:10px; align-items:center; justify-content:space-between">'
1028
+ + '<div style="min-width:0">'
1029
+ + '<div><span style="font-weight:800">' + escapeHtml(item.kind) + '</span> <span style="opacity:.7">—</span> ' + escapeHtml(item.name) + '</div>'
1030
+ + '<div style="opacity:.65; font-size:11px; margin-top:4px">' + escapeHtml(item.relPath) + '</div>'
1031
+ + '</div>'
1032
+ + '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(meta) + '</div>'
1033
+ + '</div>';
1034
+
1035
+ btn.onclick = async () => {
1036
+ try {
1037
+ await selectReport(item);
1038
+ } catch (e) {
1039
+ setPill('err', 'open failed');
1040
+ }
1041
+ };
1042
+ list.appendChild(btn);
1043
+ }
1044
+ }
1045
+
1046
+ async function refreshReports() {
1047
+ try {
1048
+ const r = await api('/api/reports/list', { dir: dirOrDefault() });
1049
+ state.reports = (r.reports || []).slice(0, 50);
1050
+ renderReportsList();
1051
+
1052
+ // Auto-select latest if nothing selected yet
1053
+ if (!state.selectedReport && state.reports && state.reports[0]) {
1054
+ await selectReport(state.reports[0]);
1055
+ }
1056
+ } catch (e) {
1057
+ // ignore
1058
+ }
1059
+ }
1060
+
823
1061
  async function pickDir() {
824
1062
  try {
825
1063
  setPill('run','picker…');
@@ -845,6 +1083,7 @@ function html() {
845
1083
  const r = await api('/api/init', { dir: dirOrDefault() });
846
1084
  setOut(r.output);
847
1085
  setLast(null);
1086
+ await refreshReports();
848
1087
  setPill('ok','init ok');
849
1088
  } catch (e) {
850
1089
  setPill('err','init failed');
@@ -861,6 +1100,7 @@ function html() {
861
1100
  const r = await api('/api/update', { dir: dirOrDefault() });
862
1101
  setOut(r.output);
863
1102
  setLast(null);
1103
+ await refreshReports();
864
1104
  setPill('ok','update ok');
865
1105
  } catch (e) {
866
1106
  setPill('err','update failed');
@@ -910,6 +1150,7 @@ function html() {
910
1150
  setOut(r.output);
911
1151
  setLast(r.reportPath || null);
912
1152
  if (r.reportText) state.lastText = r.reportText;
1153
+ await refreshReports();
913
1154
  setPill('ok', name + ' ok');
914
1155
  } catch (e) {
915
1156
  setPill('err', name + ' failed');
@@ -917,6 +1158,24 @@ function html() {
917
1158
  }
918
1159
  }
919
1160
 
1161
+ async function saveSettings() {
1162
+ try {
1163
+ saveLocal();
1164
+ setPill('run','saving…');
1165
+ await api('/api/settings/save', {
1166
+ dir: dirOrDefault(),
1167
+ settings: {
1168
+ discordWebhookUrl: $('discord').value.trim(),
1169
+ teamsWebhookUrl: $('teams').value.trim()
1170
+ }
1171
+ });
1172
+ setPill('ok','saved');
1173
+ setTimeout(() => setPill('ok','idle'), 800);
1174
+ } catch (e) {
1175
+ setPill('err','save failed');
1176
+ }
1177
+ }
1178
+
920
1179
  async function publish(target) {
921
1180
  try {
922
1181
  saveLocal();
@@ -936,6 +1195,25 @@ function html() {
936
1195
  applyTheme(localStorage.getItem('freya.theme') || 'light');
937
1196
  $('chipPort').textContent = location.host;
938
1197
  loadLocal();
1198
+
1199
+ // Load persisted settings from the workspace
1200
+ (async () => {
1201
+ try {
1202
+ const r = await api('/api/defaults', { dir: dirOrDefault() });
1203
+ if (r && r.workspaceDir) {
1204
+ $('dir').value = r.workspaceDir;
1205
+ $('sidePath').textContent = r.workspaceDir;
1206
+ }
1207
+ if (r && r.settings) {
1208
+ $('discord').value = r.settings.discordWebhookUrl || '';
1209
+ $('teams').value = r.settings.teamsWebhookUrl || '';
1210
+ }
1211
+ } catch (e) {
1212
+ // ignore
1213
+ }
1214
+ refreshReports();
1215
+ })();
1216
+
939
1217
  setPill('ok','idle');
940
1218
  </script>
941
1219
  </body>
@@ -1119,7 +1397,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1119
1397
  if (!req.url) return safeJson(res, 404, { error: 'Not found' });
1120
1398
 
1121
1399
  if (req.method === 'GET' && req.url === '/') {
1122
- const body = html();
1400
+ const body = html(dir || './freya');
1123
1401
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
1124
1402
  res.end(body);
1125
1403
  return;
@@ -1137,6 +1415,32 @@ async function cmdWeb({ port, dir, open, dev }) {
1137
1415
  return safeJson(res, 200, { dir: picked });
1138
1416
  }
1139
1417
 
1418
+ if (req.url === '/api/defaults') {
1419
+ const settings = readSettings(workspaceDir);
1420
+ const reports = listReports(workspaceDir).slice(0, 20);
1421
+ return safeJson(res, 200, { workspaceDir, settings, reports });
1422
+ }
1423
+
1424
+ if (req.url === '/api/settings/save') {
1425
+ const incoming = payload.settings || {};
1426
+ const saved = writeSettings(workspaceDir, incoming);
1427
+ return safeJson(res, 200, { ok: true, settings: { discordWebhookUrl: saved.discordWebhookUrl, teamsWebhookUrl: saved.teamsWebhookUrl } });
1428
+ }
1429
+
1430
+ if (req.url === '/api/reports/list') {
1431
+ const reports = listReports(workspaceDir);
1432
+ return safeJson(res, 200, { reports });
1433
+ }
1434
+
1435
+ if (req.url === '/api/reports/read') {
1436
+ const rel = payload.relPath;
1437
+ if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
1438
+ const full = path.join(workspaceDir, rel);
1439
+ if (!exists(full)) return safeJson(res, 404, { error: 'Report not found' });
1440
+ const text = fs.readFileSync(full, 'utf8');
1441
+ return safeJson(res, 200, { relPath: rel, text });
1442
+ }
1443
+
1140
1444
  if (req.url === '/api/init') {
1141
1445
  const pkg = '@cccarv82/freya';
1142
1446
  const r = await run(guessNpxCmd(), [guessNpxYesFlag(), pkg, 'init', workspaceDir], process.cwd());
@@ -1178,7 +1482,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1178
1482
  blockers: 'blockers-',
1179
1483
  'sm-weekly': 'sm-weekly-',
1180
1484
  status: 'executive-',
1181
- daily: null
1485
+ daily: 'daily-'
1182
1486
  };
1183
1487
  const prefix = prefixMap[script] || null;
1184
1488
  const reportPath = prefix ? newestFile(reportsDir, prefix) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",
@@ -6,6 +6,7 @@ const { safeReadJson, quarantineCorruptedFile } = require('./lib/fs-utils');
6
6
 
7
7
  const TASK_LOG_PATH = path.join(__dirname, '../data/tasks/task-log.json');
8
8
  const BLOCKERS_LOG_PATH = path.join(__dirname, '../data/blockers/blocker-log.json');
9
+ const REPORT_DIR = path.join(__dirname, '../docs/reports');
9
10
 
10
11
  // --- Helper Logic ---
11
12
  const now = new Date();
@@ -88,6 +89,16 @@ function generateDailySummary() {
88
89
 
89
90
  console.log(summary);
90
91
 
92
+ // Write report file for UI (optional, but helps preview/history)
93
+ try {
94
+ fs.mkdirSync(REPORT_DIR, { recursive: true });
95
+ const date = new Date().toISOString().slice(0, 10);
96
+ const outPath = path.join(REPORT_DIR, `daily-${date}.md`);
97
+ fs.writeFileSync(outPath, `# Daily Summary — ${date}\n\n${summary}\n`, 'utf8');
98
+ } catch (e) {
99
+ // non-fatal
100
+ }
101
+
91
102
  } catch (err) {
92
103
  console.error("Error generating daily:", err.message);
93
104
  }
@@ -218,7 +218,7 @@ function generateReport(period) {
218
218
 
219
219
  // Save
220
220
  ensureDir(OUTPUT_DIR);
221
- const filename = `report-${period}-${dateStr}.md`;
221
+ const filename = `executive-${period}-${dateStr}.md`;
222
222
  const outputPath = path.join(OUTPUT_DIR, filename);
223
223
  fs.writeFileSync(outputPath, md, 'utf8');
224
224