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.
- package/dist/core/engine.d.ts +27 -1
- package/dist/core/engine.js +50 -25
- package/dist/index.d.ts +1 -1
- package/dist/plugins/shiftLabs/src/index.js +4 -4
- package/dist/plugins/shiftLabs/src/objects.js +249 -0
- package/dist/plugins/shiftLabs/src/transcription.js +2 -11
- package/dist/plugins/shiftOcr/src/index.js +153 -0
- package/dist/sdk.d.ts +11 -1
- package/dist/sdk.js +6 -2
- package/package.json +4 -5
- package/vendor/README.md +0 -19
- package/vendor/minisearch.umd.js +0 -2013
|
@@ -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.
|
|
4
|
-
"description": "Code mode for agents
|
|
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.
|