@cccarv82/freya 3.7.3 → 3.7.5

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 +105 -27
  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,8 +970,21 @@ function run(cmd, args, cwd, extraEnv, stdinData) {
909
970
  const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
910
971
 
911
972
  try {
912
- // Use shell: true on all platforms — ensures .cmd/.bat shims (npm globals) are found on Windows
913
- child = spawn(cmd, args, { cwd, shell: true, env, windowsHide: true });
973
+ if (process.platform === 'win32') {
974
+ // Use PowerShell fnm/nvm set PATH only in PowerShell profile,
975
+ // so cmd.exe (shell:true) can't find npm global .cmd shims like copilot.
976
+ // Do NOT use -NoProfile: the profile is what loads fnm/nvm PATH entries.
977
+ const escapedArgs = args.map(a => {
978
+ const escaped = String(a).replace(/'/g, "''");
979
+ return `'${escaped}'`;
980
+ });
981
+ const psCommand = `& '${cmd}' ${escapedArgs.join(' ')}`;
982
+ child = spawn('powershell.exe', [
983
+ '-NoLogo', '-Command', psCommand
984
+ ], { cwd, env, windowsHide: true });
985
+ } else {
986
+ child = spawn(cmd, args, { cwd, shell: true, env });
987
+ }
914
988
  } catch (e) {
915
989
  return resolve({ code: 1, stdout: '', stderr: e.message || String(e) });
916
990
  }
@@ -1008,7 +1082,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1008
1082
  if (!q) return { ok: false, error: 'Missing query' };
1009
1083
 
1010
1084
  const limit = Math.max(1, Math.min(20, Number(opts.limit || 8)));
1011
- const cmd = process.env.COPILOT_CMD || 'copilot';
1085
+ const copilotResolved = getCopilotCmd();
1012
1086
 
1013
1087
  const prompt = [
1014
1088
  'Você é um buscador local de arquivos.',
@@ -1027,7 +1101,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1027
1101
  'A lista deve estar ordenada por relevância.'
1028
1102
  ].join('\n');
1029
1103
 
1030
- const args = [
1104
+ const { cmd: spawnCmd, args: spawnArgs } = copilotSpawnArgs(copilotResolved, [
1031
1105
  '-s',
1032
1106
  '--no-color',
1033
1107
  '--stream',
@@ -1037,9 +1111,9 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1037
1111
  '--allow-all-tools',
1038
1112
  '--add-dir',
1039
1113
  workspaceDir
1040
- ];
1114
+ ]);
1041
1115
 
1042
- const r = await run(cmd, args, workspaceDir);
1116
+ const r = await run(spawnCmd, spawnArgs, workspaceDir);
1043
1117
  const out = (r.stdout + r.stderr).trim();
1044
1118
  if (r.code !== 0) return { ok: false, error: out || 'Copilot returned non-zero exit code.' };
1045
1119
 
@@ -4081,8 +4155,7 @@ async function cmdWeb({ port, dir, open, dev }) {
4081
4155
  // Build the system instructions (small, always fits in -p)
4082
4156
  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}`;
4083
4157
 
4084
- // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
4085
- const cmd = process.env.COPILOT_CMD || 'copilot';
4158
+ const copilotResolved = getCopilotCmd();
4086
4159
 
4087
4160
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4088
4161
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
@@ -4097,8 +4170,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4097
4170
 
4098
4171
  try {
4099
4172
  let r;
4100
- const baseArgs = ['-s', '--no-color', '--stream', 'off'];
4101
- if (planImageDir) baseArgs.push('--add-dir', planImageDir);
4173
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4174
+ if (planImageDir) copilotExtra.push('--add-dir', planImageDir);
4102
4175
 
4103
4176
  if (needsFile) {
4104
4177
  // Write ONLY the user text to a temp file; keep instructions in -p
@@ -4112,15 +4185,18 @@ async function cmdWeb({ port, dir, open, dev }) {
4112
4185
  const rulesTmpFile = path.join(os.tmpdir(), `freya-rules-${Date.now()}.txt`);
4113
4186
  fs.writeFileSync(rulesTmpFile, rulesText, 'utf8');
4114
4187
  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`;
4115
- baseArgs.push('--add-dir', os.tmpdir());
4116
- r = await run(cmd, [...baseArgs, '-p', minPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4188
+ copilotExtra.push('--add-dir', os.tmpdir());
4189
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', minPrompt, '--allow-all-tools']);
4190
+ r = await run(sc, sa, workspaceDir, agentEnv);
4117
4191
  try { fs.unlinkSync(rulesTmpFile); } catch (_) { /* ignore */ }
4118
4192
  } else {
4119
- baseArgs.push('--add-dir', os.tmpdir());
4120
- r = await run(cmd, [...baseArgs, '-p', filePrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4193
+ copilotExtra.push('--add-dir', os.tmpdir());
4194
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', filePrompt, '--allow-all-tools']);
4195
+ r = await run(sc, sa, workspaceDir, agentEnv);
4121
4196
  }
4122
4197
  } else {
4123
- r = await run(cmd, [...baseArgs, '-p', fullPrompt, '--allow-all-tools'], workspaceDir, agentEnv);
4198
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, [...copilotExtra, '-p', fullPrompt, '--allow-all-tools']);
4199
+ r = await run(sc, sa, workspaceDir, agentEnv);
4124
4200
  }
4125
4201
  const out = (r.stdout + r.stderr).trim();
4126
4202
  if (r.code !== 0) {
@@ -4614,17 +4690,17 @@ DADOS REAIS DO WORKSPACE (use estes dados para responder):
4614
4690
  ${dataContext}
4615
4691
  ${imageContext}`;
4616
4692
 
4617
- const cmd = process.env.COPILOT_CMD || 'copilot';
4693
+ const copilotResolved = getCopilotCmd();
4618
4694
 
4619
4695
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4620
4696
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
4621
4697
  try {
4622
- const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
4698
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4623
4699
  // Allow Copilot to access the image file via its built-in tools
4624
4700
  if (imagePath) {
4625
4701
  const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
4626
4702
  if (exists(absImg)) {
4627
- copilotArgs.push('--add-dir', path.dirname(absImg));
4703
+ copilotExtra.push('--add-dir', path.dirname(absImg));
4628
4704
  }
4629
4705
  }
4630
4706
 
@@ -4637,12 +4713,14 @@ ${imageContext}`;
4637
4713
  oracleTmpFile = path.join(os.tmpdir(), `freya-orchestrator-${Date.now()}.txt`);
4638
4714
  fs.writeFileSync(oracleTmpFile, fullOraclePrompt, 'utf8');
4639
4715
  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.`;
4640
- copilotArgs.push('--add-dir', os.tmpdir());
4641
- copilotArgs.push('--allow-all-tools', '-p', filePrompt);
4642
- r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
4716
+ copilotExtra.push('--add-dir', os.tmpdir());
4717
+ copilotExtra.push('--allow-all-tools', '-p', filePrompt);
4718
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, copilotExtra);
4719
+ r = await run(sc, sa, workspaceDir, oracleEnv);
4643
4720
  } else {
4644
- copilotArgs.push('--allow-all-tools', '-p', fullOraclePrompt);
4645
- r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
4721
+ copilotExtra.push('--allow-all-tools', '-p', fullOraclePrompt);
4722
+ const { cmd: sc, args: sa } = copilotSpawnArgs(copilotResolved, copilotExtra);
4723
+ r = await run(sc, sa, workspaceDir, oracleEnv);
4646
4724
  }
4647
4725
  if (oracleTmpFile) { try { fs.unlinkSync(oracleTmpFile); } catch (_) { /* ignore */ } }
4648
4726
  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.3",
3
+ "version": "3.7.5",
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",