@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.
- package/cli/web-ui.js +89 -4
- package/cli/web.js +91 -10
- 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
|
-
|
|
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
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
3851
|
+
copilotArgs.push('--add-dir', path.dirname(absImg));
|
|
3787
3852
|
}
|
|
3788
3853
|
}
|
|
3789
|
-
|
|
3790
|
-
|
|
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.
|
|
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
|
+
}
|