@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.
- package/README.md +23 -11
- package/cli-manifest.json +2 -0
- package/clis/twitter/delete.js +14 -6
- package/clis/twitter/delete.test.js +74 -1
- package/clis/twitter/timeline.js +3 -1
- package/clis/twitter/timeline.test.js +4 -0
- package/dist/src/browser/bridge-readiness.test.js +1 -1
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +6 -4
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +7 -2
- package/dist/src/browser/daemon-client.test.js +33 -30
- package/dist/src/browser/daemon-transport.js +3 -19
- package/dist/src/browser/page.d.ts +2 -1
- package/dist/src/browser/page.js +5 -1
- package/dist/src/browser/profile.d.ts +9 -0
- package/dist/src/browser/profile.js +18 -7
- package/dist/src/browser/profile.test.d.ts +1 -0
- package/dist/src/browser/profile.test.js +44 -0
- package/dist/src/browser/protocol.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +24 -10
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +3 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +87 -0
- package/dist/src/cli.js +22 -17
- package/dist/src/cli.test.js +2 -2
- package/dist/src/commands/daemon.test.js +13 -13
- package/dist/src/constants.d.ts +2 -3
- package/dist/src/constants.js +3 -10
- package/dist/src/daemon.js +1 -5
- package/dist/src/doctor.js +12 -0
- package/dist/src/doctor.test.js +30 -3
- package/dist/src/execution.js +5 -3
- package/dist/src/external.js +19 -1
- package/dist/src/external.test.js +37 -1
- package/dist/src/main.js +0 -5
- package/dist/src/node-network.test.js +3 -3
- package/dist/src/runtime-identity.test.js +0 -8
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/package.json +2 -2
package/dist/src/execution.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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,
|
|
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
|
package/dist/src/external.js
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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:
|
|
26
|
-
expect(decideProxy(new URL('http://localhost:
|
|
27
|
-
expect(decideProxy(new URL('http://[::1]:
|
|
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
|
});
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/runtime.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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",
|