runline 0.18.0 → 0.20.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,153 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { extname } from "node:path";
3
+ import * as t from "typebox";
4
+ import { request, STRICT_OBJECT } from "../../_shared/shiftCloud.js";
5
+ /**
6
+ * Shift OCR — text and structured-field extraction from images and
7
+ * PDFs through the Shift cloud OCR service. The API key is the tenant
8
+ * authority; no organization ID is ever sent (SHFT-852).
9
+ */
10
+ const IMAGE_TYPES = {
11
+ avif: "image/avif",
12
+ gif: "image/gif",
13
+ jpeg: "image/jpeg",
14
+ jpg: "image/jpeg",
15
+ png: "image/png",
16
+ webp: "image/webp",
17
+ };
18
+ const DOCUMENT_TYPES = {
19
+ pdf: "application/pdf",
20
+ };
21
+ /**
22
+ * Raw-file ceiling for inlining as a base64 data URL. Base64 inflates
23
+ * by 4/3, so 20 MiB raw stays well inside provider request limits.
24
+ */
25
+ const MAX_FILE_BYTES = 20 * 1024 * 1024;
26
+ async function documentFromPath(path) {
27
+ const extension = extname(path).slice(1).toLowerCase();
28
+ const imageType = IMAGE_TYPES[extension];
29
+ const documentType = DOCUMENT_TYPES[extension];
30
+ if (!imageType && !documentType) {
31
+ throw new Error(`Unsupported file extension for ${path}. Supported: ${[
32
+ ...Object.keys(IMAGE_TYPES),
33
+ ...Object.keys(DOCUMENT_TYPES),
34
+ ].join(", ")}`);
35
+ }
36
+ const file = await stat(path);
37
+ if (!file.isFile())
38
+ throw new Error(`${path} is not a file`);
39
+ if (file.size === 0)
40
+ throw new Error("File is empty");
41
+ if (file.size > MAX_FILE_BYTES) {
42
+ throw new Error(`File is ${file.size} bytes; inline OCR uploads are capped at ${MAX_FILE_BYTES} bytes. Host the file and pass url instead.`);
43
+ }
44
+ const mediaType = imageType ?? documentType;
45
+ const url = `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
46
+ return imageType
47
+ ? { type: "image", url }
48
+ : { type: "document", url, name: path.replace(/.*\//, "") };
49
+ }
50
+ function documentFromUrl(url, kind) {
51
+ if (kind)
52
+ return { type: kind, url };
53
+ // Data URLs carry their media type, not an extension.
54
+ if (url.startsWith("data:")) {
55
+ return url.startsWith("data:application/pdf")
56
+ ? { type: "document", url }
57
+ : { type: "image", url };
58
+ }
59
+ const extension = new URL(url).pathname.replace(/.*\./, "").toLowerCase();
60
+ return DOCUMENT_TYPES[extension]
61
+ ? { type: "document", url }
62
+ : { type: "image", url };
63
+ }
64
+ export default function shiftOcr(rl) {
65
+ rl.setName("shiftOcr");
66
+ rl.setVersion("0.1.0");
67
+ rl.setConnectionSchema(t.Object({
68
+ // Same Shift Labs API key as the shiftLabs plugin; the cloud
69
+ // derives the organization from it, so no org ID is needed.
70
+ apiKey: t.String({
71
+ description: "Shift Labs API key",
72
+ env: "SHIFT_LABS_API_KEY",
73
+ }),
74
+ }));
75
+ rl.registerAction("ocr.extract", {
76
+ access: "write",
77
+ description: "Extract markdown text — and optionally schema-constrained JSON — " +
78
+ "from an image or PDF. Pass a local file path (inlined as a data " +
79
+ "URL) or an https URL. Provide schema to get structured fields " +
80
+ "back instead of parsing the text yourself.",
81
+ inputSchema: t.Object({
82
+ path: t.Optional(t.String({
83
+ minLength: 1,
84
+ pattern: "\\S",
85
+ description: "Host path of an image or PDF (max 20 MiB)",
86
+ })),
87
+ url: t.Optional(t.String({
88
+ minLength: 1,
89
+ pattern: "\\S",
90
+ description: "https or data URL of the image or PDF",
91
+ })),
92
+ kind: t.Optional(t.Union([t.Literal("image"), t.Literal("document")], {
93
+ description: "Override for URLs whose extension does not reveal the type. " +
94
+ "Inferred from the extension otherwise.",
95
+ })),
96
+ pages: t.Optional(t.String({
97
+ minLength: 1,
98
+ pattern: "^\\d+(-\\d+)?(,\\d+(-\\d+)?)*$",
99
+ description: "Zero-based pages for PDFs, e.g. '0', '0-5', or '0,2-4'",
100
+ })),
101
+ schema: t.Optional(t.Record(t.String(), t.Unknown(), {
102
+ description: "JSON schema for structured extraction across the document",
103
+ })),
104
+ schemaName: t.Optional(t.String({
105
+ minLength: 1,
106
+ pattern: "\\S",
107
+ description: "Extractor name for provenance. Default: extraction",
108
+ })),
109
+ prompt: t.Optional(t.String({
110
+ minLength: 1,
111
+ pattern: "\\S",
112
+ description: "Guidance for structured extraction; requires schema",
113
+ })),
114
+ }, STRICT_OBJECT),
115
+ async execute(input, ctx) {
116
+ const fields = input;
117
+ if (!fields.path === !fields.url) {
118
+ throw new Error("Provide exactly one of path or url");
119
+ }
120
+ if (fields.prompt && !fields.schema) {
121
+ throw new Error("prompt requires schema");
122
+ }
123
+ const document = fields.path
124
+ ? await documentFromPath(fields.path)
125
+ : documentFromUrl(fields.url, fields.kind);
126
+ return await request(ctx, "/v1/services/ocr/extract", {
127
+ method: "POST",
128
+ body: JSON.stringify({
129
+ document,
130
+ ...(fields.pages ? { pages: fields.pages } : {}),
131
+ ...(fields.schema
132
+ ? {
133
+ structured: {
134
+ name: fields.schemaName ?? "extraction",
135
+ schema: fields.schema,
136
+ ...(fields.prompt ? { prompt: fields.prompt } : {}),
137
+ },
138
+ }
139
+ : {}),
140
+ }),
141
+ });
142
+ },
143
+ });
144
+ rl.registerAction("ocr.providers", {
145
+ access: "read",
146
+ description: "List the OCR providers and models available.",
147
+ inputSchema: t.Object({}, STRICT_OBJECT),
148
+ async execute(_input, ctx) {
149
+ const body = await request(ctx, "/v1/services/ocr/providers");
150
+ return body.providers;
151
+ },
152
+ });
153
+ }
package/dist/sdk.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ExecuteResult } from "./core/engine.js";
1
+ import { type ActionInvocation, type ExecuteResult } from "./core/engine.js";
2
2
  import type { PluginFunction } from "./plugin/api.js";
3
3
  import type { ActionAccess, ConnectionConfig, InputSchema, PluginDef } from "./plugin/types.js";
4
4
  export interface RunlineOptions {
@@ -11,6 +11,15 @@ export interface RunlineOptions {
11
11
  * `ExecutionEngine` — this bounds cross-run contamination, not memory.
12
12
  */
13
13
  maxRunsPerWorker?: number;
14
+ /**
15
+ * Observer fired in the host process for every action invocation
16
+ * that enters a plugin's `execute()` — unknown paths and failed
17
+ * input validation never reach plugin code and are not reported.
18
+ * Exceptions thrown by the observer are swallowed: observability
19
+ * never breaks a run. Not part of `RunlineConfig` on purpose —
20
+ * config round-trips through JSON (`fromProject`), functions do not.
21
+ */
22
+ onAction?: (info: ActionInvocation) => void;
14
23
  }
15
24
  export interface RunlineExecuteOptions {
16
25
  timeoutMs?: number;
@@ -25,6 +34,7 @@ export declare class Runline {
25
34
  * leak) a fresh worker every time — the exact bug pooling exists to fix.
26
35
  */
27
36
  private _engine;
37
+ private readonly _onAction;
28
38
  private constructor();
29
39
  private engine;
30
40
  /**
package/dist/sdk.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { DEFAULT_CONFIG } from "./config/types.js";
4
- import { ExecutionEngine } from "./core/engine.js";
4
+ import { ExecutionEngine, } from "./core/engine.js";
5
5
  import { resolvePluginExport } from "./plugin/api.js";
6
6
  import { discoverPlugins } from "./plugin/loader.js";
7
7
  import { PluginRegistry } from "./plugin/registry.js";
@@ -14,8 +14,10 @@ export class Runline {
14
14
  * leak) a fresh worker every time — the exact bug pooling exists to fix.
15
15
  */
16
16
  _engine = null;
17
+ _onAction;
17
18
  constructor(options) {
18
19
  this._registry = new PluginRegistry();
20
+ this._onAction = options.onAction;
19
21
  for (const pluginOrFn of options.plugins ?? []) {
20
22
  const plugin = resolvePluginExport(pluginOrFn, "unknown");
21
23
  this._registry.register(plugin);
@@ -31,7 +33,9 @@ export class Runline {
31
33
  }
32
34
  engine() {
33
35
  if (!this._engine) {
34
- this._engine = new ExecutionEngine(this._registry, this._config);
36
+ this._engine = new ExecutionEngine(this._registry, this._config, {
37
+ onAction: this._onAction,
38
+ });
35
39
  }
36
40
  return this._engine;
37
41
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.18.0",
4
- "description": "Code mode for agents — turn any API or command into a callable action",
3
+ "version": "0.20.0",
4
+ "description": "Code mode for agents \u2014 turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
@@ -25,8 +25,7 @@
25
25
  "runline": "./dist/main.js"
26
26
  },
27
27
  "files": [
28
- "dist",
29
- "vendor"
28
+ "dist"
30
29
  ],
31
30
  "scripts": {
32
31
  "build": "tsc && bun --filter runline-plugins build && rm -rf dist/plugins && cp -R ../runline-plugins/dist dist/plugins",
@@ -59,7 +58,6 @@
59
58
  "@types/bun": "^1.2.17",
60
59
  "@types/node": "^25.5.0",
61
60
  "@types/proper-lockfile": "^4.1.4",
62
- "minisearch": "^7.2.0",
63
61
  "typescript": "^5.8.0"
64
62
  },
65
63
  "peerDependencies": {
@@ -91,6 +89,7 @@
91
89
  }
92
90
  },
93
91
  "dependencies": {
92
+ "@shift-labs/ferrosearch": "^0.1.0",
94
93
  "chalk": "^5.6.2",
95
94
  "commander": "^14.0.3",
96
95
  "jiti": "^2.7.0",
package/vendor/README.md DELETED
@@ -1,19 +0,0 @@
1
- # vendor
2
-
3
- Third-party bundles inlined into the QuickJS sandbox at runtime.
4
-
5
- These files are committed deliberately: the sandbox eval'd source is part of runline's behavior, and PR diffs should show exactly what changes when a bundle is bumped.
6
-
7
- ## minisearch.umd.js
8
-
9
- UMD build of [minisearch](https://github.com/lucaong/minisearch). Read by `src/core/engine.ts` and inlined into the sandbox so `MiniSearch` is available as a global to agent code (powers `actions.find()`).
10
-
11
- To upgrade:
12
-
13
- ```bash
14
- npm i -D minisearch@latest
15
- cp node_modules/minisearch/dist/umd/index.js vendor/minisearch.umd.js
16
- git commit -am "vendor: bump minisearch"
17
- ```
18
-
19
- The `minisearch` devDependency exists only as a convenience for that `cp` — nothing imports it at runtime.