@empirical-gauzza/cli 1.0.34 → 1.0.35

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/lib/auth.js CHANGED
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const { spawnSync } = require('child_process');
8
8
  const { log, warn, uiQuestion, uiDim, uiLink, ask } = require('./ui');
9
- const { readJsonIfExists, writeFile, ensureDir, quotePowerShellArg, runResolvedCommand } = require('./fsUtils');
9
+ const { readJsonIfExists, writeFile, ensureDir, quotePowerShellArg, runResolvedCommand, runPowerShell } = require('./fsUtils');
10
10
  const { httpGet, httpJson, httpForm } = require('./http');
11
11
  const { getEnv, parseFlag, parseOption } = require('./env');
12
12
  const {
@@ -81,17 +81,26 @@ async function oauthPreflight(baseMcpUrl) {
81
81
  }
82
82
  }
83
83
 
84
+ /** Best-effort browser launch. Returns true when the launcher ran, false when it could not. */
84
85
  function openBrowser(url) {
86
+ let result;
85
87
  if (process.platform === 'win32') {
86
- const script = `Start-Process ${quotePowerShellArg(url)}`;
87
- spawnSync('pwsh', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
88
+ // runPowerShell falls back from pwsh to powershell.exe; the old hardcoded `pwsh` silently
89
+ // did nothing on a stock Windows install (2026-09-23, real user report).
90
+ result = runPowerShell(`Start-Process ${quotePowerShellArg(url)}`, { stdio: 'ignore' });
88
91
  } else if (process.platform === 'darwin') {
89
- spawnSync('open', [url], { stdio: 'ignore' });
92
+ result = spawnSync('open', [url], { stdio: 'ignore' });
90
93
  } else {
91
- spawnSync('xdg-open', [url], { stdio: 'ignore' });
94
+ result = spawnSync('xdg-open', [url], { stdio: 'ignore' });
92
95
  }
96
+ return Boolean(result) && !result.error && result.status === 0;
93
97
  }
94
98
 
99
+ // How long the loopback listener waits for the browser to come back. Was 5 minutes; a user who
100
+ // has to find the printed link by hand, log in, and pick a workspace can take longer than that,
101
+ // and once the listener closes the browser lands on a dead 127.0.0.1 port with no explanation.
102
+ const OAUTH_CALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
103
+
95
104
  async function runMcpOAuthFlow(baseUrl) {
96
105
  return new Promise((resolve, reject) => {
97
106
  const { codeVerifier, codeChallenge } = generatePKCE();
@@ -103,9 +112,9 @@ async function runMcpOAuthFlow(baseUrl) {
103
112
  if (server) server.close();
104
113
  if (!resolved) {
105
114
  resolved = true;
106
- reject(new Error('OAuth callback timeout (5 minutes)'));
115
+ reject(new Error(`OAuth callback timeout (${OAUTH_CALLBACK_TIMEOUT_MS / 60000} minutes). The login link has expired; run the command again to get a fresh one.`));
107
116
  }
108
- }, 5 * 60 * 1000);
117
+ }, OAUTH_CALLBACK_TIMEOUT_MS);
109
118
 
110
119
  server = http.createServer((req, res) => {
111
120
  const url = new URL(`http://localhost${req.url}`);
@@ -162,8 +171,14 @@ async function runMcpOAuthFlow(baseUrl) {
162
171
  authUrl.searchParams.set('scope', 'memory:read memory:write memory:delete offline_access');
163
172
 
164
173
  log(uiQuestion('Opening browser for Empirical MCP authentication...'));
165
- openBrowser(authUrl.toString());
166
- log(uiDim(`If browser doesn't open, visit: ${uiLink(authUrl.toString())}`));
174
+ const opened = openBrowser(authUrl.toString());
175
+ if (opened) {
176
+ log(uiDim(`If browser doesn't open, visit: ${uiLink(authUrl.toString())}`));
177
+ } else {
178
+ warn('Could not open a browser automatically. Open this link yourself:');
179
+ log(uiLink(authUrl.toString()));
180
+ }
181
+ log(uiDim(`Waiting up to ${OAUTH_CALLBACK_TIMEOUT_MS / 60000} minutes for the browser to finish...`));
167
182
  });
168
183
 
169
184
  server.on('error', (error) => {
package/lib/fsUtils.js CHANGED
@@ -4,11 +4,51 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
6
 
7
+ // 2026-09-23: every Windows shell-out used to hardcode `pwsh` (PowerShell 7). Windows ships only
8
+ // Windows PowerShell 5.1 (`powershell.exe`); PowerShell 7 is a separate install most people do
9
+ // not have. A real user on plain Windows hit `spawnSync pwsh ENOENT` on every plugin install AND
10
+ // the browser never opened for OAuth (openBrowser also went through pwsh), so they found the
11
+ // login link late, after the loopback listener had closed. Resolve once per process: prefer
12
+ // pwsh when present, fall back to powershell.exe, and surface a clear error if neither works.
13
+ let resolvedPowerShell;
14
+ function resolvePowerShell() {
15
+ if (resolvedPowerShell !== undefined) return resolvedPowerShell;
16
+ const override = String(process.env.EMPIRICAL_POWERSHELL || '').trim();
17
+ const candidates = override ? [override] : ['pwsh', 'powershell'];
18
+ for (const candidate of candidates) {
19
+ const probe = spawnSync(candidate, ['-NoProfile', '-Command', 'exit 0'], { stdio: 'ignore', timeout: 15000 });
20
+ if (!probe.error && probe.status === 0) {
21
+ resolvedPowerShell = candidate;
22
+ return candidate;
23
+ }
24
+ }
25
+ resolvedPowerShell = null;
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Runs a PowerShell script on Windows through whichever PowerShell exists. Returns a spawnSync-
31
+ * shaped result; when no PowerShell can be found the result carries an `error` (never throws),
32
+ * the same shape spawnSync itself returns for ENOENT, so callers' existing error paths apply.
33
+ */
34
+ function runPowerShell(script, options = {}) {
35
+ const shell = resolvePowerShell();
36
+ if (!shell) {
37
+ return {
38
+ status: null,
39
+ stdout: '',
40
+ stderr: '',
41
+ error: new Error('No PowerShell found (tried pwsh and powershell). Install PowerShell 7 or set EMPIRICAL_POWERSHELL to your PowerShell executable.')
42
+ };
43
+ }
44
+ return spawnSync(shell, ['-NoProfile', '-Command', script], options);
45
+ }
46
+
7
47
  function commandExists(cmd) {
8
48
  if (process.platform === 'win32') {
9
49
  const escaped = cmd.replace(/'/g, "''");
10
50
  const script = `if (Get-Command '${escaped}' -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }`;
11
- const result = spawnSync('pwsh', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
51
+ const result = runPowerShell(script, { stdio: 'ignore' });
12
52
  return result.status === 0;
13
53
  }
14
54
  const result = spawnSync('which', [cmd], { stdio: 'ignore' });
@@ -57,7 +97,7 @@ function runResolvedCommand(command, args, options = {}) {
57
97
  }
58
98
 
59
99
  const script = `& ${quotePowerShellArg(command)} ${args.map(quotePowerShellArg).join(' ')}`;
60
- return spawnSync('pwsh', ['-NoProfile', '-Command', script], {
100
+ return runPowerShell(script, {
61
101
  encoding: 'utf8',
62
102
  ...(input !== undefined ? { input } : { stdio })
63
103
  });
@@ -79,6 +119,8 @@ function summarizeProcessOutput(result) {
79
119
  }
80
120
 
81
121
  module.exports = {
122
+ resolvePowerShell,
123
+ runPowerShell,
82
124
  commandExists,
83
125
  ensureDir,
84
126
  readUtf8IfExists,
@@ -77,9 +77,9 @@ function detectLockHolders(agentId) {
77
77
  const names = LOCK_HOLDER_PROCESS_NAMES[agentId] || [];
78
78
  if (!names.length) return [];
79
79
  try {
80
- const { spawnSync } = require('child_process');
80
+ const { runPowerShell } = require('../fsUtils');
81
81
  const script = `Get-Process -Name ${names.join(',')} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ProcessName -Unique`;
82
- const probe = spawnSync('pwsh', ['-NoProfile', '-Command', script], { encoding: 'utf8', timeout: 5000 });
82
+ const probe = runPowerShell(script, { encoding: 'utf8', timeout: 5000 });
83
83
  return String(probe.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
84
84
  } catch (_error) {
85
85
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empirical-gauzza/cli",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Persistent cross-tool memory for AI agents. Give Claude Code, Codex, Copilot, Cursor, and ChatGPT one shared memory: save project context once, recall it everywhere via CLI or MCP.",
5
5
  "keywords": [
6
6
  "memory",