@cccarv82/freya 1.0.43 → 1.0.45

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 +136 -2
  2. package/cli/web.js +136 -0
  3. package/package.json +4 -2
package/cli/web-ui.js CHANGED
@@ -13,7 +13,9 @@
13
13
  lastApplied: null,
14
14
  autoApply: true,
15
15
  autoRunReports: false,
16
- prettyPublish: true
16
+ prettyPublish: true,
17
+ chatSessionId: null,
18
+ chatLoaded: false
17
19
  };
18
20
 
19
21
  function applyTheme(theme) {
@@ -114,6 +116,41 @@
114
116
  return html;
115
117
  }
116
118
 
119
+ function ensureChatSession() {
120
+ if (state.chatSessionId) return state.chatSessionId;
121
+ try {
122
+ const fromLocal = localStorage.getItem('freya.chatSessionId');
123
+ if (fromLocal) {
124
+ state.chatSessionId = fromLocal;
125
+ return state.chatSessionId;
126
+ }
127
+ } catch {}
128
+
129
+ const id = (typeof crypto !== 'undefined' && crypto.randomUUID)
130
+ ? crypto.randomUUID()
131
+ : ('sess-' + Date.now() + '-' + Math.random().toString(16).slice(2, 8));
132
+
133
+ state.chatSessionId = id;
134
+ try { localStorage.setItem('freya.chatSessionId', id); } catch {}
135
+ return id;
136
+ }
137
+
138
+ async function persistChatItem(item) {
139
+ try {
140
+ const sessionId = ensureChatSession();
141
+ await api('/api/chat/append', {
142
+ dir: dirOrDefault(),
143
+ sessionId,
144
+ role: item.role,
145
+ text: item.text,
146
+ markdown: !!item.markdown,
147
+ ts: item.ts
148
+ });
149
+ } catch {
150
+ // best-effort (chat still works)
151
+ }
152
+ }
153
+
117
154
  function chatAppend(role, text, opts = {}) {
118
155
  const thread = $('chatThread');
119
156
  if (!thread) return;
@@ -123,7 +160,7 @@
123
160
 
124
161
  const meta = document.createElement('div');
125
162
  meta.className = 'bubbleMeta';
126
- meta.textContent = role === 'user' ? 'You' : 'FREYA';
163
+ meta.textContent = role === 'user' ? 'Você' : 'FREYA';
127
164
 
128
165
  const body = document.createElement('div');
129
166
  body.className = 'bubbleBody';
@@ -139,12 +176,92 @@
139
176
  bubble.appendChild(body);
140
177
  thread.appendChild(bubble);
141
178
 
179
+ // persist
180
+ persistChatItem({ ts: Date.now(), role, markdown: !!opts.markdown, text: raw });
181
+
142
182
  // keep newest in view
143
183
  try {
144
184
  thread.scrollTop = thread.scrollHeight;
145
185
  } catch {}
146
186
  }
147
187
 
188
+ async function loadChatHistory() {
189
+ if (state.chatLoaded) return;
190
+ state.chatLoaded = true;
191
+ const thread = $('chatThread');
192
+ if (!thread) return;
193
+
194
+ try {
195
+ const sessionId = ensureChatSession();
196
+ const r = await api('/api/chat/load', { dir: dirOrDefault(), sessionId });
197
+ const items = (r && Array.isArray(r.items)) ? r.items : [];
198
+ if (items.length) {
199
+ thread.innerHTML = '';
200
+ for (const it of items) {
201
+ const role = it.role === 'user' ? 'user' : 'assistant';
202
+ const text = String(it.text || '');
203
+ const markdown = !!it.markdown;
204
+ // render without re-persisting
205
+ const bubble = document.createElement('div');
206
+ bubble.className = 'bubble ' + (role === 'user' ? 'user' : 'assistant');
207
+ const meta = document.createElement('div');
208
+ meta.className = 'bubbleMeta';
209
+ meta.textContent = role === 'user' ? 'Você' : 'FREYA';
210
+ const body = document.createElement('div');
211
+ body.className = 'bubbleBody';
212
+ if (markdown) body.innerHTML = renderMarkdown(text);
213
+ else body.innerHTML = escapeHtml(text).replace(/\n/g, '<br>');
214
+ bubble.appendChild(meta);
215
+ bubble.appendChild(body);
216
+ thread.appendChild(bubble);
217
+ }
218
+ try { thread.scrollTop = thread.scrollHeight; } catch {}
219
+ }
220
+ } catch {
221
+ // ignore
222
+ }
223
+ }
224
+
225
+ async function exportChatObsidian() {
226
+ try {
227
+ const sessionId = ensureChatSession();
228
+ setPill('run', 'exportando…');
229
+ const r = await api('/api/chat/export-obsidian', { dir: dirOrDefault(), sessionId });
230
+ const rel = r && r.relPath ? r.relPath : '';
231
+ if (rel) {
232
+ chatAppend('assistant', `Conversa exportada para: **${rel}**`, { markdown: true });
233
+ setPill('ok', 'exportado');
234
+ } else {
235
+ setPill('ok', 'exportado');
236
+ }
237
+ setTimeout(() => setPill('ok', 'pronto'), 800);
238
+ } catch (e) {
239
+ setPill('err', 'export falhou');
240
+ }
241
+ }
242
+
243
+ async function askFreya() {
244
+ const input = $('inboxText');
245
+ const query = input ? input.value.trim() : '';
246
+ if (!query) {
247
+ setPill('err', 'digite uma pergunta');
248
+ return;
249
+ }
250
+
251
+ chatAppend('user', query);
252
+ setPill('run', 'pesquisando…');
253
+ try {
254
+ const sessionId = ensureChatSession();
255
+ const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query });
256
+ const answer = r && r.answer ? r.answer : 'Não encontrei registro';
257
+ chatAppend('assistant', answer, { markdown: true });
258
+ setPill('ok', 'pronto');
259
+ } catch (e) {
260
+ setPill('err', 'falhou');
261
+ chatAppend('assistant', String(e && e.message ? e.message : e));
262
+ }
263
+ }
264
+
148
265
  function setOut(text) {
149
266
  state.lastText = text || '';
150
267
  const el = $('reportPreview');
@@ -618,6 +735,19 @@
618
735
  }
619
736
  }
620
737
 
738
+ async function rebuildIndex() {
739
+ try {
740
+ setPill('run', 'indexing…');
741
+ const r = await api('/api/index/rebuild', { dir: dirOrDefault() });
742
+ setOut('## Index rebuild\n\n' + (r.output || 'ok'));
743
+ setPill('ok', 'indexed');
744
+ setTimeout(() => setPill('ok', 'pronto'), 800);
745
+ } catch (e) {
746
+ setPill('err', 'index failed');
747
+ setOut(String(e && e.message ? e.message : e));
748
+ }
749
+ }
750
+
621
751
  async function reloadSlugRules() {
622
752
  try {
623
753
  const r = await api('/api/project-slug-map/get', { dir: dirOrDefault() });
@@ -906,6 +1036,7 @@
906
1036
  refreshReports();
907
1037
  refreshToday();
908
1038
  reloadSlugRules();
1039
+ loadChatHistory();
909
1040
  })();
910
1041
 
911
1042
  setPill('ok', 'pronto');
@@ -924,6 +1055,7 @@
924
1055
  window.reloadSlugRules = reloadSlugRules;
925
1056
  window.saveSlugRules = saveSlugRules;
926
1057
  window.exportObsidian = exportObsidian;
1058
+ window.rebuildIndex = rebuildIndex;
927
1059
  window.renderReportsList = renderReportsList;
928
1060
  window.copyOut = copyOut;
929
1061
  window.copyPath = copyPath;
@@ -938,4 +1070,6 @@
938
1070
  window.togglePrettyPublish = togglePrettyPublish;
939
1071
  window.applyPlan = applyPlan;
940
1072
  window.runSuggestedReports = runSuggestedReports;
1073
+ window.exportChatObsidian = exportChatObsidian;
1074
+ window.askFreya = askFreya;
941
1075
  })();
package/cli/web.js CHANGED
@@ -5,6 +5,8 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const crypto = require('crypto');
7
7
  const { spawn } = require('child_process');
8
+ const { searchWorkspace } = require('../scripts/lib/search-utils');
9
+ const { searchIndex } = require('../scripts/lib/index-utils');
8
10
 
9
11
  function guessNpmCmd() {
10
12
  // We'll execute via cmd.exe on Windows for reliability.
@@ -840,6 +842,9 @@ function buildHtml(safeDefault) {
840
842
  <div class="panelBody">
841
843
  <div class="help">Logs ficam em <code>logs/</code> e debug traces em <code>.debuglogs/</code> dentro da workspace.</div>
842
844
  <div class="help">Use <b>Open file</b> / <b>Copy path</b> no Preview para abrir/compartilhar o relatório selecionado.</div>
845
+ <div class="stack" style="margin-top:10px">
846
+ <button class="btn" onclick="rebuildIndex()">Rebuild search index</button>
847
+ </div>
843
848
  </div>
844
849
  </div>
845
850
  </div>
@@ -870,6 +875,8 @@ function buildHtml(safeDefault) {
870
875
  <div class="composerActions">
871
876
  <button class="btn primary" type="button" onclick="saveAndPlan()">Salvar + Processar (Agents)</button>
872
877
  <button class="btn" type="button" onclick="runSuggestedReports()">Rodar relatórios sugeridos</button>
878
+ <button class="btn" type="button" onclick="exportChatObsidian()">Exportar conversa (Obsidian)</button>
879
+ <button class="btn" type="button" onclick="askFreya()">Perguntar à Freya</button>
873
880
  </div>
874
881
 
875
882
  <div class="composerToggles">
@@ -1605,6 +1612,135 @@ async function cmdWeb({ port, dir, open, dev }) {
1605
1612
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
1606
1613
  }
1607
1614
 
1615
+ if (req.url === '/api/index/rebuild') {
1616
+ const r = await run(npmCmd, ['run', 'build-index'], workspaceDir);
1617
+ const out = (r.stdout + r.stderr).trim();
1618
+ return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
1619
+ }
1620
+
1621
+ if (req.url === '/api/chat/ask') {
1622
+ const sessionId = String(payload.sessionId || '').trim();
1623
+ const query = String(payload.query || '').trim();
1624
+ if (!query) return safeJson(res, 400, { error: 'Missing query' });
1625
+
1626
+ const indexMatches = searchIndex(workspaceDir, query, { limit: 8 });
1627
+ const matches = indexMatches.length
1628
+ ? indexMatches
1629
+ : searchWorkspace(workspaceDir, query, { limit: 8 });
1630
+ if (!matches.length) {
1631
+ return safeJson(res, 200, { ok: true, sessionId, answer: 'Não encontrei registro', matches: [] });
1632
+ }
1633
+
1634
+ const lines = [];
1635
+ lines.push(`Encontrei ${matches.length} registro(s):`);
1636
+ for (const m of matches) {
1637
+ const parts = [];
1638
+ if (m.date) parts.push(`**${m.date}**`);
1639
+ if (m.file) parts.push('`' + m.file + '`');
1640
+ const prefix = parts.length ? parts.join(' — ') + ':' : '';
1641
+ const snippet = m.snippet ? String(m.snippet).trim() : '';
1642
+ lines.push(`- ${prefix} ${snippet}`);
1643
+ }
1644
+
1645
+ const answer = lines.join('\n');
1646
+ return safeJson(res, 200, { ok: true, sessionId, answer, matches });
1647
+ }
1648
+
1649
+ // Chat persistence (per session)
1650
+ if (req.url === '/api/chat/append') {
1651
+ const sessionId = String(payload.sessionId || '').trim();
1652
+ const role = String(payload.role || '').trim();
1653
+ const text = String(payload.text || '').trimEnd();
1654
+ const markdown = !!payload.markdown;
1655
+ const ts = typeof payload.ts === 'number' ? payload.ts : Date.now();
1656
+ if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
1657
+ if (!role) return safeJson(res, 400, { error: 'Missing role' });
1658
+ if (!text) return safeJson(res, 400, { error: 'Missing text' });
1659
+
1660
+ const d = isoDate();
1661
+ const base = path.join(workspaceDir, 'data', 'chat', d);
1662
+ ensureDir(base);
1663
+ const file = path.join(base, `${sessionId}.jsonl`);
1664
+ const item = { ts, role, markdown, text };
1665
+ fs.appendFileSync(file, JSON.stringify(item) + '\n', 'utf8');
1666
+ return safeJson(res, 200, { ok: true });
1667
+ }
1668
+
1669
+ if (req.url === '/api/chat/load') {
1670
+ const sessionId = String(payload.sessionId || '').trim();
1671
+ const d = String(payload.date || '').trim() || isoDate();
1672
+ if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
1673
+ const file = path.join(workspaceDir, 'data', 'chat', d, `${sessionId}.jsonl`);
1674
+ if (!exists(file)) return safeJson(res, 200, { ok: true, items: [] });
1675
+
1676
+ const rawText = fs.readFileSync(file, 'utf8');
1677
+ const lines = rawText.split(/\r?\n/).filter(Boolean);
1678
+ const items = [];
1679
+ for (const line of lines) {
1680
+ try {
1681
+ const obj = JSON.parse(line);
1682
+ if (!obj || typeof obj !== 'object') continue;
1683
+ items.push(obj);
1684
+ } catch {
1685
+ // ignore corrupt line
1686
+ }
1687
+ }
1688
+ return safeJson(res, 200, { ok: true, items });
1689
+ }
1690
+
1691
+ if (req.url === '/api/chat/export-obsidian') {
1692
+ const sessionId = String(payload.sessionId || '').trim();
1693
+ const d = String(payload.date || '').trim() || isoDate();
1694
+ if (!sessionId) return safeJson(res, 400, { error: 'Missing sessionId' });
1695
+ const file = path.join(workspaceDir, 'data', 'chat', d, `${sessionId}.jsonl`);
1696
+ if (!exists(file)) return safeJson(res, 404, { error: 'Chat not found' });
1697
+
1698
+ const rawText = fs.readFileSync(file, 'utf8');
1699
+ const lines = rawText.split(/\r?\n/).filter(Boolean);
1700
+ const items = [];
1701
+ for (const line of lines) {
1702
+ try {
1703
+ const obj = JSON.parse(line);
1704
+ if (!obj || typeof obj !== 'object') continue;
1705
+ items.push(obj);
1706
+ } catch {}
1707
+ }
1708
+
1709
+ const outDir = path.join(workspaceDir, 'docs', 'chat');
1710
+ ensureDir(outDir);
1711
+ const outName = `conversa-${d}-${sessionId}.md`;
1712
+ const outPath = path.join(outDir, outName);
1713
+
1714
+ const md = [];
1715
+ md.push('---');
1716
+ md.push(`type: chat`);
1717
+ md.push(`date: ${d}`);
1718
+ md.push(`session: ${sessionId}`);
1719
+ md.push('---');
1720
+ md.push('');
1721
+ md.push(`# Conversa - ${d}`);
1722
+ md.push('');
1723
+
1724
+ for (const it of items) {
1725
+ const when = (() => {
1726
+ try {
1727
+ const dt = new Date(Number(it.ts || 0));
1728
+ const hh = String(dt.getHours()).padStart(2, '0');
1729
+ const mm = String(dt.getMinutes()).padStart(2, '0');
1730
+ return `${hh}:${mm}`;
1731
+ } catch { return ''; }
1732
+ })();
1733
+ const who = it.role === 'user' ? 'Você' : 'FREYA';
1734
+ md.push(`## [${when}] ${who}`);
1735
+ md.push('');
1736
+ md.push(String(it.text || '').trimEnd());
1737
+ md.push('');
1738
+ }
1739
+
1740
+ fs.writeFileSync(outPath, md.join('\n') + '\n', 'utf8');
1741
+ return safeJson(res, 200, { ok: true, relPath: path.relative(workspaceDir, outPath).replace(/\\/g, '/') });
1742
+ }
1743
+
1608
1744
  if (req.url === '/api/tasks/list') {
1609
1745
  const limit = Math.max(1, Math.min(50, Number(payload.limit || 10)));
1610
1746
  const cat = payload.category ? String(payload.category).trim() : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",
@@ -11,7 +11,9 @@
11
11
  "status": "node scripts/generate-executive-report.js",
12
12
  "blockers": "node scripts/generate-blockers-report.js",
13
13
  "export-obsidian": "node scripts/export-obsidian.js",
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
+ "build-index": "node scripts/index/build-index.js",
15
+ "update-index": "node scripts/index/update-index.js",
16
+ "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-search-utils.js && node tests/unit/test-index-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"
15
17
  },
16
18
  "keywords": [],
17
19
  "author": "",