@cccarv82/freya 3.7.3 → 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 +90 -25
  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;
@@ -1008,7 +1069,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1008
1069
  if (!q) return { ok: false, error: 'Missing query' };
1009
1070
 
1010
1071
  const limit = Math.max(1, Math.min(20, Number(opts.limit || 8)));
1011
- const cmd = process.env.COPILOT_CMD || 'copilot';
1072
+ const copilotResolved = getCopilotCmd();
1012
1073
 
1013
1074
  const prompt = [
1014
1075
  'Você é um buscador local de arquivos.',
@@ -1027,7 +1088,7 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1027
1088
  'A lista deve estar ordenada por relevância.'
1028
1089
  ].join('\n');
1029
1090
 
1030
- const args = [
1091
+ const { cmd: spawnCmd, args: spawnArgs } = copilotSpawnArgs(copilotResolved, [
1031
1092
  '-s',
1032
1093
  '--no-color',
1033
1094
  '--stream',
@@ -1037,9 +1098,9 @@ async function copilotSearch(workspaceDir, query, opts = {}) {
1037
1098
  '--allow-all-tools',
1038
1099
  '--add-dir',
1039
1100
  workspaceDir
1040
- ];
1101
+ ]);
1041
1102
 
1042
- const r = await run(cmd, args, workspaceDir);
1103
+ const r = await run(spawnCmd, spawnArgs, workspaceDir);
1043
1104
  const out = (r.stdout + r.stderr).trim();
1044
1105
  if (r.code !== 0) return { ok: false, error: out || 'Copilot returned non-zero exit code.' };
1045
1106
 
@@ -4081,8 +4142,7 @@ async function cmdWeb({ port, dir, open, dev }) {
4081
4142
  // Build the system instructions (small, always fits in -p)
4082
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}`;
4083
4144
 
4084
- // Prefer COPILOT_CMD if provided, otherwise try 'copilot'
4085
- const cmd = process.env.COPILOT_CMD || 'copilot';
4145
+ const copilotResolved = getCopilotCmd();
4086
4146
 
4087
4147
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4088
4148
  const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
@@ -4097,8 +4157,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4097
4157
 
4098
4158
  try {
4099
4159
  let r;
4100
- const baseArgs = ['-s', '--no-color', '--stream', 'off'];
4101
- if (planImageDir) baseArgs.push('--add-dir', planImageDir);
4160
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4161
+ if (planImageDir) copilotExtra.push('--add-dir', planImageDir);
4102
4162
 
4103
4163
  if (needsFile) {
4104
4164
  // Write ONLY the user text to a temp file; keep instructions in -p
@@ -4112,15 +4172,18 @@ async function cmdWeb({ port, dir, open, dev }) {
4112
4172
  const rulesTmpFile = path.join(os.tmpdir(), `freya-rules-${Date.now()}.txt`);
4113
4173
  fs.writeFileSync(rulesTmpFile, rulesText, 'utf8');
4114
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`;
4115
- baseArgs.push('--add-dir', os.tmpdir());
4116
- 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);
4117
4178
  try { fs.unlinkSync(rulesTmpFile); } catch (_) { /* ignore */ }
4118
4179
  } else {
4119
- baseArgs.push('--add-dir', os.tmpdir());
4120
- 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);
4121
4183
  }
4122
4184
  } else {
4123
- 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);
4124
4187
  }
4125
4188
  const out = (r.stdout + r.stderr).trim();
4126
4189
  if (r.code !== 0) {
@@ -4614,17 +4677,17 @@ DADOS REAIS DO WORKSPACE (use estes dados para responder):
4614
4677
  ${dataContext}
4615
4678
  ${imageContext}`;
4616
4679
 
4617
- const cmd = process.env.COPILOT_CMD || 'copilot';
4680
+ const copilotResolved = getCopilotCmd();
4618
4681
 
4619
4682
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
4620
4683
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
4621
4684
  try {
4622
- const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
4685
+ const copilotExtra = ['-s', '--no-color', '--stream', 'off'];
4623
4686
  // Allow Copilot to access the image file via its built-in tools
4624
4687
  if (imagePath) {
4625
4688
  const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
4626
4689
  if (exists(absImg)) {
4627
- copilotArgs.push('--add-dir', path.dirname(absImg));
4690
+ copilotExtra.push('--add-dir', path.dirname(absImg));
4628
4691
  }
4629
4692
  }
4630
4693
 
@@ -4637,12 +4700,14 @@ ${imageContext}`;
4637
4700
  oracleTmpFile = path.join(os.tmpdir(), `freya-orchestrator-${Date.now()}.txt`);
4638
4701
  fs.writeFileSync(oracleTmpFile, fullOraclePrompt, 'utf8');
4639
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.`;
4640
- copilotArgs.push('--add-dir', os.tmpdir());
4641
- copilotArgs.push('--allow-all-tools', '-p', filePrompt);
4642
- 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);
4643
4707
  } else {
4644
- copilotArgs.push('--allow-all-tools', '-p', fullOraclePrompt);
4645
- 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);
4646
4711
  }
4647
4712
  if (oracleTmpFile) { try { fs.unlinkSync(oracleTmpFile); } catch (_) { /* ignore */ } }
4648
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.3",
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",