@opencraw/driver 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -60,6 +60,19 @@ stdin at once. Answers may arrive out of order; match them by `id`.
60
60
  `-32602` (bad params, the problems listed), `-32000` (a failure to act on: its message says what to do).
61
61
  - stdout carries protocol messages only. The driver's own logs go to stderr.
62
62
 
63
+ ## The runtime: no Node needed
64
+
65
+ Each driver release carries, as assets of its GitHub release `driver@<version>`, an archive per platform
66
+ (`win32-x64`, `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`): a Node binary, the driver and its
67
+ dependencies, and `runtime.json`, whose `command` starts it (both paths relative to the unpacked folder).
68
+ `driver-runtime.json` lists them with their SHA-256 and size; its schema is `schemas/driver-runtime.schema.json`
69
+ and `schemas/driver-runtime.fixture.json` is an example. A client downloads its platform's archive once, refuses
70
+ one whose hash does not match, unpacks it into a per-user cache and starts the driver from there.
71
+
72
+ `node packages/driver/packaging/stage-runtime.mjs` builds this platform's archive into `dist/drop/driver-runtime`
73
+ (after `smith build -p driver`), and starts the staged driver once with `PATH` emptied, to prove it needs nothing
74
+ else. Browsers are not in it: they are installed through the driver, the Playwright way.
75
+
63
76
  ## Development
64
77
 
65
78
  `smith run lint typecheck test build -p driver`. After changing a contract in `src/protocol/`, run
package/dist/index.esm.js CHANGED
@@ -240,8 +240,8 @@ function clientAccessPlugins(names, runId, ask) {
240
240
  // src/site-access/resolve-site-access.use-case.ts
241
241
  import { resolveAccess } from "@opencraw/cli";
242
242
  async function resolveSiteAccess(params, runId, ask, env = process.env) {
243
- const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
244
- if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config: give "accessConfig", or set OPENCRAW_ACCESS where the driver runs`);
243
+ const file = params.accessConfig ?? (env.OPENCRAW_ACCESS === "" ? void 0 : env.OPENCRAW_ACCESS);
244
+ if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config, and there is none: no "accessConfig" was given, and OPENCRAW_ACCESS is not set where OpenCraw runs`);
245
245
  const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
246
246
  return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
247
247
  }
@@ -344,17 +344,17 @@ function streamingSink() {
344
344
  }
345
345
 
346
346
  // src/session/run-session.use-case.ts
347
- function runSessions(notify, ask) {
347
+ function runSessions(notify, ask, local = {}) {
348
348
  const running = /* @__PURE__ */ new Map();
349
349
  let next = 1;
350
350
  const crawlerFor = async (runId, params) => {
351
351
  const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
352
352
  return createCrawler({
353
353
  sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
354
- hooks: clientHooks(params.hooks ?? [], runId, ask),
355
- captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
354
+ hooks: { ...local.hooks, ...clientHooks(params.hooks ?? [], runId, ask) },
355
+ captchaSolvers: [...local.captchaSolvers ?? [], ...clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask)],
356
356
  access,
357
- accessPlugins,
357
+ accessPlugins: [...local.accessPlugins ?? [], ...accessPlugins],
358
358
  profilesDir: process.env.OPENCRAW_PROFILES,
359
359
  parallel: params.parallel,
360
360
  resume: params.resume,
@@ -380,15 +380,16 @@ function runSessions(notify, ask) {
380
380
  }
381
381
  })();
382
382
  };
383
+ const startLoaded = async (loaded, params) => {
384
+ const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
385
+ const runId = `run-${next++}`;
386
+ const crawler = await crawlerFor(runId, params);
387
+ track(runId, crawler, async () => await crawler.run(set));
388
+ return runId;
389
+ };
383
390
  return {
384
- start: async (params) => {
385
- const loaded = await loadRecipes(params);
386
- const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
387
- const runId = `run-${next++}`;
388
- const crawler = await crawlerFor(runId, params);
389
- track(runId, crawler, async () => await crawler.run(set));
390
- return runId;
391
- },
391
+ start: async (params) => await startLoaded(await loadRecipes(params), params),
392
+ startLoaded,
392
393
  startWork: async (params) => {
393
394
  const set = await loadRecipes(params);
394
395
  const runId = `run-${next++}`;
@@ -574,10 +575,35 @@ async function serveDriver(input, output, identity) {
574
575
  await channel.drained();
575
576
  await sessions.closeAll();
576
577
  }
578
+
579
+ // src/runtime-manifest/runtime-manifest.contract.ts
580
+ import { z as z3 } from "zod";
581
+ var RUNTIME_PLATFORMS = ["win32-x64", "darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
582
+ var archiveSchema = z3.strictObject({
583
+ /** The archive's file name, an asset of the same GitHub release as the manifest. */
584
+ file: z3.string().regex(/^opencraw-driver-[\w.-]+\.tar\.gz$/),
585
+ /** Its SHA-256, lowercase hex: a client refuses an archive that does not match. */
586
+ sha256: z3.string().regex(/^[0-9a-f]{64}$/),
587
+ size: z3.number().int().positive()
588
+ });
589
+ var runtimeManifestSchema = z3.strictObject({
590
+ driverVersion: z3.string().min(1),
591
+ protocolVersion: z3.string().regex(/^\d+\.\d+\.\d+$/),
592
+ /** The Node version every archive carries. */
593
+ node: z3.string().regex(/^v\d+\.\d+\.\d+$/),
594
+ archives: z3.partialRecord(z3.enum(RUNTIME_PLATFORMS), archiveSchema)
595
+ });
596
+
597
+ // src/runtime-manifest/runtime-manifest-json-schema.mapper.ts
598
+ import { z as z4 } from "zod";
599
+ function runtimeManifestJsonSchema() {
600
+ return { $id: "https://opencraw.dev/schemas/driver-runtime.schema.json", ...z4.toJSONSchema(runtimeManifestSchema, { target: "draft-2020-12" }) };
601
+ }
577
602
  export {
578
603
  MESSAGES,
579
604
  PROTOCOL_VERSION,
580
605
  RPC_ERRORS,
606
+ RUNTIME_PLATFORMS,
581
607
  RpcFailure,
582
608
  calloutParamsSchema,
583
609
  calloutResultSchema,
@@ -606,6 +632,8 @@ export {
606
632
  runParamsSchema,
607
633
  runResultSchema,
608
634
  runSessions,
635
+ runtimeManifestJsonSchema,
636
+ runtimeManifestSchema,
609
637
  serveDriver,
610
638
  streamingSink,
611
639
  validateParamsSchema,
package/dist/main.esm.js CHANGED
@@ -201,8 +201,8 @@ function clientAccessPlugins(names, runId, ask) {
201
201
  // src/site-access/resolve-site-access.use-case.ts
202
202
  import { resolveAccess } from "@opencraw/cli";
203
203
  async function resolveSiteAccess(params, runId, ask, env = process.env) {
204
- const file = params.accessConfig ?? env.OPENCRAW_ACCESS;
205
- if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config: give "accessConfig", or set OPENCRAW_ACCESS where the driver runs`);
204
+ const file = params.accessConfig ?? (env.OPENCRAW_ACCESS === "" ? void 0 : env.OPENCRAW_ACCESS);
205
+ if (file === void 0 && params.access !== void 0) throw new Error(`access profile "${params.access}" needs an access config, and there is none: no "accessConfig" was given, and OPENCRAW_ACCESS is not set where OpenCraw runs`);
206
206
  const access = await resolveAccess({ insecureTls: false, access: file, accessProfile: params.access });
207
207
  return { access, accessPlugins: clientAccessPlugins(params.accessPlugins ?? [], runId, ask) };
208
208
  }
@@ -305,17 +305,17 @@ function streamingSink() {
305
305
  }
306
306
 
307
307
  // src/session/run-session.use-case.ts
308
- function runSessions(notify, ask) {
308
+ function runSessions(notify, ask, local = {}) {
309
309
  const running = /* @__PURE__ */ new Map();
310
310
  let next = 1;
311
311
  const crawlerFor = async (runId, params) => {
312
312
  const { access, accessPlugins } = await resolveSiteAccess(params, runId, ask);
313
313
  return createCrawler({
314
314
  sink: params.out === void 0 ? streamingSink() : fileSink2(params.out, { append: params.append, browser: { executablePath: params.browser?.executablePath } }),
315
- hooks: clientHooks(params.hooks ?? [], runId, ask),
316
- captchaSolvers: clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask),
315
+ hooks: { ...local.hooks, ...clientHooks(params.hooks ?? [], runId, ask) },
316
+ captchaSolvers: [...local.captchaSolvers ?? [], ...clientCaptchaSolvers(params.captchaSolvers ?? [], runId, ask)],
317
317
  access,
318
- accessPlugins,
318
+ accessPlugins: [...local.accessPlugins ?? [], ...accessPlugins],
319
319
  profilesDir: process.env.OPENCRAW_PROFILES,
320
320
  parallel: params.parallel,
321
321
  resume: params.resume,
@@ -341,15 +341,16 @@ function runSessions(notify, ask) {
341
341
  }
342
342
  })();
343
343
  };
344
+ const startLoaded = async (loaded, params) => {
345
+ const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
346
+ const runId = `run-${next++}`;
347
+ const crawler = await crawlerFor(runId, params);
348
+ track(runId, crawler, async () => await crawler.run(set));
349
+ return runId;
350
+ };
344
351
  return {
345
- start: async (params) => {
346
- const loaded = await loadRecipes(params);
347
- const set = params.dryRun === true ? new RecipeSet(loaded.output, loaded.inputs.map((input) => ({ ...input, limits: { ...input.limits, maxRecords: 1 } }))) : loaded;
348
- const runId = `run-${next++}`;
349
- const crawler = await crawlerFor(runId, params);
350
- track(runId, crawler, async () => await crawler.run(set));
351
- return runId;
352
- },
352
+ start: async (params) => await startLoaded(await loadRecipes(params), params),
353
+ startLoaded,
353
354
  startWork: async (params) => {
354
355
  const set = await loadRecipes(params);
355
356
  const runId = `run-${next++}`;
@@ -2,10 +2,12 @@ export { serveDriver } from './server/index.js';
2
2
  export type { DriverIdentity } from './server/index.js';
3
3
  export { PROTOCOL_VERSION, MESSAGES, driverProtocolJsonSchema, initializeParamsSchema, initializeResultSchema, validateParamsSchema, validateResultSchema, runParamsSchema, runResultSchema, cancelParamsSchema, cancelResultSchema, eventParamsSchema, recordParamsSchema, finishedParamsSchema, calloutParamsSchema, calloutResultSchema, probeParamsSchema, probeResultSchema, diffParamsSchema, diffResultSchema, exportParamsSchema, exportResultSchema, workParamsSchema, workItemSchema, workNextParamsSchema, workNextResultSchema, workDoneParamsSchema, workFailedParamsSchema, } from './protocol/index.js';
4
4
  export type { InitializeParams, InitializeResult, ValidateParams, ValidateResult, RunParams, RunResult, CancelParams, CancelResult, EventParams, RecordParams, FinishedParams, CalloutParams, ProbeParams, DiffParams, DiffResult, ExportParams, ExportResult, WorkParams, WorkItemMessage, WorkNextResult, WorkDoneParams, WorkFailedParams, } from './protocol/index.js';
5
+ export { RUNTIME_PLATFORMS, runtimeManifestSchema, runtimeManifestJsonSchema } from './runtime-manifest/index.js';
6
+ export type { RuntimeManifest, RuntimePlatform } from './runtime-manifest/index.js';
5
7
  export { openLineChannel, RPC_ERRORS, RpcFailure } from './transport/index.js';
6
8
  export type { LineChannel, RequestHandler, RpcError, RpcMessage } from './transport/index.js';
7
9
  export { runSessions, validateRecipes, streamingSink } from './session/index.js';
8
- export type { RunSessions } from './session/index.js';
10
+ export type { LocalPlugins, RunSessions } from './session/index.js';
9
11
  export { clientHooks, clientCaptchaSolvers, clientAccessPlugins } from './callout-bridge/index.js';
10
12
  export type { AskClient, NotifyClient } from './callout-bridge/index.js';
11
13
  export { clientWorkSource } from './work-bridge/index.js';
@@ -0,0 +1,4 @@
1
+ export { RUNTIME_PLATFORMS, runtimeManifestSchema } from './runtime-manifest.contract.js';
2
+ export type { RuntimeManifest, RuntimePlatform } from './runtime-manifest.contract.js';
3
+ export { runtimeManifestJsonSchema } from './runtime-manifest-json-schema.mapper.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `driver-runtime.json`'s JSON Schema, written to `schemas/driver-runtime.schema.json`: what clients in
3
+ * other languages generate their manifest model from.
4
+ *
5
+ * @returns The schema.
6
+ */
7
+ export declare function runtimeManifestJsonSchema(): Record<string, unknown>;
8
+ //# sourceMappingURL=runtime-manifest-json-schema.mapper.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ /** The platforms the driver's runtime is built for: `<process.platform>-<process.arch>`. */
3
+ export declare const RUNTIME_PLATFORMS: readonly ['win32-x64', 'darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64'];
4
+ export type RuntimePlatform = typeof RUNTIME_PLATFORMS[number];
5
+ /**
6
+ * `driver-runtime.json`, an asset of each driver release beside the archives: what a client in any
7
+ * language reads to download the runtime for its platform, check it and start it. The archive holds a
8
+ * Node binary, the driver and its dependencies, and `runtime.json` (`command`: the Node binary and the
9
+ * driver's entry, both relative to the folder the archive is unpacked into).
10
+ */
11
+ export declare const runtimeManifestSchema: z.ZodObject<{
12
+ driverVersion: z.ZodString;
13
+ protocolVersion: z.ZodString;
14
+ node: z.ZodString;
15
+ archives: z.ZodRecord<z.ZodEnum<{
16
+ "darwin-arm64": "darwin-arm64";
17
+ "darwin-x64": "darwin-x64";
18
+ "linux-arm64": "linux-arm64";
19
+ "linux-x64": "linux-x64";
20
+ "win32-x64": "win32-x64";
21
+ }> & z.core.$partial, z.ZodObject<{
22
+ file: z.ZodString;
23
+ sha256: z.ZodString;
24
+ size: z.ZodNumber;
25
+ }, z.core.$strict>>;
26
+ }, z.core.$strict>;
27
+ export type RuntimeManifest = z.infer<typeof runtimeManifestSchema>;
28
+ //# sourceMappingURL=runtime-manifest.contract.d.ts.map
@@ -1,5 +1,5 @@
1
1
  export { runSessions } from './run-session.use-case.js';
2
- export type { RunSessions } from './run-session.use-case.js';
2
+ export type { LocalPlugins, RunSessions } from './run-session.use-case.js';
3
3
  export { validateRecipes, hooksOf, loadRecipes, sourceOf } from './recipe-loading.use-case.js';
4
4
  export { streamingSink } from './streaming-sink.repository.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,12 +1,22 @@
1
+ import { RecipeSet } from '@opencraw/core';
2
+ import type { AccessPlugin, CaptchaSolver, HookMap } from '@opencraw/core';
1
3
  import type { AskClient, NotifyClient } from '../callout-bridge/index.js';
2
4
  import type { RunParams, WorkParams } from '../protocol/index.js';
3
5
  /** The runs a driver has going, by id, so `cancel` can reach them and `shutdown` close them all. */
4
6
  export interface RunSessions {
5
7
  start: (params: RunParams) => Promise<string>;
8
+ /** As `start`, for recipes the host has already loaded and bound (`params`' recipe source is ignored). */
9
+ startLoaded: (set: RecipeSet, params: Omit<RunParams, 'paths' | 'recipes'>) => Promise<string>;
6
10
  startWork: (params: WorkParams) => Promise<string>;
7
11
  cancel: (runId: string) => Promise<boolean>;
8
12
  closeAll: () => Promise<void>;
9
13
  }
14
+ /** What the host serves itself, in process, besides what the client serves: an MCP server's plugins module. */
15
+ export interface LocalPlugins {
16
+ hooks?: HookMap;
17
+ captchaSolvers?: CaptchaSolver[];
18
+ accessPlugins?: AccessPlugin[];
19
+ }
10
20
  /**
11
21
  * Starts and tracks runs and worker pools. `start` and `startWork` load and bind the recipes (a bad
12
22
  * recipe fails the request itself, with its problems), answer with a run id at once, and crawl in the
@@ -18,7 +28,8 @@ export interface RunSessions {
18
28
  *
19
29
  * @param notify - The channel's notify.
20
30
  * @param ask - The channel's request, for callouts and work.
31
+ * @param local - Hooks, captcha solvers and access plugins the host serves itself, beside the client's.
21
32
  * @returns The sessions.
22
33
  */
23
- export declare function runSessions(notify: NotifyClient, ask: AskClient): RunSessions;
34
+ export declare function runSessions(notify: NotifyClient, ask: AskClient, local?: LocalPlugins): RunSessions;
24
35
  //# sourceMappingURL=run-session.use-case.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/driver",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "OpenCraw's driver: the engine over JSON-RPC on stdio, for clients in other languages",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,9 @@
1
+ {
2
+ "driverVersion": "0.6.0",
3
+ "protocolVersion": "1.2.0",
4
+ "node": "v24.12.0",
5
+ "archives": {
6
+ "linux-x64": { "file": "opencraw-driver-0.6.0-linux-x64.tar.gz", "sha256": "697d28005ed87b2887cf79edca25f06da89ab19bcb48f49c0a67dbdbcb004ad7", "size": 65410362 },
7
+ "win32-x64": { "file": "opencraw-driver-0.6.0-win32-x64.tar.gz", "sha256": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0", "size": 66120448 }
8
+ }
9
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "$id": "https://opencraw.dev/schemas/driver-runtime.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "type": "object",
5
+ "properties": {
6
+ "driverVersion": {
7
+ "type": "string",
8
+ "minLength": 1
9
+ },
10
+ "protocolVersion": {
11
+ "type": "string",
12
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
13
+ },
14
+ "node": {
15
+ "type": "string",
16
+ "pattern": "^v\\d+\\.\\d+\\.\\d+$"
17
+ },
18
+ "archives": {
19
+ "type": "object",
20
+ "propertyNames": {
21
+ "type": "string",
22
+ "enum": [
23
+ "win32-x64",
24
+ "darwin-arm64",
25
+ "darwin-x64",
26
+ "linux-x64",
27
+ "linux-arm64"
28
+ ]
29
+ },
30
+ "additionalProperties": {
31
+ "type": "object",
32
+ "properties": {
33
+ "file": {
34
+ "type": "string",
35
+ "pattern": "^opencraw-driver-[\\w.-]+\\.tar\\.gz$"
36
+ },
37
+ "sha256": {
38
+ "type": "string",
39
+ "pattern": "^[0-9a-f]{64}$"
40
+ },
41
+ "size": {
42
+ "type": "integer",
43
+ "exclusiveMinimum": 0,
44
+ "maximum": 9007199254740991
45
+ }
46
+ },
47
+ "required": [
48
+ "file",
49
+ "sha256",
50
+ "size"
51
+ ],
52
+ "additionalProperties": false
53
+ }
54
+ }
55
+ },
56
+ "required": [
57
+ "driverVersion",
58
+ "protocolVersion",
59
+ "node",
60
+ "archives"
61
+ ],
62
+ "additionalProperties": false
63
+ }