@cccarv82/freya 1.0.20 → 1.0.22

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 +63 -9
  2. package/cli/web.js +77 -11
  3. package/package.json +1 -1
package/cli/web-ui.js CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  (function () {
6
6
  const $ = (id) => document.getElementById(id);
7
- const state = { lastReportPath: null, lastText: '', reports: [], selectedReport: null, lastPlan: '', lastApplied: null };
7
+ undefined
8
8
 
9
9
  function applyTheme(theme) {
10
10
  document.documentElement.setAttribute('data-theme', theme);
@@ -187,9 +187,23 @@
187
187
 
188
188
  function saveLocal() {
189
189
  localStorage.setItem('freya.dir', $('dir').value);
190
+ try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
191
+ try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
190
192
  }
191
193
 
192
194
  function loadLocal() {
195
+ try {
196
+ const v = localStorage.getItem('freya.autoApply');
197
+ if (v !== null) state.autoApply = v === '1';
198
+ const cb = $('autoApply');
199
+ if (cb) cb.checked = !!state.autoApply;
200
+
201
+ const v2 = localStorage.getItem('freya.autoRunReports');
202
+ if (v2 !== null) state.autoRunReports = v2 === '1';
203
+ const cb2 = $('autoRunReports');
204
+ if (cb2) cb2.checked = !!state.autoRunReports;
205
+ } catch {}
206
+
193
207
  const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
194
208
  ? window.__FREYA_DEFAULT_DIR
195
209
  : (localStorage.getItem('freya.dir') || './freya');
@@ -274,13 +288,22 @@
274
288
  }
275
289
  }
276
290
 
277
- async function refreshReports() {
291
+ async function refreshReports(opts = {}) {
278
292
  try {
279
293
  const r = await api('/api/reports/list', { dir: dirOrDefault() });
280
294
  state.reports = (r.reports || []).slice(0, 50);
281
295
  renderReportsList();
282
- if (!state.selectedReport && state.reports && state.reports[0]) {
283
- await selectReport(state.reports[0]);
296
+
297
+ const latest = state.reports && state.reports[0] ? state.reports[0] : null;
298
+ if (!latest) return;
299
+
300
+ if (opts.selectLatest) {
301
+ await selectReport(latest);
302
+ return;
303
+ }
304
+
305
+ if (!state.selectedReport) {
306
+ await selectReport(latest);
284
307
  }
285
308
  } catch (e) {
286
309
  // ignore
@@ -312,7 +335,7 @@
312
335
  const r = await api('/api/init', { dir: dirOrDefault() });
313
336
  setOut(r.output);
314
337
  setLast(null);
315
- await refreshReports();
338
+ await refreshReports({ selectLatest: true });
316
339
  // Auto health after init
317
340
  try { await doHealth(); } catch {}
318
341
  setPill('ok', 'init ok');
@@ -331,7 +354,7 @@
331
354
  const r = await api('/api/update', { dir: dirOrDefault() });
332
355
  setOut(r.output);
333
356
  setLast(null);
334
- await refreshReports();
357
+ await refreshReports({ selectLatest: true });
335
358
  // Auto health after update
336
359
  try { await doHealth(); } catch {}
337
360
  setPill('ok', 'update ok');
@@ -383,7 +406,7 @@
383
406
  setOut(r.output);
384
407
  setLast(r.reportPath || null);
385
408
  if (r.reportText) state.lastText = r.reportText;
386
- await refreshReports();
409
+ await refreshReports({ selectLatest: true });
387
410
  setPill('ok', name + ' ok');
388
411
  } catch (e) {
389
412
  setPill('err', name + ' failed');
@@ -413,10 +436,27 @@
413
436
  try {
414
437
  saveLocal();
415
438
  if (!state.lastText) throw new Error('Gere um relatório primeiro.');
439
+
440
+ // quick local warning (server also enforces)
441
+ const secretHints = [];
442
+ if (/ghp_[A-Za-z0-9]{20,}/.test(state.lastText)) secretHints.push('GitHub token');
443
+ if (/github_pat_[A-Za-z0-9_]{20,}/.test(state.lastText)) secretHints.push('GitHub fine-grained token');
444
+ if (/-----BEGIN [A-Z ]+PRIVATE KEY-----/.test(state.lastText)) secretHints.push('Private key');
445
+ if (/xox[baprs]-[A-Za-z0-9-]{10,}/.test(state.lastText)) secretHints.push('Slack token');
446
+ if (/AKIA[0-9A-Z]{16}/.test(state.lastText)) secretHints.push('AWS key');
447
+
448
+ const msg = secretHints.length
449
+ ? 'ATENÇÃO: possível segredo detectado (' + secretHints.join(', ') + ').\n\nPublicar mesmo assim? (o Freya vai mascarar automaticamente)'
450
+ : 'Publicar o relatório selecionado?';
451
+
452
+ const ok = confirm(msg);
453
+ const ok = confirm('Publicar o relatório selecionado?');
454
+ if (!ok) return;
455
+
416
456
  const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
417
457
  if (!webhookUrl) throw new Error('Configure o webhook antes.');
418
458
  setPill('run', 'publish…');
419
- await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks' });
459
+ await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks', allowSecrets: true });
420
460
  setPill('ok', 'published');
421
461
  } catch (e) {
422
462
  setPill('err', 'publish failed');
@@ -443,6 +483,18 @@
443
483
  }
444
484
  }
445
485
 
486
+ function toggleAutoRunReports() {
487
+ const cb = $('autoRunReports');
488
+ state.autoRunReports = cb ? !!cb.checked : false;
489
+ try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
490
+ }
491
+
492
+ function toggleAutoApply() {
493
+ const cb = $('autoApply');
494
+ state.autoApply = cb ? !!cb.checked : true;
495
+ try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
496
+ }
497
+
446
498
  async function saveAndPlan() {
447
499
  try {
448
500
  const ta = $('inboxText');
@@ -500,7 +552,7 @@
500
552
  }
501
553
 
502
554
  setOut(out);
503
- await refreshReports();
555
+ await refreshReports({ selectLatest: true });
504
556
  setPill('ok', 'done');
505
557
  setTimeout(() => setPill('ok', 'idle'), 800);
506
558
  } catch (e) {
@@ -594,6 +646,8 @@
594
646
  window.toggleTheme = toggleTheme;
595
647
  window.saveInbox = saveInbox;
596
648
  window.saveAndPlan = saveAndPlan;
649
+ window.toggleAutoApply = toggleAutoApply;
650
+ window.toggleAutoRunReports = toggleAutoRunReports;
597
651
  window.applyPlan = applyPlan;
598
652
  window.runSuggestedReports = runSuggestedReports;
599
653
  })();
package/cli/web.js CHANGED
@@ -228,6 +228,44 @@ function escapeJsonControlChars(jsonText) {
228
228
  return out.join('');
229
229
  }
230
230
 
231
+ function scanSecrets(text) {
232
+ const t = String(text || '');
233
+ const findings = [];
234
+
235
+ const rules = [
236
+ { name: 'GitHub token (ghp_)', re: /ghp_[A-Za-z0-9]{20,}/g },
237
+ { name: 'GitHub fine-grained token (github_pat_)', re: /github_pat_[A-Za-z0-9_]{20,}/g },
238
+ { name: 'Slack token (xox*)', re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
239
+ { name: 'AWS Access Key (AKIA)', re: /AKIA[0-9A-Z]{16}/g },
240
+ { name: 'Private key block', re: /-----BEGIN [A-Z ]+PRIVATE KEY-----/g },
241
+ { name: 'Discord webhook URL', re: /https?:\/\/(?:canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[^\s]+/g },
242
+ ];
243
+
244
+ for (const r of rules) {
245
+ const m = t.match(r.re);
246
+ if (m && m.length) findings.push({ rule: r.name, count: m.length });
247
+ }
248
+
249
+ // Basic heuristic: long high-entropy strings
250
+ const long = t.match(/[A-Za-z0-9+\/=]{60,}/g);
251
+ if (long && long.length) findings.push({ rule: 'High-entropy blob (heuristic)', count: long.length });
252
+
253
+ return findings;
254
+ }
255
+
256
+ function redactSecrets(text) {
257
+ let out = String(text || '');
258
+ const replacements = [
259
+ /ghp_[A-Za-z0-9]{20,}/g,
260
+ /github_pat_[A-Za-z0-9_]{20,}/g,
261
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
262
+ /AKIA[0-9A-Z]{16}/g,
263
+ /-----BEGIN [A-Z ]+PRIVATE KEY-----[sS]*?-----END [A-Z ]+PRIVATE KEY-----/g,
264
+ ];
265
+ for (const re of replacements) out = out.replace(re, '[REDACTED]');
266
+ return out;
267
+ }
268
+
231
269
  async function publishRobust(webhookUrl, text, opts = {}) {
232
270
  const u = new URL(webhookUrl);
233
271
  const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
@@ -422,7 +460,19 @@ function buildHtml(safeDefault) {
422
460
  <button class="btn primary sideBtn" onclick="saveAndPlan()">Save + Process (Agents)</button>
423
461
  <button class="btn sideBtn" onclick="runSuggestedReports()">Run suggested reports</button>
424
462
  </div>
425
- <div class="help">Save+Process gera um plano (draft). Apply plan cria tasks/blockers. Run suggested reports executa os reports recomendados (daily/status/sm-weekly/blockers).</div>
463
+
464
+ <div style="height:10px"></div>
465
+ <label style="display:flex; align-items:center; gap:10px; user-select:none">
466
+ <input id="autoApply" type="checkbox" checked style="width:auto" onchange="toggleAutoApply()" />
467
+ Auto-apply plan
468
+ </label>
469
+ <div class="help">Quando ligado, o Save+Process já aplica tasks/blockers automaticamente.</div>
470
+
471
+ <label style="display:flex; align-items:center; gap:10px; user-select:none; margin-top:10px">
472
+ <input id="autoRunReports" type="checkbox" style="width:auto" onchange="toggleAutoRunReports()" />
473
+ Auto-run suggested reports
474
+ </label>
475
+ <div class="help">Quando ligado, após aplicar o plano, ele também roda os reports sugeridos automaticamente.</div>
426
476
  </div>
427
477
  </aside>
428
478
 
@@ -908,7 +958,7 @@ async function cmdWeb({ port, dir, open, dev }) {
908
958
  const type = String(a.type || '').trim();
909
959
 
910
960
  if (type === 'create_task') {
911
- const description = String(a.description || '').trim();
961
+ const description = normalizeWhitespace(a.description);
912
962
  const category = String(a.category || '').trim();
913
963
  const priorityRaw = String(a.priority || '').trim().toLowerCase();
914
964
  const priority = (priorityRaw === 'high' || priorityRaw === 'medium' || priorityRaw === 'low') ? priorityRaw : undefined;
@@ -918,8 +968,8 @@ async function cmdWeb({ port, dir, open, dev }) {
918
968
  }
919
969
 
920
970
  if (type === 'create_blocker') {
921
- const title = String(a.title || '').trim();
922
- const notes = String(a.notes || '').trim();
971
+ const title = normalizeWhitespace(a.title);
972
+ const notes = normalizeWhitespace(a.notes);
923
973
  let severity = String(a.severity || '').trim().toUpperCase();
924
974
  if (!validSev.has(severity)) {
925
975
  if (severity.includes('CRIT')) severity = 'CRITICAL';
@@ -1014,8 +1064,12 @@ async function cmdWeb({ port, dir, open, dev }) {
1014
1064
  if (!Array.isArray(blockerLog.blockers)) blockerLog.blockers = [];
1015
1065
  if (typeof blockerLog.schemaVersion !== 'number') blockerLog.schemaVersion = 1;
1016
1066
 
1067
+ function normalizeWhitespace(t) {
1068
+ return String(t || '').replace(/\s+/g, ' ').trim();
1069
+ }
1070
+
1017
1071
  function normalizeTextForKey(t) {
1018
- return String(t || '').toLowerCase().replace(/\s+/g, ' ').trim();
1072
+ return normalizeWhitespace(t).toLowerCase();
1019
1073
  }
1020
1074
 
1021
1075
  function sha1(text) {
@@ -1046,7 +1100,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1046
1100
 
1047
1101
  const now = new Date().toISOString();
1048
1102
  const applyMode = String(payload.mode || 'all').trim();
1049
- undefined
1103
+ const applied = { tasks: 0, blockers: 0, tasksSkipped: 0, blockersSkipped: 0, reportsSuggested: [], oracleQueries: [], mode: applyMode };
1050
1104
 
1051
1105
  function makeId(prefix) {
1052
1106
  const rand = Math.random().toString(16).slice(2, 8);
@@ -1079,7 +1133,7 @@ async function cmdWeb({ port, dir, open, dev }) {
1079
1133
 
1080
1134
  if (type === 'create_task') {
1081
1135
  if (applyMode !== 'all' && applyMode !== 'tasks') continue;
1082
- const description = String(a.description || '').trim();
1136
+ const description = normalizeWhitespace(a.description);
1083
1137
  if (!description) continue;
1084
1138
  const key = sha1(normalizeTextForKey(description));
1085
1139
  if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
@@ -1100,10 +1154,10 @@ async function cmdWeb({ port, dir, open, dev }) {
1100
1154
 
1101
1155
  if (type === 'create_blocker') {
1102
1156
  if (applyMode !== 'all' && applyMode !== 'blockers') continue;
1103
- const title = String(a.title || '').trim();
1157
+ const title = normalizeWhitespace(a.title);
1104
1158
  const key = sha1(normalizeTextForKey(title));
1105
1159
  if (existingBlockerKeys24h.has(key)) { applied.blockersSkipped++; continue; }
1106
- const notes = String(a.notes || '').trim();
1160
+ const notes = normalizeWhitespace(a.notes);
1107
1161
  if (!title) continue;
1108
1162
  const severity = normSeverity(a.severity);
1109
1163
  const blocker = {
@@ -1219,12 +1273,24 @@ async function cmdWeb({ port, dir, open, dev }) {
1219
1273
  const webhookUrl = payload.webhookUrl;
1220
1274
  const text = payload.text;
1221
1275
  const mode = payload.mode || 'chunks';
1276
+ const allowSecrets = !!payload.allowSecrets;
1222
1277
  if (!webhookUrl) return safeJson(res, 400, { error: 'Missing webhookUrl' });
1223
1278
  if (!text) return safeJson(res, 400, { error: 'Missing text' });
1224
1279
 
1280
+ const findings = scanSecrets(text);
1281
+ if (findings.length && !allowSecrets) {
1282
+ return safeJson(res, 400, {
1283
+ error: 'Potential secrets detected. Refusing to publish.',
1284
+ details: JSON.stringify(findings, null, 2),
1285
+ hint: 'Remova/mascare tokens ou confirme a publicação mesmo assim.'
1286
+ });
1287
+ }
1288
+
1289
+ const safeText = findings.length ? redactSecrets(text) : text;
1290
+
1225
1291
  try {
1226
- const result = await publishRobust(webhookUrl, text, { mode });
1227
- return safeJson(res, 200, result);
1292
+ const result = await publishRobust(webhookUrl, safeText, { mode });
1293
+ return safeJson(res, 200, { ...result, redacted: findings.length > 0, findings });
1228
1294
  } catch (e) {
1229
1295
  return safeJson(res, 400, { error: e.message || String(e) });
1230
1296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js",