@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 +89 -4
- package/cli/web.js +73 -8
- 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,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
|
-
|
|
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
|
-
|
|
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-
|
|
3835
|
+
copilotArgs.push('--add-dir', path.dirname(absImg));
|
|
3787
3836
|
}
|
|
3788
3837
|
}
|
|
3789
|
-
copilotArgs.push('-p', prompt);
|
|
3790
|
-
|
|
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.
|
|
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
|
+
}
|