@agentrhq/webcmd 0.2.0 → 0.2.1

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 (42) hide show
  1. package/README.md +23 -11
  2. package/cli-manifest.json +2 -0
  3. package/clis/twitter/delete.js +14 -6
  4. package/clis/twitter/delete.test.js +74 -1
  5. package/clis/twitter/timeline.js +3 -1
  6. package/clis/twitter/timeline.test.js +4 -0
  7. package/dist/src/browser/bridge-readiness.test.js +1 -1
  8. package/dist/src/browser/bridge.d.ts +1 -0
  9. package/dist/src/browser/bridge.js +6 -4
  10. package/dist/src/browser/cdp.d.ts +1 -0
  11. package/dist/src/browser/daemon-client.d.ts +1 -0
  12. package/dist/src/browser/daemon-client.js +7 -2
  13. package/dist/src/browser/daemon-client.test.js +33 -30
  14. package/dist/src/browser/daemon-transport.js +3 -19
  15. package/dist/src/browser/page.d.ts +2 -1
  16. package/dist/src/browser/page.js +5 -1
  17. package/dist/src/browser/profile.d.ts +9 -0
  18. package/dist/src/browser/profile.js +18 -7
  19. package/dist/src/browser/profile.test.d.ts +1 -0
  20. package/dist/src/browser/profile.test.js +44 -0
  21. package/dist/src/browser/protocol.d.ts +1 -0
  22. package/dist/src/browser/runtime/local-cloak/actions.js +24 -10
  23. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +1 -0
  24. package/dist/src/browser/runtime/local-cloak/session-manager.js +3 -0
  25. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +87 -0
  26. package/dist/src/cli.js +22 -17
  27. package/dist/src/cli.test.js +2 -2
  28. package/dist/src/commands/daemon.test.js +13 -13
  29. package/dist/src/constants.d.ts +2 -3
  30. package/dist/src/constants.js +3 -10
  31. package/dist/src/daemon.js +1 -5
  32. package/dist/src/doctor.js +12 -0
  33. package/dist/src/doctor.test.js +30 -3
  34. package/dist/src/execution.js +5 -3
  35. package/dist/src/external.js +19 -1
  36. package/dist/src/external.test.js +37 -1
  37. package/dist/src/main.js +0 -5
  38. package/dist/src/node-network.test.js +3 -3
  39. package/dist/src/runtime-identity.test.js +0 -8
  40. package/dist/src/runtime.d.ts +2 -0
  41. package/dist/src/runtime.js +1 -0
  42. package/package.json +2 -2
@@ -18,7 +18,7 @@ import { executePipeline } from './pipeline/index.js';
18
18
  import { adapterLoadError, ArgumentError, CommandExecutionError, attachTraceReceipt, getErrorMessage } from './errors.js';
19
19
  import { shouldUseBrowserSession } from './capabilityRouting.js';
20
20
  import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
21
- import { resolveProfileContextId } from './browser/profile.js';
21
+ import { profileRouteParams, resolveProfileSelection } from './browser/profile.js';
22
22
  import { setDaemonCommandTimeoutSeconds } from './browser/daemon-client.js';
23
23
  import { emitHook } from './hooks.js';
24
24
  import { log } from './logger.js';
@@ -211,7 +211,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
211
211
  }
212
212
  }
213
213
  const BrowserFactory = getBrowserFactory(cmd.site);
214
- const contextId = resolveProfileContextId(opts.profile);
214
+ const profileSelection = resolveProfileSelection(opts.profile);
215
+ const profileRouting = profileRouteParams(profileSelection);
216
+ const contextId = profileSelection?.contextId;
215
217
  const internal = cmd;
216
218
  const siteSession = resolveSiteSession(cmd, opts.siteSession);
217
219
  const session = resolveAdapterBrowserSession(cmd, siteSession);
@@ -332,7 +334,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
332
334
  await page.closeWindow?.().catch(() => { });
333
335
  throw err;
334
336
  }
335
- }, { session, cdpEndpoint, contextId, windowMode, surface: 'adapter', siteSession });
337
+ }, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession });
336
338
  }
337
339
  else {
338
340
  // Non-browser commands: enforce a timeout only when the command exposes
@@ -161,16 +161,34 @@ export function executeExternalCli(name, args, preloaded) {
161
161
  }
162
162
  }
163
163
  // 3. Passthrough execution with stdio inherited
164
- const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
164
+ const result = spawnPassthrough(cli.binary, args);
165
165
  if (result.error) {
166
166
  log.error(`Failed to execute '${cli.binary}': ${result.error.message}`);
167
167
  process.exitCode = EXIT_CODES.GENERIC_ERROR;
168
168
  return;
169
169
  }
170
+ if (result.signal) {
171
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
172
+ return;
173
+ }
170
174
  if (result.status !== null) {
171
175
  process.exitCode = result.status;
172
176
  }
173
177
  }
178
+ function quoteForCmdShell(token) {
179
+ if (token !== '' && !/[\s"^&|<>%()]/.test(token))
180
+ return token;
181
+ return `"${token.replace(/"/g, '""')}"`;
182
+ }
183
+ function spawnPassthrough(binary, args) {
184
+ const direct = spawnSync(binary, args, { stdio: 'inherit' });
185
+ const errorCode = direct.error?.code;
186
+ if (os.platform() === 'win32' && (errorCode === 'EINVAL' || errorCode === 'ENOENT')) {
187
+ const command = [binary, ...args].map(quoteForCmdShell).join(' ');
188
+ return spawnSync(command, { stdio: 'inherit', shell: true });
189
+ }
190
+ return direct;
191
+ }
174
192
  export function registerExternalCli(name, opts) {
175
193
  const userPath = getUserRegistryPath();
176
194
  const configDir = path.dirname(userPath);
@@ -18,7 +18,8 @@ vi.mock('node:os', async () => {
18
18
  platform: mockPlatform,
19
19
  };
20
20
  });
21
- import { formatExternalCliLabel, installExternalCli, parseCommand } from './external.js';
21
+ import { spawnSync } from 'node:child_process';
22
+ import { executeExternalCli, formatExternalCliLabel, installExternalCli, parseCommand } from './external.js';
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
24
  describe('parseCommand', () => {
24
25
  it('splits binaries and quoted arguments without invoking a shell', () => {
@@ -93,3 +94,38 @@ describe('installExternalCli', () => {
93
94
  expect(mockExecFileSync).toHaveBeenCalledTimes(1);
94
95
  });
95
96
  });
97
+ describe('executeExternalCli passthrough', () => {
98
+ const spawnMock = vi.mocked(spawnSync);
99
+ const cli = { name: 'tg', binary: 'tg', description: '' };
100
+ beforeEach(() => {
101
+ spawnMock.mockReset();
102
+ mockExecFileSync.mockReset();
103
+ mockExecFileSync.mockReturnValue(Buffer.from(''));
104
+ mockPlatform.mockReturnValue('darwin');
105
+ process.exitCode = undefined;
106
+ });
107
+ it('retries through the shell on Windows when the binary is a .cmd shim', () => {
108
+ mockPlatform.mockReturnValue('win32');
109
+ const einval = Object.assign(new Error('spawnSync tg EINVAL'), { code: 'EINVAL' });
110
+ spawnMock
111
+ .mockReturnValueOnce({ error: einval, status: null, signal: null })
112
+ .mockReturnValueOnce({ status: 0, signal: null });
113
+ executeExternalCli('tg', ['send', 'hello world', '--to', 'a"b'], [cli]);
114
+ expect(spawnMock).toHaveBeenCalledTimes(2);
115
+ expect(spawnMock).toHaveBeenNthCalledWith(1, 'tg', ['send', 'hello world', '--to', 'a"b'], { stdio: 'inherit' });
116
+ expect(spawnMock).toHaveBeenNthCalledWith(2, 'tg send "hello world" --to "a""b"', { stdio: 'inherit', shell: true });
117
+ expect(process.exitCode).toBe(0);
118
+ });
119
+ it('does not retry through the shell on non-Windows platforms', () => {
120
+ const einval = Object.assign(new Error('spawnSync tg EINVAL'), { code: 'EINVAL' });
121
+ spawnMock.mockReturnValueOnce({ error: einval, status: null, signal: null });
122
+ executeExternalCli('tg', [], [cli]);
123
+ expect(spawnMock).toHaveBeenCalledTimes(1);
124
+ expect(process.exitCode).toBe(1);
125
+ });
126
+ it('reports a non-zero exit code when the child dies from a signal', () => {
127
+ spawnMock.mockReturnValueOnce({ status: null, signal: 'SIGKILL' });
128
+ executeExternalCli('tg', [], [cli]);
129
+ expect(process.exitCode).toBe(1);
130
+ });
131
+ });
package/dist/src/main.js CHANGED
@@ -21,7 +21,6 @@ import { findPackageRoot, getCliManifestPath } from './package-paths.js';
21
21
  import { PKG_VERSION } from './version.js';
22
22
  import { EXIT_CODES } from './errors.js';
23
23
  import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_MAJOR } from './runtime-detect.js';
24
- import { unsupportedDaemonPortEnvMessage } from './constants.js';
25
24
  import { CONFIG_DIR_NAME } from './brand.js';
26
25
  const __filename = fileURLToPath(import.meta.url);
27
26
  const __dirname = path.dirname(__filename);
@@ -41,10 +40,6 @@ if (typeof globalThis.Bun === 'undefined' && !isSupportedNodeVersion(process.ver
41
40
  ].join('\n'));
42
41
  process.exit(EXIT_CODES.CONFIG_ERROR);
43
42
  }
44
- if (process.env.WEBCMD_DAEMON_PORT) {
45
- process.stderr.write(`error: ${unsupportedDaemonPortEnvMessage(process.env.WEBCMD_DAEMON_PORT)}\n`);
46
- process.exit(EXIT_CODES.CONFIG_ERROR);
47
- }
48
43
  // Fast path: --version (only when it's the top-level intent, not passed to a subcommand)
49
44
  // e.g. `webcmd --version` or `webcmd -V`, but NOT `webcmd gh --version`
50
45
  if (argv[0] === '--version' || argv[0] === '-V') {
@@ -22,9 +22,9 @@ describe('node network proxy decisions', () => {
22
22
  });
23
23
  it('bypasses proxies for loopback addresses', () => {
24
24
  const env = { https_proxy: 'http://127.0.0.1:7897', http_proxy: 'http://127.0.0.1:7897' };
25
- expect(decideProxy(new URL('http://127.0.0.1:19825/status'), env)).toEqual({ mode: 'direct' });
26
- expect(decideProxy(new URL('http://localhost:19825/status'), env)).toEqual({ mode: 'direct' });
27
- expect(decideProxy(new URL('http://[::1]:19825/status'), env)).toEqual({ mode: 'direct' });
25
+ expect(decideProxy(new URL('http://127.0.0.1:9777/status'), env)).toEqual({ mode: 'direct' });
26
+ expect(decideProxy(new URL('http://localhost:9777/status'), env)).toEqual({ mode: 'direct' });
27
+ expect(decideProxy(new URL('http://[::1]:9777/status'), env)).toEqual({ mode: 'direct' });
28
28
  });
29
29
  it('honors NO_PROXY domain matches', () => {
30
30
  const decision = decideProxy(new URL('https://api.example.com/v1/items'), {
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import * as path from 'node:path';
3
- import { unsupportedDaemonPortEnvMessage } from './constants.js';
4
3
  import { getUserWebcmdDir, getUserClisDir, getPluginsDir } from './discovery.js';
5
4
  describe('webcmd runtime identity', () => {
6
5
  it('uses webcmd runtime directories', async () => {
@@ -8,11 +7,4 @@ describe('webcmd runtime identity', () => {
8
7
  expect(getUserClisDir('/home/tester')).toBe(path.join('/home/tester', '.webcmd', 'clis'));
9
8
  expect(getPluginsDir('/home/tester')).toBe(path.join('/home/tester', '.webcmd', 'plugins'));
10
9
  });
11
- it('reports unsupported daemon port with WEBCMD env names', () => {
12
- const legacyEnvName = ['OPEN', 'CLI_DAEMON_PORT'].join('');
13
- expect(unsupportedDaemonPortEnvMessage('1234')).toContain('WEBCMD_DAEMON_PORT');
14
- expect(unsupportedDaemonPortEnvMessage('1234')).toContain('Webcmd');
15
- expect(unsupportedDaemonPortEnvMessage('1234')).toContain('rerun webcmd');
16
- expect(unsupportedDaemonPortEnvMessage('1234')).not.toContain(legacyEnvName);
17
- });
18
10
  });
@@ -29,6 +29,7 @@ export interface IBrowserFactory {
29
29
  session?: string;
30
30
  cdpEndpoint?: string;
31
31
  contextId?: string;
32
+ preferredContextId?: string;
32
33
  idleTimeout?: number;
33
34
  windowMode?: BrowserWindowMode;
34
35
  surface?: BrowserSurface;
@@ -40,6 +41,7 @@ export declare function browserSession<T>(BrowserFactory: new () => IBrowserFact
40
41
  session?: string;
41
42
  cdpEndpoint?: string;
42
43
  contextId?: string;
44
+ preferredContextId?: string;
43
45
  idleTimeout?: number;
44
46
  windowMode?: BrowserWindowMode;
45
47
  surface?: BrowserSurface;
@@ -41,6 +41,7 @@ export async function browserSession(BrowserFactory, fn, opts = {}) {
41
41
  session: opts.session,
42
42
  cdpEndpoint: opts.cdpEndpoint,
43
43
  contextId: opts.contextId,
44
+ preferredContextId: opts.preferredContextId,
44
45
  idleTimeout: opts.idleTimeout,
45
46
  windowMode: opts.windowMode,
46
47
  surface: opts.surface,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrhq/webcmd",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Turn websites, browser sessions, desktop apps, and local tools into deterministic CLI surfaces for humans and AI agents.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -48,7 +48,7 @@
48
48
  "generate-release-notes": "tsx scripts/generate-release-notes.ts",
49
49
  "start": "node dist/src/main.js",
50
50
  "start:bun": "bun dist/src/main.js",
51
- "preuninstall": "node -e \"fetch('http://127.0.0.1:19825/shutdown',{method:'POST',headers:{'X-Webcmd':'1'},signal:AbortSignal.timeout(3000)}).catch(()=>{})\" || true",
51
+ "preuninstall": "node -e \"fetch('http://127.0.0.1:9777/shutdown',{method:'POST',headers:{'X-Webcmd':'1'},signal:AbortSignal.timeout(3000)}).catch(()=>{})\" || true",
52
52
  "postinstall": "node scripts/postinstall.js || true; node scripts/fetch-adapters.js || true",
53
53
  "typecheck": "tsc --noEmit",
54
54
  "prepare": "[ -d src ] && npm run build || true",