@cccarv82/freya 3.7.2 → 3.7.4

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 (2) hide show
  1. package/cli/web.js +92 -33
  2. package/package.json +1 -1
package/cli/web.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
- const { spawn } = require('child_process');
8
+ const { spawn, execSync } = require('child_process');
9
9
  const { searchWorkspace } = require('../scripts/lib/search-utils');
10
10
  const { searchIndex } = require('../scripts/lib/index-utils');
11
11
  const { initWorkspace } = require('./init');
@@ -278,7 +278,7 @@ async function backgroundIngestFromChat(workspaceDir, userQuery) {
278
278
  if (!INGEST_SIGNALS.test(userQuery)) return;
279
279
 
280
280
  try {
281
- const cmd = process.env.COPILOT_CMD || 'copilot';
281
+ const copilotResolved = getCopilotCmd();
282
282
 
283
283
  // Build a minimal planner prompt
284
284
  const schema = {
@@ -290,9 +290,9 @@ async function backgroundIngestFromChat(workspaceDir, userQuery) {
290
290
  const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nAnalise o texto abaixo e extraia APENAS tarefas e blockers explícitos.\nSe NÃO houver tarefas ou blockers claros, retorne: {"actions":[]}\nRetorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\nNÃO use code fences. NÃO inclua texto extra.\n\nTEXTO:\n${userQuery}\n`;
291
291
 
292
292
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
293
- const baseArgs = ['-s', '--no-color', '--stream', 'off', '-p', prompt];
293
+ const { cmd: spawnCmd, args: baseArgsPrefix } = copilotSpawnArgs(copilotResolved, ['-s', '--no-color', '--stream', 'off', '-p', prompt]);
294
294
 
295
- const r = await run(cmd, baseArgs, workspaceDir, agentEnv);
295
+ const r = await run(spawnCmd, baseArgsPrefix, workspaceDir, agentEnv);
296
296
  const out = (r.stdout + r.stderr).trim();
297
297
  if (r.code !== 0 || !out) return;
298
298
 
@@ -902,6 +902,67 @@ function readBody(req, maxBytes = 4 * 1024 * 1024) {
902
902
  });
903
903
  }
904
904
 
905
+ /**
906
+ * Resolve the full path to the copilot CLI.
907
+ * On Windows with fnm/nvm, npm global .cmd shims aren't in cmd.exe's PATH,
908
+ * so we use PowerShell to discover the real path. Result is cached.
909
+ */
910
+ let _copilotPathCache = null;
911
+ function getCopilotCmd() {
912
+ if (_copilotPathCache !== null) return _copilotPathCache;
913
+
914
+ // User override
915
+ if (process.env.COPILOT_CMD) {
916
+ _copilotPathCache = process.env.COPILOT_CMD;
917
+ return _copilotPathCache;
918
+ }
919
+
920
+ if (process.platform === 'win32') {
921
+ // Try 'where' first (system PATH)
922
+ try {
923
+ const p = execSync('where copilot 2>nul', { encoding: 'utf8', timeout: 5000 }).trim().split(/\r?\n/)[0];
924
+ if (p && fs.existsSync(p)) { _copilotPathCache = p; return p; }
925
+ } catch { }
926
+ // Use PowerShell to resolve (works with fnm, nvm-windows, etc.)
927
+ try {
928
+ const p = execSync(
929
+ 'powershell.exe -NoProfile -Command "(Get-Command copilot -ErrorAction SilentlyContinue).Source"',
930
+ { encoding: 'utf8', timeout: 10000 }
931
+ ).trim();
932
+ if (p && fs.existsSync(p)) { _copilotPathCache = p; return p; }
933
+ } catch { }
934
+ // Try gh
935
+ try {
936
+ const p = execSync(
937
+ 'powershell.exe -NoProfile -Command "(Get-Command gh -ErrorAction SilentlyContinue).Source"',
938
+ { encoding: 'utf8', timeout: 10000 }
939
+ ).trim();
940
+ if (p && fs.existsSync(p)) { _copilotPathCache = `gh-copilot:${p}`; return _copilotPathCache; }
941
+ } catch { }
942
+ } else {
943
+ try {
944
+ const p = execSync('which copilot 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim();
945
+ if (p) { _copilotPathCache = p; return p; }
946
+ } catch { }
947
+ try {
948
+ const p = execSync('which gh 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim();
949
+ if (p) { _copilotPathCache = `gh-copilot:${p}`; return _copilotPathCache; }
950
+ } catch { }
951
+ }
952
+
953
+ _copilotPathCache = 'copilot'; // fallback
954
+ return _copilotPathCache;
955
+ }
956
+
957
+ /** Helper: build cmd + args for copilot or gh copilot */
958
+ function copilotSpawnArgs(copilotCmd, extraArgs) {
959
+ if (copilotCmd.startsWith('gh-copilot:')) {
960
+ const ghPath = copilotCmd.slice('gh-copilot:'.length);
961
+ return { cmd: ghPath, args: ['copilot', ...extraArgs] };
962
+ }
963
+ return { cmd: copilotCmd, args: extraArgs };
964
+ }
965
+
905
966
  function run(cmd, args, cwd, extraEnv, stdinData) {
906
967
  return new Promise((resolve) => {
907
968
  let child;
@@ -909,14 +970,8 @@ function run(cmd, args, cwd, extraEnv, stdinData) {
909
970
  const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
910
971
 
911
972
  try {
912
- // On Windows, reliably execute CLI tools through cmd.exe.
913
- // This ensures PATH resolution works for tools like copilot, gh, npx, npm.
914
- if (process.platform === 'win32') {
915
- const comspec = process.env.ComSpec || 'cmd.exe';
916
- child = spawn(comspec, ['/d', '/s', '/c', cmd, ...args], { cwd, shell: false, env });
917
- } else {
918
- child = spawn(cmd, args, { cwd, shell: false, env });
919
- }
973
+ // Use shell: true on all platforms ensures .cmd/.bat shims (npm globals) are found on Windows
974
+ child = spawn(cmd, args, { cwd, shell: true, env, windowsHide: true });
920
975
  } catch (e) {
921
976
  return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
922
977
  }
@@ -1014,7 +1069,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1014
1069
  if (!q) return { ok: false, error: 'Missing query' };
1015
1070
 
1016
1071
  const limit = Math.max(1, Math.min(20, Number(opts.limit || 8)));
1017
- const cmd = process.env.COPILOT_CMD || 'copilot';
1072
+ const copilotResolved = getCopilotCmd();
1018
1073
 
1019
1074
  const prompt = [
1020
1075
  'Você é um buscador local de arquivos.',
@@ -1033,7 +1088,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1033
1088
  'A lista deve estar ordenada por relevância.'
1034
1089
  ].join('\n');
1035
1090
 
1036
- const args = [
1091
+ const { cmd: spawnCmd, args: spawnArgs } = copilotSpawnArgs(copilotResolved, [
1037
1092
  '-s',
1038
1093
  '--no-color',
1039
1094
  '--stream',
@@ -1043,9 +1098,9 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1043
1098
  '--allow-all-tools',
1044
1099
  '--add-dir',
1045
1100
  workspaceDir
1046
- ];
1101
+ ]);
1047
1102
 
1048
- const r = await run(cmd, args, workspaceDir);
1103
+ const r = await run(spawnCmd, spawnArgs, workspaceDir);
1049
1104
  const out = (r.stdout + r.stderr).trim();
1050
1105
  if (r.code !== 0) return { ok: false, error: out || 'Copilot returned non-zero exit code.' };
1051
1106
 
@@ -4087,8 +4142,7 @@ async function cmdWeb({ port, dir, open, dev }) {
4087
4142
  // Build the system instructions (small, always fits in -p)
4088
4143
  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}`;
4089
4144
 
4090
- // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
4091
- const cmd = process.env.COPILOT_CMD || 'copilot';
4145
+ const copilotResolved = getCopilotCmd();
4092
4146
 
4093
4147
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4094
4148
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
@@ -4103,8 +4157,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4103
4157
 
4104
4158
  try {
4105
4159
  let r;
4106
- const baseArgs = ['-s', '--no-color', '--stream', 'off'];
4107
- if (planImageDir) baseArgs.push('--add-dir', planImageDir);
4160
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4161
+ if (planImageDir) copilotExtra.push('--add-dir', planImageDir);
4108
4162
 
4109
4163
  if (needsFile) {
4110
4164
  // Write ONLY the user text to a temp file; keep instructions in -p
@@ -4118,15 +4172,18 @@ async function cmdWeb({ port, dir, open, dev }) {
4118
4172
  const rulesTmpFile = path.join(os.tmpdir(), `freya-rules-${Date.now()}.txt`);
4119
4173
  fs.writeFileSync(rulesTmpFile, rulesText, 'utf8');
4120
4174
  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`;
4121
- baseArgs.push('--add-dir', os.tmpdir());
4122
- r = await run(cmd, [...baseArgs, '-p', minPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4175
+ copilotExtra.push('--add-dir', os.tmpdir());
4176
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', minPrompt, '--allow-all-tools']);
4177
+ r = await run(sc, sa, workspaceDir, agentEnv);
4123
4178
  try { fs.unlinkSync(rulesTmpFile); } catch (_) { /* ignore */ }
4124
4179
  } else {
4125
- baseArgs.push('--add-dir', os.tmpdir());
4126
- r = await run(cmd, [...baseArgs, '-p', filePrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4180
+ copilotExtra.push('--add-dir', os.tmpdir());
4181
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', filePrompt, '--allow-all-tools']);
4182
+ r = await run(sc, sa, workspaceDir, agentEnv);
4127
4183
  }
4128
4184
  } else {
4129
- r = await run(cmd, [...baseArgs, '-p', fullPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4185
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', fullPrompt, '--allow-all-tools']);
4186
+ r = await run(sc, sa, workspaceDir, agentEnv);
4130
4187
  }
4131
4188
  const out = (r.stdout + r.stderr).trim();
4132
4189
  if (r.code !== 0) {
@@ -4620,17 +4677,17 @@ DADOS REAIS DO WORKSPACE (use estes dados para responder):
4620
4677
  ${dataContext}
4621
4678
  ${imageContext}`;
4622
4679
 
4623
- const cmd = process.env.COPILOT_CMD || 'copilot';
4680
+ const copilotResolved = getCopilotCmd();
4624
4681
 
4625
4682
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4626
4683
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
4627
4684
  try {
4628
- const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
4685
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4629
4686
  // Allow Copilot to access the image file via its built-in tools
4630
4687
  if (imagePath) {
4631
4688
  const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
4632
4689
  if (exists(absImg)) {
4633
- copilotArgs.push('--add-dir', path.dirname(absImg));
4690
+ copilotExtra.push('--add-dir', path.dirname(absImg));
4634
4691
  }
4635
4692
  }
4636
4693
 
@@ -4643,12 +4700,14 @@ ${imageContext}`;
4643
4700
  oracleTmpFile = path.join(os.tmpdir(), `freya-orchestrator-${Date.now()}.txt`);
4644
4701
  fs.writeFileSync(oracleTmpFile, fullOraclePrompt, 'utf8');
4645
4702
  const filePrompt = `Leia o arquivo abaixo que contém suas instruções completas, regras, dados do workspace e a consulta do usuário. Siga TODAS as instruções contidas nele.\nARQUIVO: ${oracleTmpFile}\n\nIMPORTANTE: Leia o arquivo INTEIRO e responda à consulta do usuário que está no final do arquivo.`;
4646
- copilotArgs.push('--add-dir', os.tmpdir());
4647
- copilotArgs.push('--allow-all-tools', '-p', filePrompt);
4648
- r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
4703
+ copilotExtra.push('--add-dir', os.tmpdir());
4704
+ copilotExtra.push('--allow-all-tools', '-p', filePrompt);
4705
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, copilotExtra);
4706
+ r = await run(sc, sa, workspaceDir, oracleEnv);
4649
4707
  } else {
4650
- copilotArgs.push('--allow-all-tools', '-p', fullOraclePrompt);
4651
- r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
4708
+ copilotExtra.push('--allow-all-tools', '-p', fullOraclePrompt);
4709
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, copilotExtra);
4710
+ r = await run(sc, sa, workspaceDir, oracleEnv);
4652
4711
  }
4653
4712
  if (oracleTmpFile) { try { fs.unlinkSync(oracleTmpFile); } catch (_) { /* ignore */ } }
4654
4713
  const out = (r.stdout + r.stderr).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
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",