@cccarv82/freya 3.2.0 → 3.3.0

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 CHANGED
@@ -163,6 +163,45 @@
163
163
  return html;
164
164
  }
165
165
 
166
+ // Merge multiple plan outputs from batch processing into a single plan JSON.
167
+ // Each plan may be a JSON string with { actions: [...] } or raw text.
168
+ function mergeBatchPlans(plans) {
169
+ var merged = [];
170
+ for (var pi = 0; pi < plans.length; pi++) {
171
+ var raw = String(plans[pi] || '').trim();
172
+ if (!raw) continue;
173
+ try {
174
+ // Extract first JSON object from the plan text
175
+ var start = raw.indexOf('{');
176
+ if (start !== -1) {
177
+ var depth = 0, inStr = false, esc = false, jsonStr = null;
178
+ for (var i = start; i < raw.length; i++) {
179
+ var ch = raw[i];
180
+ if (esc) { esc = false; continue; }
181
+ if (ch === '\\') { esc = true; continue; }
182
+ if (ch === '"') { inStr = !inStr; continue; }
183
+ if (inStr) continue;
184
+ if (ch === '{') depth++;
185
+ if (ch === '}') { depth--; if (depth === 0) { jsonStr = raw.slice(start, i + 1); break; } }
186
+ }
187
+ if (jsonStr) {
188
+ var obj = JSON.parse(jsonStr);
189
+ var actions = Array.isArray(obj.actions) ? obj.actions : [];
190
+ for (var ai = 0; ai < actions.length; ai++) merged.push(actions[ai]);
191
+ continue;
192
+ }
193
+ }
194
+ } catch (_) { /* fall through — keep raw text */ }
195
+ // If not parseable, keep as-is (will show raw in preview)
196
+ if (!merged._raw) merged._raw = '';
197
+ merged._raw += raw + '\n';
198
+ }
199
+ // Return combined JSON string
200
+ var result = JSON.stringify({ actions: merged });
201
+ if (merged._raw) result += '\n\n' + merged._raw;
202
+ return result;
203
+ }
204
+
166
205
  function formatPlanForDisplay(rawPlan) {
167
206
  var text = String(rawPlan || '');
168
207
  if (!text) return null;
@@ -2383,12 +2422,57 @@
2383
2422
 
2384
2423
  setPill('run', 'salvando…');
2385
2424
  var inboxPayload = { dir: dirOrDefault(), text };
2386
- if (pendingImg) inboxPayload.imagePath = 'data/attachments/' + pendingImg.filename;
2425
+ var savedImagePath = pendingImg ? 'data/attachments/' + pendingImg.filename : null;
2426
+ if (savedImagePath) inboxPayload.imagePath = savedImagePath;
2387
2427
  state.pendingImage = null;
2388
2428
  await api('/api/inbox/add', inboxPayload);
2389
2429
 
2390
- setPill('run', 'processando…');
2391
- const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
2430
+ // --- Chunked processing for large inputs (ENAMETOOLONG fix) ---
2431
+ var CHUNK_THRESHOLD = 20000; // 20KB if text is larger, split into batches
2432
+ var CHUNK_SIZE = 15000; // ~15KB per chunk (leave headroom for rules text)
2433
+ var chunks = [];
2434
+ if (text.length > CHUNK_THRESHOLD) {
2435
+ // Split at paragraph boundaries (\n\n), fall back to newlines, then hard-split
2436
+ var remaining = text;
2437
+ while (remaining.length > CHUNK_SIZE) {
2438
+ var cut = remaining.lastIndexOf('\n\n', CHUNK_SIZE);
2439
+ if (cut < CHUNK_SIZE * 0.3) cut = remaining.lastIndexOf('\n', CHUNK_SIZE);
2440
+ if (cut < CHUNK_SIZE * 0.3) cut = CHUNK_SIZE; // hard cut
2441
+ chunks.push(remaining.slice(0, cut).trim());
2442
+ remaining = remaining.slice(cut).trim();
2443
+ }
2444
+ if (remaining) chunks.push(remaining);
2445
+ } else {
2446
+ chunks = [text];
2447
+ }
2448
+
2449
+ var allPlans = [];
2450
+ var anyOk = false;
2451
+ var lastR = null;
2452
+
2453
+ for (var ci = 0; ci < chunks.length; ci++) {
2454
+ if (chunks.length > 1) {
2455
+ setPill('run', 'batch ' + (ci + 1) + '/' + chunks.length + '…');
2456
+ } else {
2457
+ setPill('run', 'processando…');
2458
+ }
2459
+ var planPayload = { dir: dirOrDefault(), text: chunks[ci] };
2460
+ // Send image path only with the first chunk
2461
+ if (ci === 0 && savedImagePath) planPayload.imagePath = savedImagePath;
2462
+ var chunkR = await api('/api/agents/plan', planPayload);
2463
+ if (chunkR.ok !== false) anyOk = true;
2464
+ if (chunkR.plan) allPlans.push(chunkR.plan);
2465
+ lastR = chunkR;
2466
+ }
2467
+
2468
+ // Merge plans: combine JSON action arrays from all chunks
2469
+ var r;
2470
+ if (chunks.length > 1 && anyOk) {
2471
+ var mergedPlan = mergeBatchPlans(allPlans);
2472
+ r = { ok: true, plan: mergedPlan };
2473
+ } else {
2474
+ r = lastR || { ok: false, plan: '' };
2475
+ }
2392
2476
 
2393
2477
  // Remove typing indicator
2394
2478
  var typingEl = $(typingId);
@@ -2397,9 +2481,10 @@
2397
2481
  state.lastPlan = r.plan || '';
2398
2482
 
2399
2483
  // Show plan output in Preview panel
2484
+ var batchNote = chunks.length > 1 ? '> 📦 Processado em **' + chunks.length + ' batches** (input grande)\n\n' : '';
2400
2485
  const header = r.ok === false ? '## Agent Plan (planner unavailable)\n\n' : '## Agent Plan (draft)\n\n';
2401
2486
  const formatted = r.ok !== false ? formatPlanForDisplay(r.plan) : null;
2402
- const planOut = header + (formatted || r.plan || '');
2487
+ const planOut = header + batchNote + (formatted || r.plan || '');
2403
2488
  setOut(planOut);
2404
2489
  chatAppend('assistant', planOut, { markdown: true });
2405
2490
  ta.value = '';
package/cli/web.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const http = require('http');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const os = require('os');
6
7
  const crypto = require('crypto');
7
8
  const { spawn } = require('child_process');
8
9
  const { searchWorkspace } = require('../scripts/lib/search-utils');
@@ -628,7 +629,7 @@ function readBody(req, maxBytes = 4 * 1024 * 1024) {
628
629
  });
629
630
  }
630
631
 
631
- function run(cmd, args, cwd, extraEnv) {
632
+ function run(cmd, args, cwd, extraEnv, stdinData) {
632
633
  return new Promise((resolve) => {
633
634
  let child;
634
635
 
@@ -646,6 +647,12 @@ function run(cmd, args, cwd, extraEnv) {
646
647
  return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
647
648
  }
648
649
 
650
+ // Pipe data to stdin if provided (avoids ENAMETOOLONG for large prompts)
651
+ if (stdinData && child.stdin) {
652
+ child.stdin.write(stdinData);
653
+ child.stdin.end();
654
+ }
655
+
649
656
  let stdout = '';
650
657
  let stderr = '';
651
658
 
@@ -3272,6 +3279,7 @@ async function cmdWeb({ port, dir, open, dev }) {
3272
3279
 
3273
3280
  if (req.url === '/api/agents/plan') {
3274
3281
  const text = String(payload.text || '').trim();
3282
+ const planImagePath = payload.imagePath ? String(payload.imagePath).trim() : null;
3275
3283
  if (!text) return safeJson(res, 400, { error: 'Missing text' });
3276
3284
 
3277
3285
  // Build planner prompt from agent rules.
@@ -3293,6 +3301,17 @@ async function cmdWeb({ port, dir, open, dev }) {
3293
3301
  return `\n\n---\nFILE: ${rel}\n---\n` + fs.readFileSync(p, 'utf8');
3294
3302
  }).join('');
3295
3303
 
3304
+ // Build image context for the prompt (Copilot reads files via --allow-all-tools)
3305
+ let planImageContext = '';
3306
+ let planImageDir = null;
3307
+ if (planImagePath) {
3308
+ const absImg = path.isAbsolute(planImagePath) ? planImagePath : path.join(workspaceDir, planImagePath);
3309
+ if (exists(absImg)) {
3310
+ planImageContext = `\n\n[IMAGEM ANEXADA]\nO usuário anexou uma imagem. Leia e analise o arquivo de imagem localizado em: ${absImg}\nInclua o conteúdo da imagem como contexto adicional na análise.\n`;
3311
+ planImageDir = path.dirname(absImg);
3312
+ }
3313
+ }
3314
+
3296
3315
  const schema = {
3297
3316
  actions: [
3298
3317
  { type: 'append_daily_log', text: '<string>' },
@@ -3303,7 +3322,7 @@ async function cmdWeb({ port, dir, open, dev }) {
3303
3322
  ]
3304
3323
  };
3305
3324
 
3306
- const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
3325
+ const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)${planImageContext}\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
3307
3326
 
3308
3327
  // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
3309
3328
  const cmd = process.env.COPILOT_CMD || 'copilot';
@@ -3312,8 +3331,26 @@ async function cmdWeb({ port, dir, open, dev }) {
3312
3331
  // so the UI can show actionable next steps instead of hard-failing.
3313
3332
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3314
3333
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3334
+
3335
+ // ENAMETOOLONG fix: when prompt exceeds safe CLI arg length,
3336
+ // write to temp file and pipe via stdin instead of -p argument.
3337
+ const SAFE_ARG_LEN = 24000; // ~24KB safe threshold (Windows CreateProcess limit is 32KB)
3338
+ const useTempFile = prompt.length > SAFE_ARG_LEN;
3339
+ let tmpFile = null;
3340
+
3315
3341
  try {
3316
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
3342
+ let r;
3343
+ const baseArgs = ['-s', '--no-color', '--stream', 'off'];
3344
+ if (planImageDir) baseArgs.push('--add-dir', planImageDir);
3345
+
3346
+ if (useTempFile) {
3347
+ tmpFile = path.join(os.tmpdir(), `freya-prompt-${Date.now()}.txt`);
3348
+ fs.writeFileSync(tmpFile, prompt, 'utf8');
3349
+ // Use -p with short hint + pipe full prompt via stdin
3350
+ r = await run(cmd, [...baseArgs, '-p', `Read the full prompt from the file: ${tmpFile}`, '--allow-all-tools'], workspaceDir, agentEnv, prompt);
3351
+ } else {
3352
+ r = await run(cmd, [...baseArgs, '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
3353
+ }
3317
3354
  const out = (r.stdout + r.stderr).trim();
3318
3355
  if (r.code !== 0) {
3319
3356
  return safeJson(res, 200, {
@@ -3329,6 +3366,9 @@ async function cmdWeb({ port, dir, open, dev }) {
3329
3366
  plan: `Copilot CLI não disponível (cmd: ${cmd}).\n\nPara habilitar:\n- Windows (winget): winget install GitHub.Copilot\n- npm: npm i -g @github/copilot\n\nDepois rode \"copilot\" uma vez e faça /login.`,
3330
3367
  details: e.message || String(e)
3331
3368
  });
3369
+ } finally {
3370
+ // Clean up temp file
3371
+ if (tmpFile) { try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore */ } }
3332
3372
  }
3333
3373
  }
3334
3374
 
@@ -3771,23 +3811,48 @@ async function cmdWeb({ port, dir, open, dev }) {
3771
3811
  console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
3772
3812
  }
3773
3813
 
3774
- const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
3814
+ // Build image context for the prompt (Copilot reads files via --allow-all-tools)
3815
+ let imageContext = '';
3816
+ if (imagePath) {
3817
+ const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3818
+ if (exists(absImg)) {
3819
+ imageContext = `\n\n[IMAGEM ANEXADA]\nO usuário anexou uma imagem. Leia e analise o arquivo de imagem localizado em: ${absImg}\nInclua a análise da imagem na sua resposta.\n`;
3820
+ }
3821
+ }
3822
+
3823
+ const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}${imageContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
3775
3824
 
3776
3825
  const cmd = process.env.COPILOT_CMD || 'copilot';
3777
3826
 
3778
3827
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3779
3828
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3780
3829
  try {
3781
- // Build copilot args; add image if user pasted a screenshot
3782
3830
  const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
3831
+ // Allow Copilot to access the image file via its built-in tools
3783
3832
  if (imagePath) {
3784
3833
  const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3785
3834
  if (exists(absImg)) {
3786
- copilotArgs.push('--add-image', absImg);
3835
+ copilotArgs.push('--add-dir', path.dirname(absImg));
3787
3836
  }
3788
3837
  }
3789
- copilotArgs.push('-p', prompt);
3790
- const r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3838
+ copilotArgs.push('--allow-all-tools', '-p', prompt);
3839
+
3840
+ // ENAMETOOLONG fix: use stdin for large prompts
3841
+ const SAFE_ARG_LEN = 24000;
3842
+ let oracleTmpFile = null;
3843
+ let r;
3844
+ if (prompt.length > SAFE_ARG_LEN) {
3845
+ oracleTmpFile = path.join(os.tmpdir(), `freya-oracle-${Date.now()}.txt`);
3846
+ fs.writeFileSync(oracleTmpFile, prompt, 'utf8');
3847
+ // Replace -p with short hint; pipe full prompt via stdin
3848
+ const idx = copilotArgs.indexOf('-p');
3849
+ if (idx !== -1) copilotArgs.splice(idx, 2); // remove -p and prompt
3850
+ copilotArgs.push('-p', `Read the full prompt from: ${oracleTmpFile}`);
3851
+ r = await run(cmd, copilotArgs, workspaceDir, oracleEnv, prompt);
3852
+ } else {
3853
+ r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3854
+ }
3855
+ if (oracleTmpFile) { try { fs.unlinkSync(oracleTmpFile); } catch (_) { /* ignore */ } }
3791
3856
  const out = (r.stdout + r.stderr).trim();
3792
3857
  if (r.code !== 0) {
3793
3858
  return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
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",
7
7
  "migrate": "node scripts/migrate-data.js",
8
- "sm-weekly": "node scripts/generate-sm-weekly-report.js",
8
+ "sm-weekly": "node scripts/generate-sm-weekly-report.js",
9
9
  "daily": "node scripts/generate-daily-summary.js",
10
10
  "status": "node scripts/generate-executive-report.js",
11
11
  "blockers": "node scripts/generate-blockers-report.js",
@@ -34,4 +34,4 @@
34
34
  "pdf-lib": "^1.17.1",
35
35
  "sql.js": "^1.12.0"
36
36
  }
37
- }
37
+ }