@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.
- package/cli/web-ui.js +136 -2
- package/cli/web.js +136 -0
- 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' ? '
|
|
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.
|
|
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
|
-
"
|
|
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": "",
|