runline 0.7.1 → 0.7.4
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/commands/exec.js +3 -1
- package/dist/main.js +3 -2
- package/dist/plugin/loader.d.ts +1 -1
- package/dist/plugin/loader.js +2 -2
- package/dist/plugins/gmail/src/index.js +69 -13
- package/dist/plugins/node/src/index.js +544 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1 -1
- package/package.json +2 -2
package/dist/commands/exec.js
CHANGED
|
@@ -5,8 +5,10 @@ import { loadAllPlugins } from "../plugin/loader.js";
|
|
|
5
5
|
import { registry } from "../plugin/registry.js";
|
|
6
6
|
import { printError, printJson } from "../utils/output.js";
|
|
7
7
|
export async function exec(code, options) {
|
|
8
|
-
await loadAllPlugins();
|
|
9
8
|
const config = loadConfig();
|
|
9
|
+
await loadAllPlugins({
|
|
10
|
+
builtinAllowlist: new Set(config.connections.map((c) => c.plugin)),
|
|
11
|
+
});
|
|
10
12
|
const engine = new ExecutionEngine(registry, config);
|
|
11
13
|
if (options.file) {
|
|
12
14
|
if (!existsSync(code)) {
|
package/dist/main.js
CHANGED
|
@@ -34,11 +34,12 @@ https://github.com/Michaelliv/runline`);
|
|
|
34
34
|
program
|
|
35
35
|
.command("exec <code>")
|
|
36
36
|
.alias("e")
|
|
37
|
-
.description("Execute JavaScript code in the
|
|
37
|
+
.description("Execute JavaScript code in the QuickJS runtime")
|
|
38
38
|
.option("-f, --file", "Treat <code> as a file path")
|
|
39
39
|
.addHelpText("after", `
|
|
40
|
-
The code runs in a QuickJS
|
|
40
|
+
The code runs in a QuickJS runtime with an \`actions\` proxy.
|
|
41
41
|
Each installed plugin is a top-level global. Dot-chain into resource and action.
|
|
42
|
+
Configure the built-in \`node\` plugin to expose host-backed fs/path/os/process/crypto/fetch actions.
|
|
42
43
|
|
|
43
44
|
Examples:
|
|
44
45
|
$ runline exec 'return await docker.containers.list()'
|
package/dist/plugin/loader.d.ts
CHANGED
|
@@ -32,4 +32,4 @@ export declare function discoverPlugins(configDir?: string | null, options?: Dis
|
|
|
32
32
|
* Load all plugins and register them into the global registry.
|
|
33
33
|
* Used by the CLI.
|
|
34
34
|
*/
|
|
35
|
-
export declare function loadAllPlugins(): Promise<void>;
|
|
35
|
+
export declare function loadAllPlugins(options?: DiscoverOptions): Promise<void>;
|
package/dist/plugin/loader.js
CHANGED
|
@@ -162,9 +162,9 @@ export async function discoverPlugins(configDir, options = {}) {
|
|
|
162
162
|
* Load all plugins and register them into the global registry.
|
|
163
163
|
* Used by the CLI.
|
|
164
164
|
*/
|
|
165
|
-
export async function loadAllPlugins() {
|
|
165
|
+
export async function loadAllPlugins(options = {}) {
|
|
166
166
|
const configDir = findConfigDir();
|
|
167
|
-
const plugins = await discoverPlugins(configDir);
|
|
167
|
+
const plugins = await discoverPlugins(configDir, options);
|
|
168
168
|
for (const p of plugins) {
|
|
169
169
|
registry.register(p);
|
|
170
170
|
}
|
|
@@ -77,6 +77,7 @@ async function paginateAll(ctx, path, key, qs) {
|
|
|
77
77
|
}
|
|
78
78
|
// ─── MIME encoding ───────────────────────────────────────────────
|
|
79
79
|
const CRLF = "\r\n";
|
|
80
|
+
const GMAIL_MAX_RAW_BYTES = 35 * 1024 * 1024;
|
|
80
81
|
function base64url(bytes) {
|
|
81
82
|
const buf = typeof bytes === "string" ? Buffer.from(bytes, "utf-8") : Buffer.from(bytes);
|
|
82
83
|
return buf
|
|
@@ -104,21 +105,57 @@ function header(name, value) {
|
|
|
104
105
|
return "";
|
|
105
106
|
return `${name}: ${value}${CRLF}`;
|
|
106
107
|
}
|
|
108
|
+
function foldedBase64ByteLength(length) {
|
|
109
|
+
return length === 0 ? 0 : length + Math.floor((length - 1) / 76) * CRLF.length;
|
|
110
|
+
}
|
|
111
|
+
function foldBase64(encoded) {
|
|
112
|
+
let folded = "";
|
|
113
|
+
for (let i = 0; i < encoded.length; i += 76) {
|
|
114
|
+
if (i > 0)
|
|
115
|
+
folded += CRLF;
|
|
116
|
+
folded += encoded.slice(i, i + 76);
|
|
117
|
+
}
|
|
118
|
+
return folded;
|
|
119
|
+
}
|
|
120
|
+
function assertAttachmentPayloadFits(atts) {
|
|
121
|
+
const attachmentBodyBytes = atts.reduce((sum, att) => sum + foldedBase64ByteLength(att.contentBase64.length), 0);
|
|
122
|
+
if (attachmentBodyBytes > GMAIL_MAX_RAW_BYTES) {
|
|
123
|
+
throw new Error(`gmail: attachment payload is ${attachmentBodyBytes} bytes after MIME folding; Gmail API raw messages must be <= ${GMAIL_MAX_RAW_BYTES} bytes`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
107
126
|
function textPart(body, mimeType) {
|
|
108
127
|
const encoded = Buffer.from(body, "utf-8").toString("base64");
|
|
109
|
-
// Fold base64 to 76 chars per RFC 2045.
|
|
110
|
-
const folded = encoded.match(/.{1,76}/g)?.join(CRLF) ?? encoded;
|
|
111
128
|
return (`Content-Type: ${mimeType}; charset="UTF-8"${CRLF}` +
|
|
112
129
|
`Content-Transfer-Encoding: base64${CRLF}${CRLF}` +
|
|
113
|
-
`${
|
|
130
|
+
`${foldBase64(encoded)}${CRLF}`);
|
|
131
|
+
}
|
|
132
|
+
function normalizeAttachment(input, index) {
|
|
133
|
+
if (!input || typeof input !== "object") {
|
|
134
|
+
throw new Error(`gmail: attachment ${index} must be an object`);
|
|
135
|
+
}
|
|
136
|
+
const content = input.contentBase64;
|
|
137
|
+
const contentBase64 = typeof content === "string"
|
|
138
|
+
? content
|
|
139
|
+
: content &&
|
|
140
|
+
typeof content === "object" &&
|
|
141
|
+
typeof content.contentBase64 === "string"
|
|
142
|
+
? content.contentBase64
|
|
143
|
+
: undefined;
|
|
144
|
+
if (typeof contentBase64 !== "string") {
|
|
145
|
+
throw new Error(`gmail: attachment ${index} contentBase64 must be a base64 string`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
name: input.name ?? input.filename ?? `attachment-${index + 1}`,
|
|
149
|
+
mimeType: input.mimeType ?? "application/octet-stream",
|
|
150
|
+
contentBase64,
|
|
151
|
+
};
|
|
114
152
|
}
|
|
115
153
|
function attachmentPart(att) {
|
|
116
154
|
const encodedName = encodeHeaderWord(att.name);
|
|
117
|
-
const folded = att.contentBase64.match(/.{1,76}/g)?.join(CRLF) ?? att.contentBase64;
|
|
118
155
|
return (`Content-Type: ${att.mimeType}; name="${encodedName}"${CRLF}` +
|
|
119
156
|
`Content-Disposition: attachment; filename="${encodedName}"${CRLF}` +
|
|
120
157
|
`Content-Transfer-Encoding: base64${CRLF}${CRLF}` +
|
|
121
|
-
`${
|
|
158
|
+
`${foldBase64(att.contentBase64)}${CRLF}`);
|
|
122
159
|
}
|
|
123
160
|
/**
|
|
124
161
|
* Build a MIME message and return its base64url-encoded form,
|
|
@@ -143,7 +180,8 @@ export function encodeEmail(email) {
|
|
|
143
180
|
headers.push(header("MIME-Version", "1.0"));
|
|
144
181
|
const text = email.text ?? "";
|
|
145
182
|
const html = email.html ?? "";
|
|
146
|
-
const atts = email.attachments ?? [];
|
|
183
|
+
const atts = (email.attachments ?? []).map((att, index) => normalizeAttachment(att, index));
|
|
184
|
+
assertAttachmentPayloadFits(atts);
|
|
147
185
|
const hasAtt = atts.length > 0;
|
|
148
186
|
const hasBoth = text && html;
|
|
149
187
|
let bodyBlock;
|
|
@@ -189,6 +227,10 @@ export function encodeEmail(email) {
|
|
|
189
227
|
`--${mixedBoundary}--${CRLF}`;
|
|
190
228
|
}
|
|
191
229
|
const raw = `${headers.join("")}${rootType}${bodyBlock}`;
|
|
230
|
+
const rawBytes = Buffer.byteLength(raw, "utf-8");
|
|
231
|
+
if (rawBytes > GMAIL_MAX_RAW_BYTES) {
|
|
232
|
+
throw new Error(`gmail: encoded message is ${rawBytes} bytes; Gmail API raw messages must be <= ${GMAIL_MAX_RAW_BYTES} bytes`);
|
|
233
|
+
}
|
|
192
234
|
return base64url(raw);
|
|
193
235
|
}
|
|
194
236
|
// ─── Email address helpers ───────────────────────────────────────
|
|
@@ -351,7 +393,11 @@ function simplifyMessage(raw, labels) {
|
|
|
351
393
|
}
|
|
352
394
|
}
|
|
353
395
|
// Body + attachments only meaningful when format=full was used.
|
|
354
|
-
const acc = {
|
|
396
|
+
const acc = {
|
|
397
|
+
text: [],
|
|
398
|
+
html: [],
|
|
399
|
+
attachments: [],
|
|
400
|
+
};
|
|
355
401
|
walkPayload(payload, acc);
|
|
356
402
|
if (acc.text.length > 0)
|
|
357
403
|
out.text = acc.text.join("\n");
|
|
@@ -412,7 +458,9 @@ async function replyToMessage(ctx, messageId, p) {
|
|
|
412
458
|
to,
|
|
413
459
|
cc: normalizeAddressList(p.cc),
|
|
414
460
|
bcc: normalizeAddressList(p.bcc),
|
|
415
|
-
subject: subject.toLowerCase().startsWith("re:")
|
|
461
|
+
subject: subject.toLowerCase().startsWith("re:")
|
|
462
|
+
? subject
|
|
463
|
+
: `Re: ${subject}`,
|
|
416
464
|
text: p.text,
|
|
417
465
|
html: p.html,
|
|
418
466
|
inReplyTo: messageIdHeader,
|
|
@@ -756,7 +804,11 @@ export default function gmail(rl) {
|
|
|
756
804
|
description: "Get a thread by ID",
|
|
757
805
|
inputSchema: {
|
|
758
806
|
id: { type: "string", required: true },
|
|
759
|
-
format: {
|
|
807
|
+
format: {
|
|
808
|
+
type: "string",
|
|
809
|
+
required: false,
|
|
810
|
+
description: "minimal | full | metadata",
|
|
811
|
+
},
|
|
760
812
|
metadataHeaders: { type: "array", required: false },
|
|
761
813
|
simple: {
|
|
762
814
|
type: "boolean",
|
|
@@ -996,10 +1048,12 @@ export default function gmail(rl) {
|
|
|
996
1048
|
async execute(input, ctx) {
|
|
997
1049
|
const p = (input ?? {});
|
|
998
1050
|
const body = { name: p.name };
|
|
999
|
-
if (p.labelListVisibility)
|
|
1051
|
+
if (p.labelListVisibility) {
|
|
1000
1052
|
body.labelListVisibility = p.labelListVisibility;
|
|
1001
|
-
|
|
1053
|
+
}
|
|
1054
|
+
if (p.messageListVisibility) {
|
|
1002
1055
|
body.messageListVisibility = p.messageListVisibility;
|
|
1056
|
+
}
|
|
1003
1057
|
return gmailRequest(ctx, "POST", "/labels", body);
|
|
1004
1058
|
},
|
|
1005
1059
|
});
|
|
@@ -1039,10 +1093,12 @@ export default function gmail(rl) {
|
|
|
1039
1093
|
const body = {};
|
|
1040
1094
|
if (p.name)
|
|
1041
1095
|
body.name = p.name;
|
|
1042
|
-
if (p.labelListVisibility)
|
|
1096
|
+
if (p.labelListVisibility) {
|
|
1043
1097
|
body.labelListVisibility = p.labelListVisibility;
|
|
1044
|
-
|
|
1098
|
+
}
|
|
1099
|
+
if (p.messageListVisibility) {
|
|
1045
1100
|
body.messageListVisibility = p.messageListVisibility;
|
|
1101
|
+
}
|
|
1046
1102
|
return gmailRequest(ctx, "PATCH", `/labels/${p.id}`, body);
|
|
1047
1103
|
},
|
|
1048
1104
|
});
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { exec as execCb, execFile as execFileCb } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { access, appendFile, copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { arch, cpus, EOL, freemem, homedir, hostname, platform, release, tmpdir, totalmem, type, uptime, userInfo, } from "node:os";
|
|
6
|
+
import { basename, delimiter, dirname, extname, format, isAbsolute, join, normalize, parse, relative, resolve, sep, } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
const exec = promisify(execCb);
|
|
9
|
+
const execFile = promisify(execFileCb);
|
|
10
|
+
const pathInput = (description = "Filesystem path") => ({
|
|
11
|
+
path: { type: "string", required: true, description },
|
|
12
|
+
});
|
|
13
|
+
function action(name, def) {
|
|
14
|
+
return { name, ...def };
|
|
15
|
+
}
|
|
16
|
+
export default function node(rl) {
|
|
17
|
+
rl.setName("node");
|
|
18
|
+
rl.setVersion("0.1.0");
|
|
19
|
+
for (const action of nodeActions) {
|
|
20
|
+
rl.registerAction(action.name, action);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const nodeActions = [
|
|
24
|
+
// fs/promises-shaped actions
|
|
25
|
+
action("fs.readFile", {
|
|
26
|
+
description: "Read a file from the host filesystem",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
path: { type: "string", required: true, description: "File path" },
|
|
29
|
+
encoding: {
|
|
30
|
+
type: "string",
|
|
31
|
+
required: false,
|
|
32
|
+
description: "Text encoding. Defaults to utf8. Use base64 for binary-safe reads.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async execute(input) {
|
|
36
|
+
const { path, encoding = "utf8" } = objectInput(input);
|
|
37
|
+
return readFile(path, { encoding });
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
action("fs.writeFile", {
|
|
41
|
+
description: "Write text data to a file on the host filesystem",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
path: { type: "string", required: true, description: "File path" },
|
|
44
|
+
data: { type: "string", required: true, description: "File contents" },
|
|
45
|
+
encoding: {
|
|
46
|
+
type: "string",
|
|
47
|
+
required: false,
|
|
48
|
+
description: "Text encoding. Defaults to utf8.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
async execute(input) {
|
|
52
|
+
const { path, data, encoding = "utf8", } = objectInput(input);
|
|
53
|
+
await writeFile(path, data, { encoding });
|
|
54
|
+
return ok({ path });
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
action("fs.appendFile", {
|
|
58
|
+
description: "Append text data to a file on the host filesystem",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
path: { type: "string", required: true, description: "File path" },
|
|
61
|
+
data: {
|
|
62
|
+
type: "string",
|
|
63
|
+
required: true,
|
|
64
|
+
description: "File contents to append",
|
|
65
|
+
},
|
|
66
|
+
encoding: {
|
|
67
|
+
type: "string",
|
|
68
|
+
required: false,
|
|
69
|
+
description: "Text encoding. Defaults to utf8.",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
async execute(input) {
|
|
73
|
+
const { path, data, encoding = "utf8", } = objectInput(input);
|
|
74
|
+
await appendFile(path, data, { encoding });
|
|
75
|
+
return ok({ path });
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
action("fs.readdir", {
|
|
79
|
+
description: "List a directory",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
...pathInput("Directory path"),
|
|
82
|
+
withFileTypes: {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
required: false,
|
|
85
|
+
description: "Return typed entries instead of names",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
async execute(input) {
|
|
89
|
+
const { path, withFileTypes = false } = objectInput(input);
|
|
90
|
+
if (!withFileTypes)
|
|
91
|
+
return readdir(path);
|
|
92
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
93
|
+
return entries.map((entry) => ({
|
|
94
|
+
name: entry.name,
|
|
95
|
+
isFile: entry.isFile(),
|
|
96
|
+
isDirectory: entry.isDirectory(),
|
|
97
|
+
isSymbolicLink: entry.isSymbolicLink(),
|
|
98
|
+
}));
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
action("fs.stat", {
|
|
102
|
+
description: "Stat a filesystem path",
|
|
103
|
+
inputSchema: pathInput(),
|
|
104
|
+
async execute(input) {
|
|
105
|
+
return serializeStats(await stat(objectInput(input).path));
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
action("fs.lstat", {
|
|
109
|
+
description: "lstat a filesystem path",
|
|
110
|
+
inputSchema: pathInput(),
|
|
111
|
+
async execute(input) {
|
|
112
|
+
return serializeStats(await lstat(objectInput(input).path));
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
action("fs.exists", {
|
|
116
|
+
description: "Check whether a filesystem path exists",
|
|
117
|
+
inputSchema: pathInput(),
|
|
118
|
+
execute(input) {
|
|
119
|
+
return existsSync(objectInput(input).path);
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
action("fs.access", {
|
|
123
|
+
description: "Check file access",
|
|
124
|
+
inputSchema: pathInput(),
|
|
125
|
+
async execute(input) {
|
|
126
|
+
await access(objectInput(input).path);
|
|
127
|
+
return ok();
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
action("fs.mkdir", {
|
|
131
|
+
description: "Create a directory",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
...pathInput(),
|
|
134
|
+
recursive: {
|
|
135
|
+
type: "boolean",
|
|
136
|
+
required: false,
|
|
137
|
+
description: "Create parent directories. Defaults to true.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
async execute(input) {
|
|
141
|
+
const { path, recursive = true } = objectInput(input);
|
|
142
|
+
await mkdir(path, { recursive });
|
|
143
|
+
return ok({ path });
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
action("fs.rm", {
|
|
147
|
+
description: "Remove a file or directory",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
...pathInput(),
|
|
150
|
+
recursive: {
|
|
151
|
+
type: "boolean",
|
|
152
|
+
required: false,
|
|
153
|
+
description: "Remove directories recursively",
|
|
154
|
+
},
|
|
155
|
+
force: {
|
|
156
|
+
type: "boolean",
|
|
157
|
+
required: false,
|
|
158
|
+
description: "Ignore missing paths",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async execute(input) {
|
|
162
|
+
const { path, recursive = false, force = false, } = objectInput(input);
|
|
163
|
+
await rm(path, { recursive, force });
|
|
164
|
+
return ok({ path });
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
action("fs.unlink", {
|
|
168
|
+
description: "Remove a file",
|
|
169
|
+
inputSchema: pathInput(),
|
|
170
|
+
async execute(input) {
|
|
171
|
+
const { path } = objectInput(input);
|
|
172
|
+
await unlink(path);
|
|
173
|
+
return ok({ path });
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
action("fs.rename", {
|
|
177
|
+
description: "Rename a file or directory",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
from: { type: "string", required: true, description: "Source path" },
|
|
180
|
+
to: { type: "string", required: true, description: "Destination path" },
|
|
181
|
+
},
|
|
182
|
+
async execute(input) {
|
|
183
|
+
const { from, to } = objectInput(input);
|
|
184
|
+
await rename(from, to);
|
|
185
|
+
return ok({ from, to });
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
action("fs.copyFile", {
|
|
189
|
+
description: "Copy a file",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
from: { type: "string", required: true, description: "Source path" },
|
|
192
|
+
to: { type: "string", required: true, description: "Destination path" },
|
|
193
|
+
},
|
|
194
|
+
async execute(input) {
|
|
195
|
+
const { from, to } = objectInput(input);
|
|
196
|
+
await copyFile(from, to);
|
|
197
|
+
return ok({ from, to });
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
// path-shaped actions. Most accept either an array or { segments: string[] }.
|
|
201
|
+
action("path.join", {
|
|
202
|
+
description: "Join path segments",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
segments: {
|
|
205
|
+
type: "array",
|
|
206
|
+
required: true,
|
|
207
|
+
description: "Path segments",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
execute(input) {
|
|
211
|
+
return join(...stringArrayInput(input));
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
action("path.resolve", {
|
|
215
|
+
description: "Resolve path segments",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
segments: {
|
|
218
|
+
type: "array",
|
|
219
|
+
required: true,
|
|
220
|
+
description: "Path segments",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
execute(input) {
|
|
224
|
+
return resolve(...stringArrayInput(input));
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
action("path.normalize", {
|
|
228
|
+
description: "Normalize a path",
|
|
229
|
+
inputSchema: pathInput(),
|
|
230
|
+
execute(input) {
|
|
231
|
+
return normalize(objectInput(input).path);
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
action("path.dirname", {
|
|
235
|
+
description: "Get a path dirname",
|
|
236
|
+
inputSchema: pathInput(),
|
|
237
|
+
execute(input) {
|
|
238
|
+
return dirname(objectInput(input).path);
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
action("path.basename", {
|
|
242
|
+
description: "Get a path basename",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
...pathInput(),
|
|
245
|
+
suffix: {
|
|
246
|
+
type: "string",
|
|
247
|
+
required: false,
|
|
248
|
+
description: "Optional suffix to remove",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
execute(input) {
|
|
252
|
+
const { path, suffix } = objectInput(input);
|
|
253
|
+
return basename(path, suffix);
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
action("path.extname", {
|
|
257
|
+
description: "Get a path extension",
|
|
258
|
+
inputSchema: pathInput(),
|
|
259
|
+
execute(input) {
|
|
260
|
+
return extname(objectInput(input).path);
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
action("path.relative", {
|
|
264
|
+
description: "Get a relative path",
|
|
265
|
+
inputSchema: {
|
|
266
|
+
from: { type: "string", required: true, description: "Source path" },
|
|
267
|
+
to: { type: "string", required: true, description: "Destination path" },
|
|
268
|
+
},
|
|
269
|
+
execute(input) {
|
|
270
|
+
const { from, to } = objectInput(input);
|
|
271
|
+
return relative(from, to);
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
action("path.isAbsolute", {
|
|
275
|
+
description: "Check whether a path is absolute",
|
|
276
|
+
inputSchema: pathInput(),
|
|
277
|
+
execute(input) {
|
|
278
|
+
return isAbsolute(objectInput(input).path);
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
action("path.parse", {
|
|
282
|
+
description: "Parse a path into components",
|
|
283
|
+
inputSchema: pathInput(),
|
|
284
|
+
execute(input) {
|
|
285
|
+
return parse(objectInput(input).path);
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
action("path.format", {
|
|
289
|
+
description: "Format a path object into a path string",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
pathObject: {
|
|
292
|
+
type: "object",
|
|
293
|
+
required: true,
|
|
294
|
+
description: "Node path object",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
execute(input) {
|
|
298
|
+
return format(objectInput(input)
|
|
299
|
+
.pathObject);
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
action("path.constants", {
|
|
303
|
+
description: "Get path separator constants",
|
|
304
|
+
execute() {
|
|
305
|
+
return { sep, delimiter };
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
// os/process/shell actions
|
|
309
|
+
action("os.info", {
|
|
310
|
+
description: "Get useful host OS information",
|
|
311
|
+
execute() {
|
|
312
|
+
return {
|
|
313
|
+
platform: platform(),
|
|
314
|
+
arch: arch(),
|
|
315
|
+
type: type(),
|
|
316
|
+
release: release(),
|
|
317
|
+
hostname: hostname(),
|
|
318
|
+
homedir: homedir(),
|
|
319
|
+
tmpdir: tmpdir(),
|
|
320
|
+
uptime: uptime(),
|
|
321
|
+
totalmem: totalmem(),
|
|
322
|
+
freemem: freemem(),
|
|
323
|
+
eol: EOL,
|
|
324
|
+
cpus: cpus().map((cpu) => ({ model: cpu.model, speed: cpu.speed })),
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
action("os.platform", {
|
|
329
|
+
description: "Get OS platform",
|
|
330
|
+
execute: () => platform(),
|
|
331
|
+
}),
|
|
332
|
+
action("os.arch", {
|
|
333
|
+
description: "Get OS architecture",
|
|
334
|
+
execute: () => arch(),
|
|
335
|
+
}),
|
|
336
|
+
action("os.homedir", {
|
|
337
|
+
description: "Get home directory",
|
|
338
|
+
execute: () => homedir(),
|
|
339
|
+
}),
|
|
340
|
+
action("os.tmpdir", {
|
|
341
|
+
description: "Get temp directory",
|
|
342
|
+
execute: () => tmpdir(),
|
|
343
|
+
}),
|
|
344
|
+
action("os.userInfo", {
|
|
345
|
+
description: "Get current user information",
|
|
346
|
+
execute() {
|
|
347
|
+
const info = userInfo();
|
|
348
|
+
return {
|
|
349
|
+
username: info.username,
|
|
350
|
+
uid: info.uid,
|
|
351
|
+
gid: info.gid,
|
|
352
|
+
shell: info.shell,
|
|
353
|
+
homedir: info.homedir,
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
action("process.cwd", {
|
|
358
|
+
description: "Get the current working directory",
|
|
359
|
+
execute: () => process.cwd(),
|
|
360
|
+
}),
|
|
361
|
+
action("process.env", {
|
|
362
|
+
description: "Read environment variables from the host process",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
name: {
|
|
365
|
+
type: "string",
|
|
366
|
+
required: false,
|
|
367
|
+
description: "Optional variable name. Omit to return all env vars.",
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
execute(input) {
|
|
371
|
+
const name = input?.name;
|
|
372
|
+
return name ? process.env[name] : { ...process.env };
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
action("process.exec", {
|
|
376
|
+
description: "Run a shell command on the host",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
command: {
|
|
379
|
+
type: "string",
|
|
380
|
+
required: true,
|
|
381
|
+
description: "Shell command",
|
|
382
|
+
},
|
|
383
|
+
cwd: {
|
|
384
|
+
type: "string",
|
|
385
|
+
required: false,
|
|
386
|
+
description: "Working directory",
|
|
387
|
+
},
|
|
388
|
+
timeout: {
|
|
389
|
+
type: "number",
|
|
390
|
+
required: false,
|
|
391
|
+
description: "Timeout in milliseconds",
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
async execute(input) {
|
|
395
|
+
const { command, cwd, timeout } = objectInput(input);
|
|
396
|
+
const { stdout, stderr } = await exec(command, {
|
|
397
|
+
cwd,
|
|
398
|
+
timeout,
|
|
399
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
400
|
+
});
|
|
401
|
+
return { stdout, stderr };
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
action("process.execFile", {
|
|
405
|
+
description: "Run a host executable without a shell",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
file: {
|
|
408
|
+
type: "string",
|
|
409
|
+
required: true,
|
|
410
|
+
description: "Executable path or name",
|
|
411
|
+
},
|
|
412
|
+
args: { type: "array", required: false, description: "Arguments" },
|
|
413
|
+
cwd: {
|
|
414
|
+
type: "string",
|
|
415
|
+
required: false,
|
|
416
|
+
description: "Working directory",
|
|
417
|
+
},
|
|
418
|
+
timeout: {
|
|
419
|
+
type: "number",
|
|
420
|
+
required: false,
|
|
421
|
+
description: "Timeout in milliseconds",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
async execute(input) {
|
|
425
|
+
const { file, args = [], cwd, timeout, } = objectInput(input);
|
|
426
|
+
const { stdout, stderr } = await execFile(file, args, {
|
|
427
|
+
cwd,
|
|
428
|
+
timeout,
|
|
429
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
430
|
+
});
|
|
431
|
+
return { stdout, stderr };
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
action("crypto.randomUUID", {
|
|
435
|
+
description: "Generate a random UUID using the host crypto runtime",
|
|
436
|
+
execute() {
|
|
437
|
+
return randomUUID();
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
action("crypto.randomBytes", {
|
|
441
|
+
description: "Generate cryptographically strong random bytes as hex or base64",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
size: {
|
|
444
|
+
type: "number",
|
|
445
|
+
required: true,
|
|
446
|
+
description: "Number of bytes",
|
|
447
|
+
},
|
|
448
|
+
encoding: {
|
|
449
|
+
type: "string",
|
|
450
|
+
required: false,
|
|
451
|
+
description: "hex or base64. Defaults to hex.",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
execute(input) {
|
|
455
|
+
const { size, encoding = "hex" } = objectInput(input);
|
|
456
|
+
return randomBytes(size).toString(encoding);
|
|
457
|
+
},
|
|
458
|
+
}),
|
|
459
|
+
action("crypto.hash", {
|
|
460
|
+
description: "Hash text data with a Node crypto digest algorithm",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
algorithm: {
|
|
463
|
+
type: "string",
|
|
464
|
+
required: false,
|
|
465
|
+
description: "Digest algorithm. Defaults to sha256.",
|
|
466
|
+
},
|
|
467
|
+
data: { type: "string", required: true, description: "Data to hash" },
|
|
468
|
+
encoding: {
|
|
469
|
+
type: "string",
|
|
470
|
+
required: false,
|
|
471
|
+
description: "Digest encoding. Defaults to hex.",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
execute(input) {
|
|
475
|
+
const { algorithm = "sha256", data, encoding = "hex", } = objectInput(input);
|
|
476
|
+
return createHash(algorithm).update(data).digest(encoding);
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
action("fetch", {
|
|
480
|
+
description: "Perform an HTTP fetch from the host runtime",
|
|
481
|
+
inputSchema: {
|
|
482
|
+
url: { type: "string", required: true, description: "Request URL" },
|
|
483
|
+
method: { type: "string", required: false, description: "HTTP method" },
|
|
484
|
+
headers: {
|
|
485
|
+
type: "object",
|
|
486
|
+
required: false,
|
|
487
|
+
description: "Request headers",
|
|
488
|
+
},
|
|
489
|
+
body: { type: "string", required: false, description: "Request body" },
|
|
490
|
+
},
|
|
491
|
+
async execute(input) {
|
|
492
|
+
const { url, method, headers, body } = objectInput(input);
|
|
493
|
+
const res = await fetch(url, { method, headers, body });
|
|
494
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
495
|
+
const text = await res.text();
|
|
496
|
+
return {
|
|
497
|
+
ok: res.ok,
|
|
498
|
+
status: res.status,
|
|
499
|
+
statusText: res.statusText,
|
|
500
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
501
|
+
body: contentType.includes("application/json") ? safeJson(text) : text,
|
|
502
|
+
};
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
];
|
|
506
|
+
function objectInput(input) {
|
|
507
|
+
return (input && typeof input === "object" ? input : {});
|
|
508
|
+
}
|
|
509
|
+
function stringArrayInput(input) {
|
|
510
|
+
if (Array.isArray(input))
|
|
511
|
+
return input.map(String);
|
|
512
|
+
const { segments } = objectInput(input);
|
|
513
|
+
return Array.isArray(segments) ? segments.map(String) : [];
|
|
514
|
+
}
|
|
515
|
+
function ok(extra = {}) {
|
|
516
|
+
return { ok: true, ...extra };
|
|
517
|
+
}
|
|
518
|
+
function safeJson(text) {
|
|
519
|
+
try {
|
|
520
|
+
return JSON.parse(text);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return text;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function serializeStats(s) {
|
|
527
|
+
return {
|
|
528
|
+
size: s.size,
|
|
529
|
+
mode: s.mode,
|
|
530
|
+
uid: s.uid,
|
|
531
|
+
gid: s.gid,
|
|
532
|
+
atimeMs: s.atimeMs,
|
|
533
|
+
mtimeMs: s.mtimeMs,
|
|
534
|
+
ctimeMs: s.ctimeMs,
|
|
535
|
+
birthtimeMs: s.birthtimeMs,
|
|
536
|
+
isFile: s.isFile(),
|
|
537
|
+
isDirectory: s.isDirectory(),
|
|
538
|
+
isSymbolicLink: s.isSymbolicLink(),
|
|
539
|
+
isBlockDevice: s.isBlockDevice(),
|
|
540
|
+
isCharacterDevice: s.isCharacterDevice(),
|
|
541
|
+
isFIFO: s.isFIFO(),
|
|
542
|
+
isSocket: s.isSocket(),
|
|
543
|
+
};
|
|
544
|
+
}
|
package/dist/sdk.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare class Runline {
|
|
|
12
12
|
private _config;
|
|
13
13
|
private constructor();
|
|
14
14
|
static create(options?: RunlineOptions): Runline;
|
|
15
|
-
/** Execute JavaScript code in the
|
|
15
|
+
/** Execute JavaScript code in the QuickJS runtime. */
|
|
16
16
|
execute(code: string): Promise<ExecuteResult>;
|
|
17
17
|
/** Register an additional plugin after creation. */
|
|
18
18
|
addPlugin(pluginOrFn: PluginDef | PluginFunction, connections?: ConnectionConfig[]): void;
|
package/dist/sdk.js
CHANGED
|
@@ -23,7 +23,7 @@ export class Runline {
|
|
|
23
23
|
static create(options = {}) {
|
|
24
24
|
return new Runline(options);
|
|
25
25
|
}
|
|
26
|
-
/** Execute JavaScript code in the
|
|
26
|
+
/** Execute JavaScript code in the QuickJS runtime. */
|
|
27
27
|
async execute(code) {
|
|
28
28
|
const engine = new ExecutionEngine(this._registry, this._config);
|
|
29
29
|
return engine.execute(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Code mode for agents — turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
],
|
|
56
56
|
"license": "MIT",
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@biomejs/biome": "^2.
|
|
58
|
+
"@biomejs/biome": "^2.4.12",
|
|
59
59
|
"@types/bun": "^1.2.17",
|
|
60
60
|
"@types/node": "^25.5.0",
|
|
61
61
|
"@types/proper-lockfile": "^4.1.4",
|