@cccarv82/freya 3.2.0 → 3.3.1

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 +89 -4
  2. package/cli/web.js +91 -10
  3. package/package.json +3 -3
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,17 +3322,50 @@ 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
+ // Build the system instructions (small, always fits in -p)
3326
+ const sysInstructions = `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}`;
3307
3327
 
3308
3328
  // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
3309
3329
  const cmd = process.env.COPILOT_CMD || 'copilot';
3310
3330
 
3311
- // Best-effort: if Copilot CLI isn't available, return 200 with an explanatory plan
3312
- // so the UI can show actionable next steps instead of hard-failing.
3313
3331
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3314
3332
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3333
+
3334
+ // ENAMETOOLONG fix: when user text is large, write it to a temp file
3335
+ // and reference it in the prompt. System instructions stay in -p (small).
3336
+ // The user text is the large part; rules are moderate (~10KB).
3337
+ const fullPrompt = `${sysInstructions}\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
3338
+ const SAFE_ARG_LEN = 24000; // ~24KB safe for Windows CreateProcess
3339
+ const needsFile = fullPrompt.length > SAFE_ARG_LEN;
3340
+ let tmpFile = null;
3341
+
3315
3342
  try {
3316
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
3343
+ let r;
3344
+ const baseArgs = ['-s', '--no-color', '--stream', 'off'];
3345
+ if (planImageDir) baseArgs.push('--add-dir', planImageDir);
3346
+
3347
+ if (needsFile) {
3348
+ // Write ONLY the user text to a temp file; keep instructions in -p
3349
+ tmpFile = path.join(os.tmpdir(), `freya-input-${Date.now()}.txt`);
3350
+ fs.writeFileSync(tmpFile, text, 'utf8');
3351
+ // -p contains: system instructions + rules + reference to the file
3352
+ const filePrompt = `${sysInstructions}\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\nO texto do usuário é MUITO GRANDE e foi salvo no arquivo abaixo. Você DEVE ler o conteúdo completo deste arquivo usando suas ferramentas de leitura de arquivo e processar TODO o conteúdo como input do usuário.\nARQUIVO: ${tmpFile}\n\nIMPORTANTE: NÃO descreva o arquivo. LEIA o conteúdo e processe-o gerando as ações JSON conforme as regras acima.\n`;
3353
+ // If even the filePrompt (instructions + rules + file ref) is too large,
3354
+ // also write rules to a separate file
3355
+ if (filePrompt.length > SAFE_ARG_LEN) {
3356
+ const rulesTmpFile = path.join(os.tmpdir(), `freya-rules-${Date.now()}.txt`);
3357
+ fs.writeFileSync(rulesTmpFile, rulesText, 'utf8');
3358
+ const minPrompt = `${sysInstructions}\n\nREGRAS: Leia as regras do arquivo: ${rulesTmpFile}\n\nINPUT DO USUÁRIO:\nO texto do usuário é MUITO GRANDE e foi salvo no arquivo abaixo. Você DEVE ler o conteúdo completo deste arquivo e processar TODO o conteúdo como input do usuário.\nARQUIVO: ${tmpFile}\n\nIMPORTANTE: NÃO descreva os arquivos. LEIA os conteúdos e processe-os gerando as ações JSON conforme as regras.\n`;
3359
+ baseArgs.push('--add-dir', os.tmpdir());
3360
+ r = await run(cmd, [...baseArgs, '-p', minPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
3361
+ try { fs.unlinkSync(rulesTmpFile); } catch (_) { /* ignore */ }
3362
+ } else {
3363
+ baseArgs.push('--add-dir', os.tmpdir());
3364
+ r = await run(cmd, [...baseArgs, '-p', filePrompt, '--allow-all-tools'], workspaceDir, agentEnv);
3365
+ }
3366
+ } else {
3367
+ r = await run(cmd, [...baseArgs, '-p', fullPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
3368
+ }
3317
3369
  const out = (r.stdout + r.stderr).trim();
3318
3370
  if (r.code !== 0) {
3319
3371
  return safeJson(res, 200, {
@@ -3329,6 +3381,9 @@ async function cmdWeb({ port, dir, open, dev }) {
3329
3381
  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
3382
  details: e.message || String(e)
3331
3383
  });
3384
+ } finally {
3385
+ // Clean up temp file
3386
+ if (tmpFile) { try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore */ } }
3332
3387
  }
3333
3388
  }
3334
3389
 
@@ -3771,23 +3826,49 @@ async function cmdWeb({ port, dir, open, dev }) {
3771
3826
  console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
3772
3827
  }
3773
3828
 
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`;
3829
+ // Build image context for the prompt (Copilot reads files via --allow-all-tools)
3830
+ let imageContext = '';
3831
+ if (imagePath) {
3832
+ const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3833
+ if (exists(absImg)) {
3834
+ 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`;
3835
+ }
3836
+ }
3837
+
3838
+ // System instructions (small, always fits in -p)
3839
+ const oracleSysInstr = `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}`;
3775
3840
 
3776
3841
  const cmd = process.env.COPILOT_CMD || 'copilot';
3777
3842
 
3778
3843
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3779
3844
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3780
3845
  try {
3781
- // Build copilot args; add image if user pasted a screenshot
3782
3846
  const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
3847
+ // Allow Copilot to access the image file via its built-in tools
3783
3848
  if (imagePath) {
3784
3849
  const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3785
3850
  if (exists(absImg)) {
3786
- copilotArgs.push('--add-image', absImg);
3851
+ copilotArgs.push('--add-dir', path.dirname(absImg));
3787
3852
  }
3788
3853
  }
3789
- copilotArgs.push('-p', prompt);
3790
- const r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3854
+
3855
+ // ENAMETOOLONG fix: when prompt is large, write user query to temp file
3856
+ const fullOraclePrompt = `${oracleSysInstr}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
3857
+ const SAFE_ARG_LEN = 24000;
3858
+ let oracleTmpFile = null;
3859
+ let r;
3860
+ if (fullOraclePrompt.length > SAFE_ARG_LEN) {
3861
+ oracleTmpFile = path.join(os.tmpdir(), `freya-oracle-input-${Date.now()}.txt`);
3862
+ fs.writeFileSync(oracleTmpFile, query, 'utf8');
3863
+ const filePrompt = `${oracleSysInstr}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\nA consulta do usuário é grande e foi salva no arquivo abaixo. LEIA o conteúdo completo do arquivo e responda com base nele.\nARQUIVO: ${oracleTmpFile}\n\nIMPORTANTE: NÃO descreva o arquivo. LEIA e RESPONDA à consulta.\n`;
3864
+ copilotArgs.push('--add-dir', os.tmpdir());
3865
+ copilotArgs.push('--allow-all-tools', '-p', filePrompt);
3866
+ r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3867
+ } else {
3868
+ copilotArgs.push('--allow-all-tools', '-p', fullOraclePrompt);
3869
+ r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3870
+ }
3871
+ if (oracleTmpFile) { try { fs.unlinkSync(oracleTmpFile); } catch (_) { /* ignore */ } }
3791
3872
  const out = (r.stdout + r.stderr).trim();
3792
3873
  if (r.code !== 0) {
3793
3874
  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.1",
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
+ }