@cccarv82/freya 2.7.1 → 2.8.2

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.
@@ -64,11 +64,8 @@ function runNpmInstall(workspaceDir) {
64
64
  env: { ...process.env }
65
65
  };
66
66
 
67
- if (process.platform === 'win32') {
68
- execSync('npm install --no-audit --no-fund', opts);
69
- } else {
70
- execSync('npm install --no-audit --no-fund', opts);
71
- }
67
+ // BUG-16: both branches were identical; collapsed to one line
68
+ execSync('npm install --no-audit --no-fund', opts);
72
69
  console.log('[FREYA] Dependencies installed successfully.');
73
70
  return true;
74
71
  } catch (err) {
package/cli/web.js CHANGED
@@ -8,7 +8,7 @@ const { spawn } = require('child_process');
8
8
  const { searchWorkspace } = require('../scripts/lib/search-utils');
9
9
  const { searchIndex } = require('../scripts/lib/index-utils');
10
10
  const { initWorkspace } = require('./init');
11
- const { defaultInstance: dl, ready } = require('../scripts/lib/DataLayer');
11
+ const { defaultInstance: dl, ready, configure: configureDataLayer } = require('../scripts/lib/DataLayer');
12
12
  const DataManager = require('../scripts/lib/DataManager');
13
13
 
14
14
  function readAppVersion() {
@@ -40,8 +40,22 @@ const CHAT_ID_PATTERNS = [
40
40
  ];
41
41
 
42
42
  function guessNpmCmd() {
43
- // We'll execute via cmd.exe on Windows for reliability.
44
- return process.platform === 'win32' ? 'npm' : 'npm';
43
+ // On Windows, use npm.cmd so child_process.spawn can locate the executable without shell.
44
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
45
+ }
46
+
47
+ // --- Module-level helpers used across multiple request handlers ---
48
+
49
+ function sha1(text) {
50
+ return crypto.createHash('sha1').update(String(text || ''), 'utf8').digest('hex');
51
+ }
52
+
53
+ function normalizeWhitespace(t) {
54
+ return String(t || '').replace(/\s+/g, ' ').trim();
55
+ }
56
+
57
+ function normalizeTextForKey(t) {
58
+ return normalizeWhitespace(t).toLowerCase();
45
59
  }
46
60
 
47
61
  function guessOpenCmd() {
@@ -250,6 +264,8 @@ function splitForDiscord(text, limit = 1900) {
250
264
  const cut2 = window.lastIndexOf(NL);
251
265
  if (cut > 400) end = i + cut;
252
266
  else if (cut2 > 600) end = i + cut2;
267
+ // BUG-24: guarantee forward progress to prevent infinite loop
268
+ if (end <= i) end = i + 1;
253
269
  const chunk = t.slice(i, end).trim();
254
270
  if (chunk) parts.push(chunk);
255
271
  i = end;
@@ -635,26 +651,38 @@ function normalizeWorkspaceDir(inputDir) {
635
651
  return d;
636
652
  }
637
653
 
638
- function readBody(req) {
654
+ function readBody(req, maxBytes = 4 * 1024 * 1024) {
655
+ // BUG-34: enforce a request body size limit to prevent memory exhaustion
639
656
  return new Promise((resolve, reject) => {
640
657
  const chunks = [];
641
- req.on('data', (c) => chunks.push(c));
658
+ let total = 0;
659
+ req.on('data', chunk => {
660
+ total += chunk.length;
661
+ if (total > maxBytes) {
662
+ req.destroy();
663
+ reject(new Error('Request body too large'));
664
+ return;
665
+ }
666
+ chunks.push(chunk);
667
+ });
642
668
  req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
643
669
  req.on('error', reject);
644
670
  });
645
671
  }
646
672
 
647
- function run(cmd, args, cwd) {
673
+ function run(cmd, args, cwd, extraEnv) {
648
674
  return new Promise((resolve) => {
649
675
  let child;
650
676
 
677
+ const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
678
+
651
679
  try {
652
680
  // On Windows, reliably execute CLI tools through cmd.exe.
653
681
  if (process.platform === 'win32' && (cmd === 'npx' || cmd === 'npm')) {
654
682
  const comspec = process.env.ComSpec || 'cmd.exe';
655
- child = spawn(comspec, ['/d', '/s', '/c', cmd, ...args], { cwd, shell: false, env: process.env });
683
+ child = spawn(comspec, ['/d', '/s', '/c', cmd, ...args], { cwd, shell: false, env });
656
684
  } else {
657
- child = spawn(cmd, args, { cwd, shell: false, env: process.env });
685
+ child = spawn(cmd, args, { cwd, shell: false, env });
658
686
  }
659
687
  } catch (e) {
660
688
  return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
@@ -1306,7 +1334,14 @@ function buildHtml(safeDefault, appVersion) {
1306
1334
  <label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
1307
1335
  <input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
1308
1336
  Publicação bonita (cards/embeds)
1337
+ </label>
1309
1338
 
1339
+ </div>
1340
+ </div>
1341
+ </div>
1342
+ </div>
1343
+ </div>
1344
+ </main>
1310
1345
  </div>
1311
1346
  </div>
1312
1347
  </div>
@@ -2080,13 +2115,30 @@ function getTimelineItems(workspaceDir) {
2080
2115
  }
2081
2116
  }
2082
2117
  }
2118
+
2119
+ // BUG-33: Prefer SQLite tasks (primary source) over legacy task-log.json
2120
+ const seenIds = new Set();
2121
+ if (dl.db) {
2122
+ try {
2123
+ const sqliteTasks = dl.db.prepare('SELECT id, description, project_slug, created_at, completed_at FROM tasks').all();
2124
+ for (const t of sqliteTasks) {
2125
+ seenIds.add(t.id);
2126
+ if (t.created_at) items.push({ kind: 'task', date: String(t.created_at).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.project_slug || '' });
2127
+ if (t.completed_at) items.push({ kind: 'task', date: String(t.completed_at).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.project_slug || '' });
2128
+ }
2129
+ } catch { /* db may not be ready yet, fall through to legacy */ }
2130
+ }
2131
+
2132
+ // Legacy JSON fallback for tasks not yet in SQLite
2083
2133
  const taskFile = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
2084
2134
  const taskDoc = readJsonOrNull(taskFile) || { tasks: [] };
2085
2135
  const tasks = Array.isArray(taskDoc.tasks) ? taskDoc.tasks : [];
2086
2136
  for (const t of tasks) {
2137
+ if (seenIds.has(t.id)) continue; // already from SQLite
2087
2138
  if (t.createdAt) items.push({ kind: 'task', date: String(t.createdAt).slice(0, 10), title: `Task criada: ${t.description || t.id}`, content: t.projectSlug || '' });
2088
2139
  if (t.completedAt) items.push({ kind: 'task', date: String(t.completedAt).slice(0, 10), title: `Task concluida: ${t.description || t.id}`, content: t.projectSlug || '' });
2089
2140
  }
2141
+
2090
2142
  items.sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));
2091
2143
  return items;
2092
2144
  }
@@ -2237,12 +2289,24 @@ function seedDevWorkspace(workspaceDir) {
2237
2289
  }
2238
2290
 
2239
2291
  async function cmdWeb({ port, dir, open, dev }) {
2240
- await ready;
2292
+ // Determine workspace directory first, before any DB access.
2293
+ const wsDir = normalizeWorkspaceDir(dir || './freya');
2294
+
2295
+ // Configure the DataLayer singleton to point at the workspace's SQLite file
2296
+ // BEFORE awaiting `ready`. `ready` was started with the default path at
2297
+ // module-load time; configureDataLayer closes that connection and reopens
2298
+ // against the correct file so no code ever uses the wrong database.
2299
+ try {
2300
+ await configureDataLayer(wsDir);
2301
+ } catch (e) {
2302
+ console.error('[FREYA] Warning: Failed to configure DataLayer for workspace:', e.message || String(e));
2303
+ // Non-fatal: fall back to default DB path.
2304
+ await ready;
2305
+ }
2241
2306
 
2242
2307
  // Auto-update workspace scripts/deps if Freya version changed
2243
2308
  try {
2244
2309
  const { autoUpdate } = require('./auto-update');
2245
- const wsDir = normalizeWorkspaceDir(dir || './freya');
2246
2310
  await autoUpdate(wsDir);
2247
2311
  } catch { /* non-fatal */ }
2248
2312
 
@@ -2254,9 +2318,12 @@ async function cmdWeb({ port, dir, open, dev }) {
2254
2318
  try {
2255
2319
  if (!req.url) return safeJson(res, 404, { error: 'Not found' });
2256
2320
 
2321
+ // BUG-29: helper to replace hardcoded port placeholder with the actual listen port
2322
+ const injectPort = (body) => body.replace(/127\.0\.0\.1:3872/g, `127.0.0.1:${port}`);
2323
+
2257
2324
  if (req.method === 'GET' && req.url === '/') {
2258
2325
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2259
- const body = html(dir || './freya');
2326
+ const body = injectPort(html(dir || './freya'));
2260
2327
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2261
2328
  res.end(body);
2262
2329
  return;
@@ -2264,7 +2331,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2264
2331
 
2265
2332
  if (req.method === 'GET' && req.url === '/reports') {
2266
2333
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2267
- const body = reportsHtml(dir || './freya');
2334
+ const body = injectPort(reportsHtml(dir || './freya'));
2268
2335
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2269
2336
  res.end(body);
2270
2337
  return;
@@ -2272,7 +2339,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2272
2339
 
2273
2340
  if (req.method === 'GET' && req.url === '/companion') {
2274
2341
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2275
- const body = companionHtml(dir || './freya');
2342
+ const body = injectPort(companionHtml(dir || './freya'));
2276
2343
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2277
2344
  res.end(body);
2278
2345
  return;
@@ -2280,7 +2347,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2280
2347
 
2281
2348
  if (req.method === 'GET' && req.url === '/projects') {
2282
2349
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2283
- const body = projectsHtml(dir || './freya');
2350
+ const body = injectPort(projectsHtml(dir || './freya'));
2284
2351
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2285
2352
  res.end(body);
2286
2353
  return;
@@ -2288,7 +2355,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2288
2355
 
2289
2356
  if (req.method === 'GET' && req.url === '/graph') {
2290
2357
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2291
- const body = buildGraphHtml(dir || './freya', APP_VERSION);
2358
+ const body = injectPort(buildGraphHtml(dir || './freya', APP_VERSION));
2292
2359
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2293
2360
  res.end(body);
2294
2361
  return;
@@ -2296,7 +2363,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2296
2363
 
2297
2364
  if (req.method === 'GET' && req.url === '/timeline') {
2298
2365
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2299
- const body = timelineHtml(dir || './freya');
2366
+ const body = injectPort(timelineHtml(dir || './freya'));
2300
2367
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2301
2368
  res.end(body);
2302
2369
  return;
@@ -2304,7 +2371,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2304
2371
 
2305
2372
  if (req.method === 'GET' && req.url === '/settings') {
2306
2373
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2307
- const body = settingsHtml(dir || './freya');
2374
+ const body = injectPort(settingsHtml(dir || './freya'));
2308
2375
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2309
2376
  res.end(body);
2310
2377
  return;
@@ -2312,7 +2379,7 @@ async function cmdWeb({ port, dir, open, dev }) {
2312
2379
 
2313
2380
  if (req.method === 'GET' && req.url === '/docs') {
2314
2381
  try { res.__freyaDebug.workspaceDir = normalizeWorkspaceDir(dir || './freya'); } catch { }
2315
- const body = docsHtml(dir || './freya');
2382
+ const body = injectPort(docsHtml(dir || './freya'));
2316
2383
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
2317
2384
  res.end(body);
2318
2385
  return;
@@ -2880,8 +2947,10 @@ async function cmdWeb({ port, dir, open, dev }) {
2880
2947
  const reportsDir = path.join(workspaceDir, 'docs', 'reports');
2881
2948
  const safeReportsDir = path.resolve(reportsDir);
2882
2949
  const safeFull = path.resolve(full);
2883
- if (!safeFull.startsWith(safeReportsDir + path.sep)) {
2884
- return safeJson(res, 400, { error: 'Invalid report path' });
2950
+ // BUG-07: use path.relative for traversal check (startsWith can be fooled on some OS)
2951
+ const rel2 = path.relative(safeReportsDir, safeFull);
2952
+ if (!rel2 || rel2.startsWith('..') || path.isAbsolute(rel2)) {
2953
+ return safeJson(res, 400, { error: 'Caminho de relatório inválido' });
2885
2954
  }
2886
2955
 
2887
2956
  const text = fs.readFileSync(safeFull, 'utf8');
@@ -3045,8 +3114,10 @@ async function cmdWeb({ port, dir, open, dev }) {
3045
3114
 
3046
3115
  // Best-effort: if Copilot CLI isn't available, return 200 with an explanatory plan
3047
3116
  // so the UI can show actionable next steps instead of hard-failing.
3117
+ // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3118
+ const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3048
3119
  try {
3049
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir);
3120
+ const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
3050
3121
  const out = (r.stdout + r.stderr).trim();
3051
3122
  if (r.code !== 0) {
3052
3123
  return safeJson(res, 200, {
@@ -3190,23 +3261,6 @@ async function cmdWeb({ port, dir, open, dev }) {
3190
3261
  if (!Array.isArray(actions) || actions.length === 0) {
3191
3262
  return safeJson(res, 400, { error: 'Plan has no actions[]' });
3192
3263
  }
3193
- function normalizeWhitespace(t) {
3194
- return String(t || '').replace(/\s+/g, ' ').trim();
3195
- }
3196
-
3197
- function normalizeTextForKey(t) {
3198
- return normalizeWhitespace(t).toLowerCase();
3199
- }
3200
-
3201
- function sha1(text) {
3202
- return crypto.createHash('sha1').update(String(text || ''), 'utf8').digest('hex');
3203
- }
3204
-
3205
- const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
3206
- const existingTaskKeys24h = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
3207
-
3208
- const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
3209
- const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
3210
3264
 
3211
3265
  const now = new Date().toISOString();
3212
3266
  const applyMode = String(payload.mode || 'all').trim();
@@ -3241,8 +3295,14 @@ async function cmdWeb({ port, dir, open, dev }) {
3241
3295
  const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
3242
3296
  const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
3243
3297
 
3244
- // Execute everything inside a transaction for the apply process
3298
+ // BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
3245
3299
  const applyTx = dl.db.transaction((actionsToApply) => {
3300
+ // Query for existing keys inside the transaction for atomicity
3301
+ const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
3302
+ const existingTaskKeys24h = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
3303
+ const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
3304
+ const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
3305
+
3246
3306
  for (const a of actionsToApply) {
3247
3307
  if (!a || typeof a !== 'object') continue;
3248
3308
  const type = String(a.type || '').trim();
@@ -3370,18 +3430,21 @@ async function cmdWeb({ port, dir, open, dev }) {
3370
3430
  }
3371
3431
 
3372
3432
  const npmCmd = guessNpmCmd();
3433
+ // Pass workspace dir to all child npm scripts so their DataLayer instances
3434
+ // resolve the same SQLite file as the web server process.
3435
+ const workspaceEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3373
3436
 
3374
3437
  if (req.url === '/api/health') {
3375
3438
  if (!looksLikeFreyaWorkspace(workspaceDir)) {
3376
3439
  return safeJson(res, 200, { ok: false, needsInit: true, error: 'Workspace not initialized' });
3377
3440
  }
3378
- const r = await run(npmCmd, ['run', 'health'], workspaceDir);
3441
+ const r = await run(npmCmd, ['run', 'health'], workspaceDir, workspaceEnv);
3379
3442
  const output = (r.stdout + r.stderr).trim();
3380
3443
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'health failed', output });
3381
3444
  }
3382
3445
 
3383
3446
  if (req.url === '/api/migrate') {
3384
- const r = await run(npmCmd, ['run', 'migrate'], workspaceDir);
3447
+ const r = await run(npmCmd, ['run', 'migrate'], workspaceDir, workspaceEnv);
3385
3448
  const output = (r.stdout + r.stderr).trim();
3386
3449
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { output } : { error: output || 'migrate failed', output });
3387
3450
  }
@@ -3389,13 +3452,13 @@ async function cmdWeb({ port, dir, open, dev }) {
3389
3452
 
3390
3453
 
3391
3454
  if (req.url === '/api/obsidian/export') {
3392
- const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir);
3455
+ const r = await run(npmCmd, ['run', 'export-obsidian'], workspaceDir, workspaceEnv);
3393
3456
  const out = (r.stdout + r.stderr).trim();
3394
3457
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'export failed', output: out });
3395
3458
  }
3396
3459
 
3397
3460
  if (req.url === '/api/index/rebuild') {
3398
- const r = await run(npmCmd, ['run', 'build-index'], workspaceDir);
3461
+ const r = await run(npmCmd, ['run', 'build-index'], workspaceDir, workspaceEnv);
3399
3462
  const out = (r.stdout + r.stderr).trim();
3400
3463
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
3401
3464
  }
@@ -3435,9 +3498,11 @@ async function cmdWeb({ port, dir, open, dev }) {
3435
3498
 
3436
3499
  const cmd = process.env.COPILOT_CMD || 'copilot';
3437
3500
 
3501
+ // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3502
+ const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3438
3503
  try {
3439
3504
  // Removed --allow-all-tools and --add-dir to force reliance on RAG context
3440
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir);
3505
+ const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir, oracleEnv);
3441
3506
  const out = (r.stdout + r.stderr).trim();
3442
3507
  if (r.code !== 0) {
3443
3508
  return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
@@ -3652,48 +3717,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3652
3717
  return safeJson(res, 200, { ok: true, items, stats: { pendingTasks, openBlockers, reportsToday, reportsTotal: reports.length } });
3653
3718
  }
3654
3719
 
3655
- if (req.url === '/api/incidents/resolve') {
3656
- const title = payload.title;
3657
- const index = Number.isInteger(payload.index) ? payload.index : null;
3658
- if (!title) return safeJson(res, 400, { error: 'Missing title' });
3659
- const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
3660
- if (!exists(p)) return safeJson(res, 404, { error: 'Incident index not found' });
3661
- const md = fs.readFileSync(p, 'utf8');
3662
- const updated = resolveIncidentInMarkdown(md, title, index);
3663
- if (!updated) return safeJson(res, 404, { error: 'Incident not found' });
3664
- fs.writeFileSync(p, updated, 'utf8');
3665
- return safeJson(res, 200, { ok: true });
3666
- }
3667
-
3668
- if (req.url === '/api/incidents') {
3669
- const p = path.join(workspaceDir, 'docs', 'reports', 'fidelizacao-incident-index.md');
3670
- if (!exists(p)) return safeJson(res, 200, { ok: true, markdown: '' });
3671
- const md = fs.readFileSync(p, 'utf8');
3672
- return safeJson(res, 200, { ok: true, markdown: md });
3673
- }
3674
-
3675
- if (req.url === '/api/tasks/heatmap') {
3676
- const file = path.join(workspaceDir, 'data', 'tasks', 'task-log.json');
3677
- const doc = readJsonOrNull(file) || { tasks: [] };
3678
- const tasks = Array.isArray(doc.tasks) ? doc.tasks : [];
3679
- const map = {};
3680
- const priorityRank = { high: 3, medium: 2, low: 1, '': 0 };
3681
- for (const t of tasks) {
3682
- const slug = t.projectSlug || 'unassigned';
3683
- if (!map[slug]) map[slug] = { total: 0, pending: 0, completed: 0, priority: '' };
3684
- map[slug].total++;
3685
- if (t.status === 'COMPLETED') map[slug].completed++; else map[slug].pending++;
3686
- const p = normalizePriority(t.priority || t.severity);
3687
- if (priorityRank[p] > priorityRank[map[slug].priority || '']) map[slug].priority = p;
3688
- }
3689
- const items = Object.entries(map).map(([slug, v]) => {
3690
- const statusPath = path.join(workspaceDir, 'data', 'Clients', slug, 'status.json');
3691
- const linkRel = exists(statusPath) ? path.relative(workspaceDir, statusPath).replace(/\\/g, '/') : '';
3692
- return { slug, ...v, linkRel };
3693
- });
3694
- items.sort((a, b) => b.total - a.total);
3695
- return safeJson(res, 200, { ok: true, items });
3696
- }
3720
+ // BUG-12: duplicate handlers for /api/incidents/resolve, /api/incidents,
3721
+ // and /api/tasks/heatmap were removed here — originals remain earlier in the file.
3697
3722
 
3698
3723
  if (req.url === '/api/blockers/summary') {
3699
3724
  const open = dl.db.prepare(`
@@ -3808,7 +3833,15 @@ async function cmdWeb({ port, dir, open, dev }) {
3808
3833
  const script = payload.script;
3809
3834
  if (!script) return safeJson(res, 400, { error: 'Missing script' });
3810
3835
 
3811
- const r = await run(npmCmd, ['run', script], workspaceDir);
3836
+ // BUG-15: Whitelist allowed report scripts to prevent arbitrary npm run execution
3837
+ const ALLOWED_REPORT_SCRIPTS = new Set(['blockers', 'sm-weekly', 'status', 'daily', 'report', 'build-index', 'update-index', 'export-obsidian']);
3838
+ if (!ALLOWED_REPORT_SCRIPTS.has(script)) {
3839
+ return safeJson(res, 400, { error: 'Script não permitido: ' + script });
3840
+ }
3841
+
3842
+ // Pass FREYA_WORKSPACE_DIR so the workspace's DataLayer uses the same
3843
+ // SQLite file as the web server process (fixes the two-database split).
3844
+ const r = await run(npmCmd, ['run', script], workspaceDir, workspaceEnv);
3812
3845
  const out = (r.stdout + r.stderr).trim();
3813
3846
 
3814
3847
  const reportsDir = path.join(workspaceDir, 'docs', 'reports');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.7.1",
3
+ "version": "2.8.2",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",
@@ -5,7 +5,12 @@ const { toIsoDate, safeParseToMs, isWithinRange } = require('./lib/date-utils');
5
5
 
6
6
  const DataManager = require('./lib/DataManager');
7
7
  const { ready } = require('./lib/DataLayer');
8
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
8
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
9
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
10
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
11
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
12
+
13
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
9
14
 
10
15
  const SEVERITY_ORDER = {
11
16
  CRITICAL: 0,
@@ -4,9 +4,14 @@ const path = require('path');
4
4
  const DataManager = require('./lib/DataManager');
5
5
  const { ready } = require('./lib/DataLayer');
6
6
 
7
- const DATA_DIR = path.join(__dirname, '../data');
8
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
9
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
7
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
8
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
9
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
10
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
11
+
12
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
13
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
14
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
10
15
 
11
16
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
12
17
 
@@ -24,9 +29,10 @@ async function generateDailySummary() {
24
29
  let summary = "";
25
30
 
26
31
  // 1. Ontem (Completed < 24h)
32
+ // BUG-09: use completedAt (not createdAt) to decide if a task appears in "yesterday"
27
33
  const completedRecently = tasks.filter(t => {
28
34
  if (t.status !== 'COMPLETED') return false;
29
- const ms = dm.getCreatedAt(t) || Date.parse(t.completedAt || t.completed_at || ''); // Use native since no safeParse helper here
35
+ const ms = dm.getResolvedAt(t);
30
36
  if (!Number.isFinite(ms)) return false;
31
37
  return ms >= ms24h && ms <= now.getTime();
32
38
  });
@@ -13,10 +13,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
13
  const DataManager = require('./lib/DataManager');
14
14
  const { ready } = require('./lib/DataLayer');
15
15
 
16
- // --- Configuration ---
17
- const DATA_DIR = path.join(__dirname, '../data');
18
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
19
- const OUTPUT_DIR = path.join(__dirname, '../docs/reports');
16
+ // --- Configuration (BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname) ---
17
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
18
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
19
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
20
+
21
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
22
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
23
+ const OUTPUT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
20
24
 
21
25
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
22
26
 
@@ -5,9 +5,14 @@ const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
5
5
  const DataManager = require('./lib/DataManager');
6
6
  const { ready } = require('./lib/DataLayer');
7
7
 
8
- const DATA_DIR = path.join(__dirname, '../data');
9
- const LOGS_DIR = path.join(__dirname, '../logs/daily');
10
- const REPORT_DIR = path.join(__dirname, '../docs/reports');
8
+ // BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname
9
+ const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
10
+ ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
11
+ : path.join(__dirname, '..'); // fallback: scripts/ is one level below repo root
12
+
13
+ const DATA_DIR = path.join(WORKSPACE_DIR, 'data');
14
+ const LOGS_DIR = path.join(WORKSPACE_DIR, 'logs', 'daily');
15
+ const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
11
16
 
12
17
  const dm = new DataManager(DATA_DIR, LOGS_DIR);
13
18
 
@@ -109,7 +114,8 @@ async function generate() {
109
114
  projectsWithUpdates.forEach(p => {
110
115
  md += `### ${p.client} / ${p.project}\n`;
111
116
  if (p.currentStatus) md += `**Status:** ${p.currentStatus}\n`;
112
- p.recent.forEach(e => {
117
+ const projectEvents = p.events || p.recent || [];
118
+ projectEvents.forEach(e => {
113
119
  md += `- [${e.date || e.timestamp}] ${e.content || ''}\n`;
114
120
  });
115
121
  md += `\n`;