@opice/browser 0.1.0

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.
@@ -0,0 +1,9 @@
1
+ import { type Command } from '@opice/harness';
2
+ export declare const builtins: Command[];
3
+ /**
4
+ * Positional-argument hints for built-ins, so the ergonomic
5
+ * `opice-browser click add` works alongside `--selector add`. User verbs from
6
+ * browser-tools.ts are flag-only.
7
+ */
8
+ export declare const positionalHints: Record<string, string[]>;
9
+ //# sourceMappingURL=builtins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,OAAO,EAAmB,MAAM,gBAAgB,CAAA;AA0C1E,eAAO,MAAM,QAAQ,EAAE,OAAO,EA8E7B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAcpD,CAAA"}
@@ -0,0 +1,117 @@
1
+ import { command, z } from '@opice/harness';
2
+ /**
3
+ * Built-in browser verbs for the authoring CLI, defined with the same
4
+ * `command()` primitive a repo's `browser-tools.ts` uses. The agent drives the
5
+ * app through these during the dry-run; the test it writes uses the matching
6
+ * harness DSL — same vocabulary, same Playwright backing.
7
+ */
8
+ /** Apply an action to a resolved locator (shared by byRole/byLabel/byText). */
9
+ async function applyAction(locator, action, opts) {
10
+ switch (action) {
11
+ case 'click':
12
+ return locator.click();
13
+ case 'fill':
14
+ return locator.fill(opts.value ?? '');
15
+ case 'focus':
16
+ return locator.focus();
17
+ case 'hover':
18
+ return locator.hover();
19
+ case 'press':
20
+ return locator.press(opts.key ?? 'Enter');
21
+ case 'text':
22
+ return locator.textContent();
23
+ case 'count':
24
+ return locator.count();
25
+ default:
26
+ throw new Error(`Unknown action "${action}" (click|fill|focus|hover|press|text|count)`);
27
+ }
28
+ }
29
+ const actionFields = {
30
+ action: z.string().default('click'),
31
+ value: z.string().optional(),
32
+ key: z.string().optional(),
33
+ };
34
+ export const builtins = [
35
+ command('open', z.object({ url: z.string() }), async ({ page }, { url }) => {
36
+ await page.goto(url);
37
+ return page.url();
38
+ }, 'Navigate to a URL'),
39
+ command('reload', z.object({}), async ({ page }) => {
40
+ await page.reload();
41
+ return page.url();
42
+ }, 'Reload the current page'),
43
+ command('back', z.object({}), async ({ page }) => {
44
+ await page.goBack();
45
+ return page.url();
46
+ }, 'Go back in history'),
47
+ command('forward', z.object({}), async ({ page }) => {
48
+ await page.goForward();
49
+ return page.url();
50
+ }, 'Go forward in history'),
51
+ command('click', z.object({ selector: z.string() }), async ({ el }, { selector }) => {
52
+ await el(selector).click();
53
+ }, 'Click an element (test-id or CSS selector)'),
54
+ command('fill', z.object({ selector: z.string(), value: z.string() }), async ({ el }, { selector, value }) => {
55
+ await el(selector).fill(value);
56
+ }, 'Fill an input/textarea'),
57
+ command('press', z.object({ key: z.string(), selector: z.string().optional() }), async ({ el, page }, { key, selector }) => {
58
+ if (selector)
59
+ await el(selector).press(key);
60
+ else
61
+ await page.keyboard.press(key);
62
+ }, 'Press a key (optionally focusing a selector first)'),
63
+ command('hover', z.object({ selector: z.string() }), async ({ el }, { selector }) => {
64
+ await el(selector).hover();
65
+ }, 'Hover an element'),
66
+ command('text', z.object({ selector: z.string() }), async ({ el }, { selector }) => {
67
+ return el(selector).textContent();
68
+ }, 'Read an element\'s textContent'),
69
+ command('value', z.object({ selector: z.string() }), async ({ el }, { selector }) => {
70
+ return el(selector).inputValue();
71
+ }, 'Read an input\'s value'),
72
+ command('count', z.object({ selector: z.string() }), async ({ el }, { selector }) => {
73
+ return el(selector).count();
74
+ }, 'Count elements matching a selector'),
75
+ command('byRole', z.object({ role: z.string(), name: z.string().optional(), ...actionFields }), async (ctx, { role, name, action, value, key }) => {
76
+ return applyAction(ctx.byRole(role, name), action, { value, key });
77
+ }, 'Resolve by ARIA role (+optional --name) and run --action (default click)'),
78
+ command('byLabel', z.object({ label: z.string(), ...actionFields }), async (ctx, { label, action, value, key }) => {
79
+ return applyAction(ctx.byLabel(label), action, { value, key });
80
+ }, 'Resolve a form control by its <label> and run --action'),
81
+ command('byText', z.object({ text: z.string(), ...actionFields }), async (ctx, { text, action, value, key }) => {
82
+ return applyAction(ctx.byText(text), action, { value, key });
83
+ }, 'Resolve by visible text and run --action'),
84
+ command('aria-snapshot', z.object({ selector: z.string().optional() }), async ({ page }, { selector }) => {
85
+ const root = selector ? page.locator(selector) : page.locator('body');
86
+ return root.ariaSnapshot();
87
+ }, 'Print the ARIA accessibility tree (YAML) — the agent\'s view of the page'),
88
+ command('screenshot', z.object({ path: z.string().optional() }), async ({ page }, { path }) => {
89
+ const target = path ?? `/tmp/opice-browser-${Date.now()}.png`;
90
+ await page.screenshot({ path: target });
91
+ return target;
92
+ }, 'Capture a screenshot'),
93
+ command('title', z.object({}), async ({ page }) => page.title(), 'Read the document title'),
94
+ command('url', z.object({}), async ({ page }) => page.url(), 'Read the current URL'),
95
+ command('eval', z.object({ js: z.string() }), async ({ page }, { js }) => page.evaluate(js), 'Evaluate JS in the page'),
96
+ ];
97
+ /**
98
+ * Positional-argument hints for built-ins, so the ergonomic
99
+ * `opice-browser click add` works alongside `--selector add`. User verbs from
100
+ * browser-tools.ts are flag-only.
101
+ */
102
+ export const positionalHints = {
103
+ open: ['url'],
104
+ click: ['selector'],
105
+ fill: ['selector', 'value'],
106
+ press: ['key'],
107
+ hover: ['selector'],
108
+ text: ['selector'],
109
+ value: ['selector'],
110
+ count: ['selector'],
111
+ byRole: ['role', 'action'],
112
+ byLabel: ['label', 'action'],
113
+ byText: ['text', 'action'],
114
+ 'aria-snapshot': ['selector'],
115
+ eval: ['js'],
116
+ };
117
+ //# sourceMappingURL=builtins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtins.js","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { loadUserCommands, runCommand, z } from '@opice/harness';
5
+ import { builtins, positionalHints } from './builtins.js';
6
+ import { launch, quit, sessionAlive, setSessionName, withPage } from './session.js';
7
+ // opice-browser must run under Node: Playwright's `connectOverCDP` websocket
8
+ // can't complete its handshake under Bun. However it gets launched (bunx, a
9
+ // bun-created bin shim, `bun run`), re-exec under Node so the verb always works.
10
+ function reexecUnderNodeIfBun() {
11
+ if (!process.versions['bun'])
12
+ return;
13
+ const result = spawnSync('node', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], { stdio: 'inherit' });
14
+ if (result.error) {
15
+ console.error('[opice-browser] requires Node on PATH (Playwright CDP does not work under Bun):', result.error.message);
16
+ process.exit(127);
17
+ }
18
+ process.exit(result.status ?? 1);
19
+ }
20
+ const HELP = `opice-browser — stateful Playwright browser for opice authoring
21
+
22
+ Usage: opice-browser [--session NAME] <command> [positionals] [--flag value]
23
+
24
+ Sessions: each named session is its own browser (default: "default", or
25
+ $OPICE_BROWSER_SESSION). opice-batch gives each parallel author its own.
26
+
27
+ Lifecycle:
28
+ launch [url] [--headed] Start the persistent browser (idempotent).
29
+ status Show whether a session is alive.
30
+ quit Close the browser and clear the session.
31
+
32
+ Inspect:
33
+ commands List all verbs (built-in + browser-tools.ts).
34
+ aria-snapshot [selector] Print the ARIA tree (the agent's view).
35
+
36
+ Verbs (examples):
37
+ open <url>
38
+ click <selector> fill <selector> <value>
39
+ byRole <role> [action] --name X byLabel <label> [action]
40
+ text <selector> press <key> [--selector s]
41
+
42
+ Selectors: a bare word is a data-testid; anything with CSS chars is raw CSS.
43
+ Verbs from <repo>/browser-tools.ts are available too (flag or positional args).
44
+ `;
45
+ function parseArgs(tokens) {
46
+ const flags = {};
47
+ const positionals = [];
48
+ for (let i = 0; i < tokens.length; i++) {
49
+ const token = tokens[i];
50
+ if (token.startsWith('--')) {
51
+ const eq = token.indexOf('=');
52
+ if (eq >= 0) {
53
+ flags[token.slice(2, eq)] = token.slice(eq + 1);
54
+ }
55
+ else {
56
+ const key = token.slice(2);
57
+ const next = tokens[i + 1];
58
+ if (next !== undefined && !next.startsWith('--')) {
59
+ flags[key] = next;
60
+ i++;
61
+ }
62
+ else {
63
+ flags[key] = true;
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ positionals.push(token);
69
+ }
70
+ }
71
+ return { flags, positionals };
72
+ }
73
+ /** Field names for positional mapping: explicit hint, else object-schema keys. */
74
+ function positionalNames(name, cmd) {
75
+ if (positionalHints[name])
76
+ return positionalHints[name];
77
+ if (cmd.params instanceof z.ZodObject)
78
+ return Object.keys(cmd.params.shape);
79
+ return [];
80
+ }
81
+ function paramSummary(cmd) {
82
+ if (cmd.params instanceof z.ZodObject) {
83
+ const keys = Object.keys(cmd.params.shape);
84
+ return keys.length ? keys.map((k) => `<${k}>`).join(' ') : '(no args)';
85
+ }
86
+ return '';
87
+ }
88
+ async function buildRegistry() {
89
+ const registry = new Map();
90
+ for (const cmd of builtins)
91
+ registry.set(cmd.name, cmd);
92
+ const user = await loadUserCommands();
93
+ for (const [name, cmd] of user)
94
+ registry.set(name, cmd); // user verbs override built-ins
95
+ return registry;
96
+ }
97
+ function printResult(result) {
98
+ if (result === undefined || result === null) {
99
+ console.log('ok');
100
+ }
101
+ else if (typeof result === 'string') {
102
+ console.log(result);
103
+ }
104
+ else {
105
+ console.log(JSON.stringify(result));
106
+ }
107
+ }
108
+ /** Consume a leading `--session NAME` / `--session=NAME` (else env / default). */
109
+ function takeSession(argv) {
110
+ const first = argv[0];
111
+ if (first === '--session' && argv[1] !== undefined) {
112
+ setSessionName(argv[1]);
113
+ return argv.slice(2);
114
+ }
115
+ if (first?.startsWith('--session=')) {
116
+ setSessionName(first.slice('--session='.length));
117
+ return argv.slice(1);
118
+ }
119
+ return argv;
120
+ }
121
+ async function main(rawArgv) {
122
+ reexecUnderNodeIfBun();
123
+ const [name, ...rest] = takeSession(rawArgv);
124
+ const { flags, positionals } = parseArgs(rest);
125
+ switch (name) {
126
+ case undefined:
127
+ case 'help':
128
+ case '--help':
129
+ case '-h':
130
+ console.log(HELP);
131
+ return 0;
132
+ case 'launch': {
133
+ const session = await launch({ headed: !!flags['headed'], url: positionals[0] });
134
+ console.error(`[opice-browser] session up (pid ${session.pid}, port ${session.port})`);
135
+ return 0;
136
+ }
137
+ case 'status': {
138
+ const session = sessionAlive();
139
+ console.log(session ? `alive (pid ${session.pid}, port ${session.port})` : 'no session');
140
+ return session ? 0 : 1;
141
+ }
142
+ case 'quit':
143
+ case 'close': {
144
+ quit();
145
+ console.error('[opice-browser] session closed');
146
+ return 0;
147
+ }
148
+ case 'commands': {
149
+ const registry = await buildRegistry();
150
+ for (const cmd of registry.values()) {
151
+ const builtin = builtins.some((b) => b.name === cmd.name);
152
+ const tag = builtin ? '' : ' (user)';
153
+ console.log(`${cmd.name} ${paramSummary(cmd)}${tag}\n ${cmd.description ?? ''}`.trimEnd());
154
+ }
155
+ return 0;
156
+ }
157
+ default: {
158
+ const registry = await buildRegistry();
159
+ const cmd = registry.get(name);
160
+ if (!cmd) {
161
+ console.error(`Unknown command: ${name}\n`);
162
+ console.error(HELP);
163
+ return 1;
164
+ }
165
+ const names = positionalNames(name, cmd);
166
+ const args = { ...flags };
167
+ positionals.forEach((val, i) => {
168
+ const key = names[i];
169
+ if (key && !(key in args))
170
+ args[key] = val;
171
+ });
172
+ try {
173
+ const result = await withPage((page) => runCommand(page, cmd, args));
174
+ printResult(result);
175
+ return 0;
176
+ }
177
+ catch (err) {
178
+ console.error(`[opice-browser] ${name} failed: ${err instanceof Error ? err.message : String(err)}`);
179
+ return 1;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ process.exit(await main(process.argv.slice(2)));
185
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type Page } from 'playwright';
2
+ export declare function setSessionName(name: string): void;
3
+ export interface Session {
4
+ pid: number;
5
+ port: number;
6
+ wsEndpoint: string;
7
+ userDataDir: string;
8
+ }
9
+ export declare function readSession(): Session;
10
+ /** Is there a live session whose browser process is still running? */
11
+ export declare function sessionAlive(): Session | null;
12
+ export interface LaunchOptions {
13
+ headed?: boolean;
14
+ url?: string;
15
+ }
16
+ /** Launch the persistent browser and record the session. Returns the session. */
17
+ export declare function launch(opts?: LaunchOptions): Promise<Session>;
18
+ /** Kill the persistent browser and clear the session. */
19
+ export declare function quit(): void;
20
+ /**
21
+ * Connect to the live browser, hand its current page to `fn`, then disconnect.
22
+ * Disconnecting (`browser.close()` on a CDP connection) does NOT terminate the
23
+ * browser process — the page state persists for the next verb.
24
+ */
25
+ export declare function withPage<T>(fn: (page: Page) => Promise<T>): Promise<T>;
26
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAKA,OAAO,EAA0B,KAAK,IAAI,EAAE,MAAM,YAAY,CAAA;AAwB9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAUD,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,IAAI,OAAO,CAMrC;AAWD,sEAAsE;AACtE,wBAAgB,YAAY,IAAI,OAAO,GAAG,IAAI,CAU7C;AA+BD,MAAM,WAAW,aAAa;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAED,iFAAiF;AACjF,wBAAsB,MAAM,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBvE;AAED,yDAAyD;AACzD,wBAAgB,IAAI,IAAI,IAAI,CAU3B;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAW5E"}
@@ -0,0 +1,148 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createServer } from 'node:net';
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { chromium } from 'playwright';
7
+ /**
8
+ * Persistent-browser session for CLI-over-CDP authoring.
9
+ *
10
+ * `launch` spawns a real Chrome (the Playwright-managed binary) with a
11
+ * remote-debugging port, **detached** so it outlives the launching command,
12
+ * and records the CDP endpoint in a session file. Each subsequent verb
13
+ * (`open`, `click`, …) connects over CDP, drives the live page, and
14
+ * disconnects — statefulness lives in the browser process, not in a
15
+ * long-running Node daemon. `connectOverCDP` is why the CLI runs on Node and
16
+ * not Bun (Bun's websocket client can't complete the CDP handshake).
17
+ */
18
+ const SESSION_DIR = path.join(tmpdir(), 'opice-browser');
19
+ /**
20
+ * Active session name. Multiple named sessions can run side by side, each its
21
+ * own browser — `opice-batch` gives every parallel author its own so they
22
+ * don't share a page. Set via `--session NAME` or `OPICE_BROWSER_SESSION`;
23
+ * defaults to `default`.
24
+ */
25
+ let sessionName = process.env['OPICE_BROWSER_SESSION'] ?? 'default';
26
+ export function setSessionName(name) {
27
+ sessionName = name;
28
+ }
29
+ function sessionFile() {
30
+ return path.join(SESSION_DIR, `${sessionName}.json`);
31
+ }
32
+ function profileDir() {
33
+ return path.join(SESSION_DIR, `profile-${sessionName}`);
34
+ }
35
+ export function readSession() {
36
+ const file = sessionFile();
37
+ if (!existsSync(file)) {
38
+ throw new Error(`No browser session "${sessionName}". Run \`opice-browser launch\` first.`);
39
+ }
40
+ return JSON.parse(readFileSync(file, 'utf-8'));
41
+ }
42
+ function writeSession(session) {
43
+ mkdirSync(SESSION_DIR, { recursive: true });
44
+ writeFileSync(sessionFile(), JSON.stringify(session), 'utf-8');
45
+ }
46
+ function clearSession() {
47
+ rmSync(sessionFile(), { force: true });
48
+ }
49
+ /** Is there a live session whose browser process is still running? */
50
+ export function sessionAlive() {
51
+ if (!existsSync(sessionFile()))
52
+ return null;
53
+ const session = readSession();
54
+ try {
55
+ process.kill(session.pid, 0);
56
+ return session;
57
+ }
58
+ catch {
59
+ clearSession();
60
+ return null;
61
+ }
62
+ }
63
+ async function freePort() {
64
+ return new Promise((resolve, reject) => {
65
+ const srv = createServer();
66
+ srv.once('error', reject);
67
+ srv.listen(0, '127.0.0.1', () => {
68
+ const addr = srv.address();
69
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
70
+ srv.close(() => resolve(port));
71
+ });
72
+ });
73
+ }
74
+ async function waitForEndpoint(port, timeoutMs = 15_000) {
75
+ const deadline = Date.now() + timeoutMs;
76
+ while (Date.now() < deadline) {
77
+ try {
78
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`);
79
+ if (res.ok) {
80
+ const body = (await res.json());
81
+ if (body.webSocketDebuggerUrl)
82
+ return body.webSocketDebuggerUrl;
83
+ }
84
+ }
85
+ catch {
86
+ // not up yet
87
+ }
88
+ await new Promise((r) => setTimeout(r, 100));
89
+ }
90
+ throw new Error(`Chrome did not expose a CDP endpoint on port ${port} within ${timeoutMs}ms`);
91
+ }
92
+ /** Launch the persistent browser and record the session. Returns the session. */
93
+ export async function launch(opts = {}) {
94
+ const existing = sessionAlive();
95
+ if (existing)
96
+ return existing;
97
+ const port = await freePort();
98
+ const userDataDir = profileDir();
99
+ mkdirSync(userDataDir, { recursive: true });
100
+ const args = [
101
+ `--remote-debugging-port=${port}`,
102
+ `--user-data-dir=${userDataDir}`,
103
+ '--no-first-run',
104
+ '--no-default-browser-check',
105
+ ...(opts.headed ? [] : ['--headless=new']),
106
+ opts.url ?? 'about:blank',
107
+ ];
108
+ const child = spawn(chromium.executablePath(), args, { detached: true, stdio: 'ignore' });
109
+ child.unref();
110
+ if (child.pid == null)
111
+ throw new Error('Failed to spawn Chrome');
112
+ const wsEndpoint = await waitForEndpoint(port);
113
+ const session = { pid: child.pid, port, wsEndpoint, userDataDir };
114
+ writeSession(session);
115
+ return session;
116
+ }
117
+ /** Kill the persistent browser and clear the session. */
118
+ export function quit() {
119
+ const session = sessionAlive();
120
+ if (session) {
121
+ try {
122
+ process.kill(session.pid);
123
+ }
124
+ catch {
125
+ // already gone
126
+ }
127
+ }
128
+ clearSession();
129
+ }
130
+ /**
131
+ * Connect to the live browser, hand its current page to `fn`, then disconnect.
132
+ * Disconnecting (`browser.close()` on a CDP connection) does NOT terminate the
133
+ * browser process — the page state persists for the next verb.
134
+ */
135
+ export async function withPage(fn) {
136
+ const session = readSession();
137
+ let browser = null;
138
+ try {
139
+ browser = await chromium.connectOverCDP(session.wsEndpoint);
140
+ const context = browser.contexts()[0] ?? (await browser.newContext());
141
+ const page = context.pages()[0] ?? (await context.newPage());
142
+ return await fn(page);
143
+ }
144
+ finally {
145
+ await browser?.close();
146
+ }
147
+ }
148
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@opice/browser",
3
+ "version": "0.1.0",
4
+ "description": "opice-browser — stateful Playwright browser for opice test authoring (CLI over CDP)",
5
+ "type": "module",
6
+ "bin": {
7
+ "opice-browser": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc --build",
14
+ "typecheck": "tsc --build"
15
+ },
16
+ "dependencies": {
17
+ "@opice/harness": "^0.1.0",
18
+ "playwright": "^1.60.0",
19
+ "zod": "^4.4.3"
20
+ },
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/contember/opice.git",
25
+ "directory": "packages/browser"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.9.1"
32
+ }
33
+ }