@deepraxis/sdk 0.1.1-dev.d25d171 → 0.1.1-dev.d3030b1

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 (36) hide show
  1. package/METHODS.md +640 -47
  2. package/README.md +3 -3
  3. package/dist/automation.d.ts +61 -0
  4. package/dist/core/client.d.ts +11 -1
  5. package/dist/core/client.js +13 -0
  6. package/dist/core/errors.d.ts +6 -0
  7. package/dist/core/errors.js +8 -0
  8. package/dist/core/transport.d.ts +9 -0
  9. package/dist/core/transport.js +13 -2
  10. package/dist/generated/index.d.ts +2 -2
  11. package/dist/generated/index.js +1 -1
  12. package/dist/generated/sdk.gen.d.ts +1644 -317
  13. package/dist/generated/sdk.gen.js +4882 -1067
  14. package/dist/generated/types.gen.d.ts +56812 -35322
  15. package/dist/helpers/application-build.d.ts +10 -0
  16. package/dist/helpers/application-build.js +98 -0
  17. package/dist/helpers/application-publish.d.ts +8 -0
  18. package/dist/helpers/application-publish.js +26 -9
  19. package/dist/helpers/browser-actions.d.ts +84 -0
  20. package/dist/helpers/browser-actions.js +141 -0
  21. package/dist/helpers/browser-connect.d.ts +102 -0
  22. package/dist/helpers/browser-connect.js +399 -0
  23. package/dist/helpers/browser-decisions.d.ts +88 -0
  24. package/dist/helpers/browser-decisions.js +137 -0
  25. package/dist/helpers/browser-documentation.d.ts +2 -0
  26. package/dist/helpers/browser-documentation.js +56 -0
  27. package/dist/helpers/file-upload.d.ts +2 -2
  28. package/dist/helpers/sandbox-templates.d.ts +121 -0
  29. package/dist/helpers/sandbox-templates.js +60 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +3 -0
  32. package/dist/operations.generated.d.ts +10375 -4269
  33. package/dist/operations.generated.js +12821 -5218
  34. package/dist/resources.generated.d.ts +906 -463
  35. package/dist/resources.generated.js +397 -26
  36. package/package.json +2 -2
@@ -0,0 +1,10 @@
1
+ import type { PublishInput } from './application-publish.js';
2
+ /** A vite/vinext build read into memory: paths are relative to dist/server and dist/client. */
3
+ export interface BuildOutput {
4
+ serverFiles: Record<string, Uint8Array>;
5
+ clientFiles: Record<string, Uint8Array>;
6
+ /** Parsed dist/server/wrangler.json. */
7
+ wranglerJson: unknown;
8
+ }
9
+ /** Pure mapping from a vite/vinext build (dist/server + dist/client + wrangler.json) to `praxis.application.publish` input. No fs, no network. */
10
+ export declare function publishInputFromBuild(build: BuildOutput, name: string): PublishInput;
@@ -0,0 +1,98 @@
1
+ import { PraxisError } from "../core/errors.js";
2
+ // wrangler.json keys that declare a binding the platform does not accept (it answers UNSUPPORTED).
3
+ const UNSUPPORTED_BINDING_KEYS = ['services', 'queues', 'durable_objects', 'ai', 'analytics_engine_datasets', 'vectorize', 'hyperdrive',
4
+ 'browser', 'send_email', 'mtls_certificates', 'dispatch_namespaces', 'unsafe', 'version_metadata', 'images', 'workflows', 'pipelines',
5
+ 'secrets_store_secrets', 'ratelimits', 'vpc_services', 'media', 'tail_consumers', 'worker_loaders'];
6
+ // A key declares something only if it holds a value somewhere inside: vite's generated wrangler.json writes
7
+ // empty containers such as `queues: { producers: [], consumers: [] }` and `durable_objects: { bindings: [] }`.
8
+ const isPresent = (value) => {
9
+ if (value === undefined || value === null)
10
+ return false;
11
+ if (Array.isArray(value))
12
+ return value.some(isPresent);
13
+ if (typeof value === 'object')
14
+ return Object.values(value).some(isPresent);
15
+ return true;
16
+ };
17
+ /** Compile one .assetsignore line (gitignore subset: names, `*`, `**`, leading /, trailing /) to a matcher over a relative path. */
18
+ function ignoreMatcher(line) {
19
+ let pattern = line.trim();
20
+ if (!pattern || pattern.startsWith('#') || pattern.startsWith('!'))
21
+ return null;
22
+ const anchored = pattern.startsWith('/') || pattern.slice(0, -1).includes('/');
23
+ pattern = pattern.replace(/^\//, '').replace(/\/$/, '');
24
+ const source = pattern.split('**').map(part => part.split('*').map(piece => piece.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')).join('.*');
25
+ const regex = new RegExp(`^${source}$`);
26
+ return path => {
27
+ const segments = path.split('/');
28
+ // The path or any of its parent directories matches, as in gitignore.
29
+ for (let end = 1; end <= segments.length; end++) {
30
+ if (anchored ? regex.test(segments.slice(0, end).join('/')) : regex.test(segments[end - 1]))
31
+ return true;
32
+ }
33
+ return false;
34
+ };
35
+ }
36
+ function bindingsFrom(wrangler) {
37
+ const bindings = [];
38
+ const fromList = (key, type) => {
39
+ const list = wrangler[key];
40
+ if (!isPresent(list))
41
+ return;
42
+ if (!Array.isArray(list))
43
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json ${key} must be an array` });
44
+ for (const entry of list) {
45
+ if (typeof entry?.binding !== 'string')
46
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json ${key} entry has no binding name` });
47
+ bindings.push({ type, name: entry.binding });
48
+ }
49
+ };
50
+ fromList('d1_databases', 'd1');
51
+ fromList('kv_namespaces', 'kv_namespace');
52
+ fromList('r2_buckets', 'r2_bucket');
53
+ for (const [name, value] of Object.entries((wrangler.vars ?? {}))) {
54
+ bindings.push((typeof value === 'string' ? { type: 'plain_text', name, text: value } : { type: 'json', name, json: value }));
55
+ }
56
+ if (typeof wrangler.assets?.binding === 'string')
57
+ bindings.push({ type: 'assets', name: wrangler.assets.binding });
58
+ return bindings;
59
+ }
60
+ /** Pure mapping from a vite/vinext build (dist/server + dist/client + wrangler.json) to `praxis.application.publish` input. No fs, no network. */
61
+ export function publishInputFromBuild(build, name) {
62
+ const wrangler = build.wranglerJson;
63
+ if (!wrangler || typeof wrangler !== 'object' || Array.isArray(wrangler)) {
64
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'wranglerJson must be the parsed dist/server/wrangler.json object' });
65
+ }
66
+ const offending = UNSUPPORTED_BINDING_KEYS.filter(key => isPresent(wrangler[key]));
67
+ if (offending.length) {
68
+ throw new PraxisError({ code: 'UNSUPPORTED', message: `wrangler.json declares bindings the platform does not support: ${offending.join(', ')}` });
69
+ }
70
+ const modules = Object.entries(build.serverFiles)
71
+ .filter(([path]) => path.endsWith('.js'))
72
+ .map(([path, content]) => ({ name: path, content, content_type: 'application/javascript+module' }));
73
+ if (typeof wrangler.main !== 'string')
74
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'wrangler.json has no main entry' });
75
+ if (!modules.some(module => module.name === wrangler.main)) {
76
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: `wrangler.json main ${wrangler.main} is not a .js file under dist/server` });
77
+ }
78
+ const ignored = new TextDecoder().decode(build.clientFiles['.assetsignore'] ?? new Uint8Array()).split(/\r?\n/)
79
+ .map(ignoreMatcher).filter((matcher) => matcher !== null);
80
+ const files = {};
81
+ for (const [path, content] of Object.entries(build.clientFiles)) {
82
+ if (path === '.assetsignore' || ignored.some(matches => matches(path)))
83
+ continue;
84
+ files[`/${path}`] = content;
85
+ }
86
+ const { binding: _binding, directory: _directory, ...assetsConfig } = (wrangler.assets ?? {});
87
+ const bindings = bindingsFrom(wrangler);
88
+ return {
89
+ name,
90
+ files,
91
+ modules,
92
+ main_module: wrangler.main,
93
+ ...(typeof wrangler.compatibility_date === 'string' ? { compatibility_date: wrangler.compatibility_date } : {}),
94
+ ...(Array.isArray(wrangler.compatibility_flags) ? { compatibility_flags: wrangler.compatibility_flags } : {}),
95
+ ...(bindings.length ? { bindings } : {}),
96
+ ...(Object.keys(assetsConfig).length ? { assets_config: assetsConfig } : {}),
97
+ };
98
+ }
@@ -12,6 +12,14 @@ export interface PublishInput {
12
12
  content: string;
13
13
  content_type?: string;
14
14
  };
15
+ /** Multi-module Worker (e.g. a vite/vinext build). Names pass through unchanged; relative paths with / are legal. Mutually exclusive with main. */
16
+ modules?: {
17
+ name: string;
18
+ content: string | Uint8Array;
19
+ content_type?: string;
20
+ }[];
21
+ /** Entry module name; defaults to the first module and must name one of modules. */
22
+ main_module?: string;
15
23
  /** 运行时兼容日期,默认当天 UTC(YYYY-MM-DD)。 */
16
24
  compatibility_date?: string;
17
25
  /** 运行时兼容标志,原样传递;平台固定附加 global_fetch_strictly_public。 */
@@ -21,11 +21,30 @@ export async function publish(client, input) {
21
21
  const timeout = input.timeout_ms ?? 120_000;
22
22
  if (!Number.isFinite(timeout) || timeout < 0)
23
23
  throw new RangeError('timeout_ms must be non-negative and finite');
24
- const contentType = ['application/javascript+module', 'text/javascript', 'application/javascript', 'application/json',
25
- 'text/plain', 'application/wasm', 'application/octet-stream', 'application/x-python']
26
- .find(type => type === (input.main?.content_type ?? 'application/javascript+module'));
27
- if (!contentType)
28
- throw new PraxisError({ code: 'INVALID_PARAMS', message: `Unsupported module content_type: ${input.main?.content_type}` });
24
+ const moduleTypes = ['application/javascript+module', 'text/javascript', 'application/javascript', 'application/json',
25
+ 'text/plain', 'application/wasm', 'application/octet-stream', 'application/x-python'];
26
+ const moduleType = (type) => {
27
+ const found = moduleTypes.find(candidate => candidate === (type ?? 'application/javascript+module'));
28
+ if (!found)
29
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: `Unsupported module content_type: ${type}` });
30
+ return found;
31
+ };
32
+ if (input.main && input.modules)
33
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'Pass either main or modules, not both' });
34
+ if (input.main_module !== undefined && !input.modules)
35
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'main_module requires modules; with main the entry is main.name' });
36
+ if (input.modules && input.modules.length === 0)
37
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'modules must not be empty' });
38
+ const entryModules = input.main
39
+ ? [{ name: input.main.name, content_type: moduleType(input.main.content_type), content_base64: base64(new TextEncoder().encode(input.main.content)) }]
40
+ : input.modules?.map(module => ({ name: module.name, content_type: moduleType(module.content_type),
41
+ content_base64: base64(typeof module.content === 'string' ? new TextEncoder().encode(module.content) : module.content) }));
42
+ if (new Set(entryModules?.map(module => module.name)).size !== (entryModules?.length ?? 0))
43
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: 'Duplicate module name' });
44
+ const mainModule = input.main ? input.main.name : input.modules ? (input.main_module ?? input.modules[0].name) : undefined;
45
+ if (input.modules && !input.modules.some(module => module.name === mainModule)) {
46
+ throw new PraxisError({ code: 'INVALID_PARAMS', message: `main_module ${mainModule} is not among modules` });
47
+ }
29
48
  const files = Object.entries(input.files);
30
49
  if (files.length > 100_000)
31
50
  throw new PraxisError({ code: 'LIMIT_EXCEEDED', message: 'At most 100000 asset paths can be published' });
@@ -80,7 +99,7 @@ export async function publish(client, input) {
80
99
  const removed = Object.keys(previous).filter(key => !Object.hasOwn(manifest, key));
81
100
  const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(manifest))));
82
101
  const manifestHash = Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
83
- const staticOnly = !['main', 'bindings', 'compatibility_date', 'compatibility_flags', 'assets_config'].some(key => Object.hasOwn(input, key));
102
+ const staticOnly = !['main', 'modules', 'main_module', 'bindings', 'compatibility_date', 'compatibility_flags', 'assets_config'].some(key => Object.hasOwn(input, key));
84
103
  const unchanged = staticOnly && !!currentVersion && manifestHash === currentVersion.praxis.assets_manifest?.manifest_hash;
85
104
  const hashes = new Set(Object.values(manifest).map(entry => entry.hash));
86
105
  const currentResult = { url: worker.subdomain.url, worker_id: worker.id, version_id: currentVersion?.id ?? null,
@@ -127,9 +146,7 @@ export async function publish(client, input) {
127
146
  assets: versionAssets,
128
147
  ...(input.compatibility_flags ? { compatibility_flags: input.compatibility_flags } : {}),
129
148
  ...(input.bindings ? { bindings: input.bindings } : {}),
130
- ...(input.main ? { main_module: input.main.name, modules: [{ name: input.main.name,
131
- content_type: contentType,
132
- content_base64: base64(new TextEncoder().encode(input.main.content)) }] } : {}),
149
+ ...(entryModules ? { main_module: mainModule, modules: entryModules } : {}),
133
150
  } });
134
151
  let deployment = result.deployment;
135
152
  if (!deployment)
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ticket-944 — the `actions` driver: the cloud browser driven over the public HTTP faces, with no Playwright and
3
+ * no `node:child_process`. A light machine cannot install `playwright-core` at all, so instead of asking to be
4
+ * upgraded it drives the same cloud session through:
5
+ *
6
+ * GET /browser/v1/sessions?name=<name>&status=RUNNING the live session this agent session already has
7
+ * POST /browser/v1/sessions open one under that name when there is none
8
+ * POST /browser/v1/sessions/<id>/actions run one page action (#942)
9
+ *
10
+ * The service executes each action on the session's own CDP control connection, so `open` / `click` / `fill` / …
11
+ * keep working on a machine with no browser stack of its own
12
+ * (`services/cloud-browser/src/domain/page-actions.ts`).
13
+ */
14
+ /** Where screenshots land, matching the Playwright driver's directory. */
15
+ export declare const SCREENSHOT_DIR = ".praxis/screenshots";
16
+ /** The two `fs/promises` members this driver uses; the light isolate provides `fs/promises`, never sync `fs`. */
17
+ export interface FsPromisesLike {
18
+ mkdir(path: string, options: {
19
+ recursive: true;
20
+ }): Promise<unknown>;
21
+ writeFile(path: string, data: Uint8Array): Promise<unknown>;
22
+ }
23
+ /** What the actions driver needs from the platform and the process. All of it is injectable for tests. */
24
+ export interface BrowserActionsDeps {
25
+ api: string;
26
+ token: string;
27
+ /** The cloud session name — the agent session id, exactly as the Playwright path's connect URL uses it. */
28
+ name: string;
29
+ labels: Record<string, string>;
30
+ /** Browser context id (bsc_…) carrying saved sign-ins; omitted without one. */
31
+ context?: string;
32
+ home: string;
33
+ fetch: typeof globalThis.fetch;
34
+ fs: FsPromisesLike;
35
+ now: () => number;
36
+ }
37
+ /** An existing tab as `tabs.list` reports it. */
38
+ export interface ActionTabInfo {
39
+ id: string;
40
+ url: string;
41
+ title: string;
42
+ active: boolean;
43
+ }
44
+ /** A page action's answer, structurally: every result kind carries at least `type`. */
45
+ type ActionAnswer = Record<string, unknown> & {
46
+ type?: unknown;
47
+ };
48
+ /** `~/.praxis/screenshots/<timestamp>-<tab>.png`, the Playwright driver's own naming. */
49
+ export declare const screenshotPath: (home: string, tab: string, now: number) => string;
50
+ /**
51
+ * base64 → bytes, without Buffer or atob: the light machine's isolate is promised neither, and the screenshot is
52
+ * the one binary this file writes. Characters outside the alphabet are ignored, as base64 decoders do.
53
+ */
54
+ export declare function decodeBase64(text: string): Uint8Array;
55
+ /** One tab handle: every method is one page action on the resolved session. */
56
+ export interface ActionsTab {
57
+ readonly id: string;
58
+ snapshot(): Promise<string>;
59
+ click(ref: string): Promise<void>;
60
+ fill(ref: string, text: string): Promise<void>;
61
+ press(key: string): Promise<void>;
62
+ scroll(direction: 'up' | 'down', pixels?: number): Promise<void>;
63
+ evaluate(expression: string): Promise<unknown>;
64
+ screenshot(options?: {
65
+ fullPage?: boolean;
66
+ }): Promise<{
67
+ path: string;
68
+ hint: string;
69
+ }>;
70
+ }
71
+ /** The page-action client: session resolution, one request per action, one tab handle per tab id. */
72
+ export interface BrowserActions {
73
+ sessionId(): Promise<string>;
74
+ list(): Promise<ActionTabInfo[]>;
75
+ tab(id: string): ActionsTab;
76
+ /** One already-built action, for the shapes no tab method covers. */
77
+ action(body: Record<string, unknown>): Promise<ActionAnswer>;
78
+ }
79
+ /**
80
+ * Builds the actions client. It never opens a connection, imports Playwright, or reads a Node builtin: a session
81
+ * id is resolved once per call from the name, and every action is a request that returns its own answer.
82
+ */
83
+ export declare function createBrowserActions(deps: BrowserActionsDeps): BrowserActions;
84
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ticket-944 — the `actions` driver: the cloud browser driven over the public HTTP faces, with no Playwright and
3
+ * no `node:child_process`. A light machine cannot install `playwright-core` at all, so instead of asking to be
4
+ * upgraded it drives the same cloud session through:
5
+ *
6
+ * GET /browser/v1/sessions?name=<name>&status=RUNNING the live session this agent session already has
7
+ * POST /browser/v1/sessions open one under that name when there is none
8
+ * POST /browser/v1/sessions/<id>/actions run one page action (#942)
9
+ *
10
+ * The service executes each action on the session's own CDP control connection, so `open` / `click` / `fill` / …
11
+ * keep working on a machine with no browser stack of its own
12
+ * (`services/cloud-browser/src/domain/page-actions.ts`).
13
+ */
14
+ import { PraxisError } from "../core/errors.js";
15
+ /** The public own-face path prefix — the same one this SDK's connect URL is built on. */
16
+ const BROWSER_V1 = '/browser/v1';
17
+ /** Where screenshots land, matching the Playwright driver's directory. */
18
+ export const SCREENSHOT_DIR = '.praxis/screenshots';
19
+ /**
20
+ * A refusal becomes an error here and nowhere else: the server's own code, status and message travel unchanged,
21
+ * so ACTION_REF_STALE stays distinguishable from an outage (ticket-834's rule, for the actions door).
22
+ */
23
+ function failure(status, body) {
24
+ const record = body;
25
+ const code = typeof record?.code === 'string' ? record.code : undefined;
26
+ const message = typeof record?.message === 'string' ? record.message
27
+ : typeof record?.error === 'string' ? record.error
28
+ : `The browser action failed with HTTP ${status}.`;
29
+ return new PraxisError({ status, ...(code === undefined ? {} : { code }), message, raw: body });
30
+ }
31
+ /** `~/.praxis/screenshots/<timestamp>-<tab>.png`, the Playwright driver's own naming. */
32
+ export const screenshotPath = (home, tab, now) => `${home}/${SCREENSHOT_DIR}/${now}-${tab.slice(0, 8)}.png`;
33
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
34
+ /**
35
+ * base64 → bytes, without Buffer or atob: the light machine's isolate is promised neither, and the screenshot is
36
+ * the one binary this file writes. Characters outside the alphabet are ignored, as base64 decoders do.
37
+ */
38
+ export function decodeBase64(text) {
39
+ const clean = text.replace(/[^A-Za-z0-9+/]/g, '');
40
+ const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
41
+ let byte = 0;
42
+ let bits = 0;
43
+ let written = 0;
44
+ for (const character of clean) {
45
+ const value = ALPHABET.indexOf(character);
46
+ if (value < 0)
47
+ continue;
48
+ byte = (byte << 6) | value;
49
+ bits += 6;
50
+ if (bits >= 8) {
51
+ bits -= 8;
52
+ out[written++] = (byte >> bits) & 0xff;
53
+ }
54
+ }
55
+ return out.subarray(0, written);
56
+ }
57
+ /**
58
+ * Builds the actions client. It never opens a connection, imports Playwright, or reads a Node builtin: a session
59
+ * id is resolved once per call from the name, and every action is a request that returns its own answer.
60
+ */
61
+ export function createBrowserActions(deps) {
62
+ const request = async (path, init = {}) => {
63
+ const headers = {
64
+ Authorization: `Bearer ${deps.token}`,
65
+ ...(init.body === undefined ? {} : { 'content-type': 'application/json' }),
66
+ ...(init.headers ?? {}),
67
+ };
68
+ let response;
69
+ try {
70
+ response = await deps.fetch(`${deps.api}${BROWSER_V1}${path}`, { ...init, headers });
71
+ }
72
+ catch (cause) {
73
+ throw new PraxisError({ status: 503, code: 'BROWSER_UNAVAILABLE', message: `Reaching the cloud browser failed: ${cause.message.split('\n')[0]}` });
74
+ }
75
+ const text = await response.text();
76
+ let body;
77
+ try {
78
+ body = text ? JSON.parse(text) : undefined;
79
+ }
80
+ catch {
81
+ body = text;
82
+ }
83
+ if (!response.ok)
84
+ throw failure(response.status, body);
85
+ return body;
86
+ };
87
+ /** Resolved once per client: one connect() is one program, and the session it named does not change inside it. */
88
+ let resolved;
89
+ /** The live session named for this agent session, else a new one under that name. */
90
+ const sessionId = async () => {
91
+ resolved ??= (async () => {
92
+ const found = (await request(`/sessions?name=${encodeURIComponent(deps.name)}&status=RUNNING`));
93
+ // A page holds only this account's sessions, but a name is what identifies ours: anything else is not it.
94
+ const live = (found?.data ?? []).find(row => typeof row.id === 'string' && row.name === deps.name);
95
+ if (live?.id)
96
+ return live.id;
97
+ const created = (await request('/sessions', {
98
+ method: 'POST',
99
+ body: JSON.stringify({
100
+ name: deps.name,
101
+ labels: deps.labels,
102
+ ...(deps.context === undefined ? {} : { browserSettings: { context: { id: deps.context, persist: true } } }),
103
+ }),
104
+ }));
105
+ if (typeof created?.id !== 'string')
106
+ throw new PraxisError({ status: 503, code: 'BROWSER_UNAVAILABLE', message: 'The cloud browser session could not be opened.' });
107
+ return created.id;
108
+ })();
109
+ // A failed resolution is not cached: the next action may find the service again.
110
+ return await resolved.catch(cause => { resolved = undefined; throw cause; });
111
+ };
112
+ const action = async (body) => {
113
+ const id = await sessionId();
114
+ return (await request(`/sessions/${encodeURIComponent(id)}/actions`, { method: 'POST', body: JSON.stringify(body) }));
115
+ };
116
+ const list = async () => {
117
+ const answer = await action({ type: 'tabs.list' });
118
+ return (answer.tabs ?? []).map(tab => ({ id: tab.id, url: tab.url, title: tab.title, active: tab.active }));
119
+ };
120
+ const tab = (id) => {
121
+ const on = (type, extra = {}) => action({ type, tab: id, ...extra });
122
+ return {
123
+ id,
124
+ // The server's own rendering; same reading model as the Playwright driver's ariaSnapshot({ mode: "ai" }).
125
+ snapshot: async () => String((await on('snapshot')).snapshot ?? ''),
126
+ click: async (ref) => { await on('click', { ref }); },
127
+ fill: async (ref, text) => { await on('fill', { ref, text }); },
128
+ press: async (key) => { await on('press', { key }); },
129
+ scroll: async (direction, pixels) => { await on('scroll', { direction, ...(pixels === undefined ? {} : { pixels }) }); },
130
+ evaluate: async (expression) => (await on('evaluate', { expression })).value,
131
+ screenshot: async (options) => {
132
+ const answer = await on('screenshot', options?.fullPage === true ? { fullPage: true } : {});
133
+ const path = screenshotPath(deps.home, id, deps.now());
134
+ await deps.fs.mkdir(`${deps.home}/${SCREENSHOT_DIR}`, { recursive: true });
135
+ await deps.fs.writeFile(path, decodeBase64(String(answer.data ?? '')));
136
+ return { path, hint: 'View this PNG with the Read tool.' };
137
+ },
138
+ };
139
+ };
140
+ return { sessionId, list, tab, action };
141
+ }
@@ -0,0 +1,102 @@
1
+ import { PraxisError } from '../core/errors.js';
2
+ import { type BrowserTabInfo, type ContextLike } from './browser-decisions.js';
3
+ /** Version of the Playwright client library: preinstalled in the base image, installed on demand elsewhere. */
4
+ export declare const PLAYWRIGHT_CORE_VERSION = "1.63.0";
5
+ /** Playwright's Page, kept structural: the SDK does not depend on playwright-core. */
6
+ export type PlaywrightPage = any;
7
+ export interface BrowserTab {
8
+ readonly id: string;
9
+ /** The Playwright Page for locators and waits. Absent in the `actions` driver, where reading it throws. */
10
+ readonly playwright: PlaywrightPage;
11
+ goto(url: string): Promise<void>;
12
+ back(): Promise<void>;
13
+ forward(): Promise<void>;
14
+ reload(): Promise<void>;
15
+ close(): Promise<void>;
16
+ url(): Promise<string>;
17
+ title(): Promise<string>;
18
+ /** The page as an accessibility snapshot with [ref=eN] marks: Playwright's, or the server's own text. */
19
+ snapshot(): Promise<string>;
20
+ /** Clicks the element a snapshot marked `[ref=eN]`. */
21
+ click(ref: string): Promise<void>;
22
+ /** Fills the element a snapshot marked `[ref=eN]`. */
23
+ fill(ref: string, text: string): Promise<void>;
24
+ /** Presses one key (Enter, Tab, a single character). */
25
+ press(key: string): Promise<void>;
26
+ /** Scrolls the page by `pixels` (default 800) in one direction. */
27
+ scroll(direction: 'up' | 'down', pixels?: number): Promise<void>;
28
+ /** Evaluates an expression in the page and returns its value. */
29
+ evaluate(expression: string): Promise<unknown>;
30
+ /** Writes a PNG under ~/.praxis/screenshots and returns its path; view it with the Read tool. */
31
+ screenshot(options?: {
32
+ fullPage?: boolean;
33
+ }): Promise<{
34
+ path: string;
35
+ hint: string;
36
+ }>;
37
+ }
38
+ export interface PraxisBrowser {
39
+ readonly tabs: {
40
+ list(): Promise<BrowserTabInfo[]>;
41
+ get(id: string): Promise<BrowserTab>;
42
+ 'new'(): Promise<BrowserTab>;
43
+ };
44
+ open(url: string, options?: {
45
+ reuseTab?: boolean;
46
+ }): Promise<BrowserTab>;
47
+ documentation(): string;
48
+ /** Drops this connection only; the cloud session and its tabs stay until it idles out. */
49
+ disconnect(): Promise<void>;
50
+ }
51
+ export interface BrowserConnectOptions {
52
+ /** false skips the agent's saved sign-in context. */
53
+ context?: false;
54
+ }
55
+ interface ContextsApi {
56
+ list(args?: {
57
+ query?: {
58
+ limit?: number;
59
+ cursor?: string;
60
+ name?: string;
61
+ };
62
+ }): Promise<{
63
+ data: (ContextLike & {
64
+ name?: unknown;
65
+ })[];
66
+ nextCursor: string | null;
67
+ }>;
68
+ create(args: {
69
+ body: {
70
+ name: string;
71
+ labels: Record<string, string>;
72
+ };
73
+ }): Promise<ContextLike>;
74
+ }
75
+ /** The playwright-core module shape the helper uses. */
76
+ export interface PlaywrightModule {
77
+ chromium: {
78
+ connectOverCDP(url: string, options: {
79
+ headers: Record<string, string>;
80
+ timeout?: number;
81
+ }): Promise<unknown>;
82
+ };
83
+ }
84
+ /** What connect() needs from the platform client and the process. All of it is injectable for tests. */
85
+ export interface BrowserConnectDeps {
86
+ contexts: ContextsApi;
87
+ env: Readonly<Record<string, string | undefined>>;
88
+ /** process.getBuiltinModule; undefined on runtimes without it. */
89
+ builtin: ((name: string) => unknown) | undefined;
90
+ now?: () => number;
91
+ /** An already-loaded playwright-core; the real loader finds or installs one and imports it. */
92
+ playwright?: PlaywrightModule;
93
+ /** The fetch the actions driver sends its requests with; globalThis.fetch by default. */
94
+ fetch?: typeof globalThis.fetch;
95
+ }
96
+ export declare const unavailable: (message: string) => PraxisError;
97
+ /** Same signal the plugin installer and child_process use: the light machine asks to be upgraded and the program runs again. */
98
+ export declare const needsFullMachine: () => Error & {
99
+ code: string;
100
+ };
101
+ export declare function connectBrowser(deps: BrowserConnectDeps, options?: BrowserConnectOptions): Promise<PraxisBrowser>;
102
+ export {};