runline 0.7.0 → 0.7.2

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.
@@ -1,12 +1,14 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { getQuickJS, shouldInterruptAfterDeadline, } from "quickjs-emscripten";
3
3
  import { applyEnvOverrides, updateConnectionConfig } from "../config/loader.js";
4
+ import { registerNodePlugin } from "../plugin/node-plugin.js";
4
5
  export class ExecutionEngine {
5
6
  registry;
6
7
  config;
7
8
  constructor(registry, config) {
8
9
  this.registry = registry;
9
10
  this.config = config;
11
+ registerNodePlugin(this.registry);
10
12
  }
11
13
  async execute(code, options) {
12
14
  const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
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 sandbox")
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 sandbox with an \`actions\` proxy.
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
+ The built-in \`node\` global exposes host-backed fs/path/os/process/crypto/fetch actions.
42
43
 
43
44
  Examples:
44
45
  $ runline exec 'return await docker.containers.list()'
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { findConfigDir } from "../config/loader.js";
6
6
  import { resolvePluginExport } from "./api.js";
7
+ import { registerNodePlugin } from "./node-plugin.js";
7
8
  import { registry } from "./registry.js";
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  export async function loadPluginFromPath(path) {
@@ -168,4 +169,5 @@ export async function loadAllPlugins() {
168
169
  for (const p of plugins) {
169
170
  registry.register(p);
170
171
  }
172
+ registerNodePlugin(registry);
171
173
  }
@@ -0,0 +1,4 @@
1
+ import type { PluginRegistry } from "./registry.js";
2
+ import type { PluginDef } from "./types.js";
3
+ export declare function registerNodePlugin(registry: PluginRegistry): void;
4
+ export declare const nodePlugin: PluginDef;
@@ -0,0 +1,546 @@
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 function registerNodePlugin(registry) {
17
+ registry.register(nodePlugin);
18
+ }
19
+ export const nodePlugin = {
20
+ name: "node",
21
+ version: "0.1.0",
22
+ actions: [
23
+ // fs/promises-shaped actions
24
+ action("fs.readFile", {
25
+ description: "Read a file from the host filesystem",
26
+ inputSchema: {
27
+ path: { type: "string", required: true, description: "File path" },
28
+ encoding: {
29
+ type: "string",
30
+ required: false,
31
+ description: "Text encoding. Defaults to utf8. Use base64 for binary-safe reads.",
32
+ },
33
+ },
34
+ async execute(input) {
35
+ const { path, encoding = "utf8" } = objectInput(input);
36
+ return readFile(path, { encoding });
37
+ },
38
+ }),
39
+ action("fs.writeFile", {
40
+ description: "Write text data to a file on the host filesystem",
41
+ inputSchema: {
42
+ path: { type: "string", required: true, description: "File path" },
43
+ data: { type: "string", required: true, description: "File contents" },
44
+ encoding: {
45
+ type: "string",
46
+ required: false,
47
+ description: "Text encoding. Defaults to utf8.",
48
+ },
49
+ },
50
+ async execute(input) {
51
+ const { path, data, encoding = "utf8", } = objectInput(input);
52
+ await writeFile(path, data, { encoding });
53
+ return ok({ path });
54
+ },
55
+ }),
56
+ action("fs.appendFile", {
57
+ description: "Append text data to a file on the host filesystem",
58
+ inputSchema: {
59
+ path: { type: "string", required: true, description: "File path" },
60
+ data: {
61
+ type: "string",
62
+ required: true,
63
+ description: "File contents to append",
64
+ },
65
+ encoding: {
66
+ type: "string",
67
+ required: false,
68
+ description: "Text encoding. Defaults to utf8.",
69
+ },
70
+ },
71
+ async execute(input) {
72
+ const { path, data, encoding = "utf8", } = objectInput(input);
73
+ await appendFile(path, data, { encoding });
74
+ return ok({ path });
75
+ },
76
+ }),
77
+ action("fs.readdir", {
78
+ description: "List a directory",
79
+ inputSchema: {
80
+ ...pathInput("Directory path"),
81
+ withFileTypes: {
82
+ type: "boolean",
83
+ required: false,
84
+ description: "Return typed entries instead of names",
85
+ },
86
+ },
87
+ async execute(input) {
88
+ const { path, withFileTypes = false } = objectInput(input);
89
+ if (!withFileTypes)
90
+ return readdir(path);
91
+ const entries = await readdir(path, { withFileTypes: true });
92
+ return entries.map((entry) => ({
93
+ name: entry.name,
94
+ isFile: entry.isFile(),
95
+ isDirectory: entry.isDirectory(),
96
+ isSymbolicLink: entry.isSymbolicLink(),
97
+ }));
98
+ },
99
+ }),
100
+ action("fs.stat", {
101
+ description: "Stat a filesystem path",
102
+ inputSchema: pathInput(),
103
+ async execute(input) {
104
+ return serializeStats(await stat(objectInput(input).path));
105
+ },
106
+ }),
107
+ action("fs.lstat", {
108
+ description: "lstat a filesystem path",
109
+ inputSchema: pathInput(),
110
+ async execute(input) {
111
+ return serializeStats(await lstat(objectInput(input).path));
112
+ },
113
+ }),
114
+ action("fs.exists", {
115
+ description: "Check whether a filesystem path exists",
116
+ inputSchema: pathInput(),
117
+ execute(input) {
118
+ return existsSync(objectInput(input).path);
119
+ },
120
+ }),
121
+ action("fs.access", {
122
+ description: "Check file access",
123
+ inputSchema: pathInput(),
124
+ async execute(input) {
125
+ await access(objectInput(input).path);
126
+ return ok();
127
+ },
128
+ }),
129
+ action("fs.mkdir", {
130
+ description: "Create a directory",
131
+ inputSchema: {
132
+ ...pathInput(),
133
+ recursive: {
134
+ type: "boolean",
135
+ required: false,
136
+ description: "Create parent directories. Defaults to true.",
137
+ },
138
+ },
139
+ async execute(input) {
140
+ const { path, recursive = true } = objectInput(input);
141
+ await mkdir(path, { recursive });
142
+ return ok({ path });
143
+ },
144
+ }),
145
+ action("fs.rm", {
146
+ description: "Remove a file or directory",
147
+ inputSchema: {
148
+ ...pathInput(),
149
+ recursive: {
150
+ type: "boolean",
151
+ required: false,
152
+ description: "Remove directories recursively",
153
+ },
154
+ force: {
155
+ type: "boolean",
156
+ required: false,
157
+ description: "Ignore missing paths",
158
+ },
159
+ },
160
+ async execute(input) {
161
+ const { path, recursive = false, force = false, } = objectInput(input);
162
+ await rm(path, { recursive, force });
163
+ return ok({ path });
164
+ },
165
+ }),
166
+ action("fs.unlink", {
167
+ description: "Remove a file",
168
+ inputSchema: pathInput(),
169
+ async execute(input) {
170
+ const { path } = objectInput(input);
171
+ await unlink(path);
172
+ return ok({ path });
173
+ },
174
+ }),
175
+ action("fs.rename", {
176
+ description: "Rename a file or directory",
177
+ inputSchema: {
178
+ from: { type: "string", required: true, description: "Source path" },
179
+ to: { type: "string", required: true, description: "Destination path" },
180
+ },
181
+ async execute(input) {
182
+ const { from, to } = objectInput(input);
183
+ await rename(from, to);
184
+ return ok({ from, to });
185
+ },
186
+ }),
187
+ action("fs.copyFile", {
188
+ description: "Copy a file",
189
+ inputSchema: {
190
+ from: { type: "string", required: true, description: "Source path" },
191
+ to: { type: "string", required: true, description: "Destination path" },
192
+ },
193
+ async execute(input) {
194
+ const { from, to } = objectInput(input);
195
+ await copyFile(from, to);
196
+ return ok({ from, to });
197
+ },
198
+ }),
199
+ // path-shaped actions. Most accept either an array or { segments: string[] }.
200
+ action("path.join", {
201
+ description: "Join path segments",
202
+ inputSchema: {
203
+ segments: {
204
+ type: "array",
205
+ required: true,
206
+ description: "Path segments",
207
+ },
208
+ },
209
+ execute(input) {
210
+ return join(...stringArrayInput(input));
211
+ },
212
+ }),
213
+ action("path.resolve", {
214
+ description: "Resolve path segments",
215
+ inputSchema: {
216
+ segments: {
217
+ type: "array",
218
+ required: true,
219
+ description: "Path segments",
220
+ },
221
+ },
222
+ execute(input) {
223
+ return resolve(...stringArrayInput(input));
224
+ },
225
+ }),
226
+ action("path.normalize", {
227
+ description: "Normalize a path",
228
+ inputSchema: pathInput(),
229
+ execute(input) {
230
+ return normalize(objectInput(input).path);
231
+ },
232
+ }),
233
+ action("path.dirname", {
234
+ description: "Get a path dirname",
235
+ inputSchema: pathInput(),
236
+ execute(input) {
237
+ return dirname(objectInput(input).path);
238
+ },
239
+ }),
240
+ action("path.basename", {
241
+ description: "Get a path basename",
242
+ inputSchema: {
243
+ ...pathInput(),
244
+ suffix: {
245
+ type: "string",
246
+ required: false,
247
+ description: "Optional suffix to remove",
248
+ },
249
+ },
250
+ execute(input) {
251
+ const { path, suffix } = objectInput(input);
252
+ return basename(path, suffix);
253
+ },
254
+ }),
255
+ action("path.extname", {
256
+ description: "Get a path extension",
257
+ inputSchema: pathInput(),
258
+ execute(input) {
259
+ return extname(objectInput(input).path);
260
+ },
261
+ }),
262
+ action("path.relative", {
263
+ description: "Get a relative path",
264
+ inputSchema: {
265
+ from: { type: "string", required: true, description: "Source path" },
266
+ to: { type: "string", required: true, description: "Destination path" },
267
+ },
268
+ execute(input) {
269
+ const { from, to } = objectInput(input);
270
+ return relative(from, to);
271
+ },
272
+ }),
273
+ action("path.isAbsolute", {
274
+ description: "Check whether a path is absolute",
275
+ inputSchema: pathInput(),
276
+ execute(input) {
277
+ return isAbsolute(objectInput(input).path);
278
+ },
279
+ }),
280
+ action("path.parse", {
281
+ description: "Parse a path into components",
282
+ inputSchema: pathInput(),
283
+ execute(input) {
284
+ return parse(objectInput(input).path);
285
+ },
286
+ }),
287
+ action("path.format", {
288
+ description: "Format a path object into a path string",
289
+ inputSchema: {
290
+ pathObject: {
291
+ type: "object",
292
+ required: true,
293
+ description: "Node path object",
294
+ },
295
+ },
296
+ execute(input) {
297
+ return format(objectInput(input)
298
+ .pathObject);
299
+ },
300
+ }),
301
+ action("path.constants", {
302
+ description: "Get path separator constants",
303
+ execute() {
304
+ return { sep, delimiter };
305
+ },
306
+ }),
307
+ // os/process/shell actions
308
+ action("os.info", {
309
+ description: "Get useful host OS information",
310
+ execute() {
311
+ return {
312
+ platform: platform(),
313
+ arch: arch(),
314
+ type: type(),
315
+ release: release(),
316
+ hostname: hostname(),
317
+ homedir: homedir(),
318
+ tmpdir: tmpdir(),
319
+ uptime: uptime(),
320
+ totalmem: totalmem(),
321
+ freemem: freemem(),
322
+ eol: EOL,
323
+ cpus: cpus().map((cpu) => ({ model: cpu.model, speed: cpu.speed })),
324
+ };
325
+ },
326
+ }),
327
+ action("os.platform", {
328
+ description: "Get OS platform",
329
+ execute: () => platform(),
330
+ }),
331
+ action("os.arch", {
332
+ description: "Get OS architecture",
333
+ execute: () => arch(),
334
+ }),
335
+ action("os.homedir", {
336
+ description: "Get home directory",
337
+ execute: () => homedir(),
338
+ }),
339
+ action("os.tmpdir", {
340
+ description: "Get temp directory",
341
+ execute: () => tmpdir(),
342
+ }),
343
+ action("os.userInfo", {
344
+ description: "Get current user information",
345
+ execute() {
346
+ const info = userInfo();
347
+ return {
348
+ username: info.username,
349
+ uid: info.uid,
350
+ gid: info.gid,
351
+ shell: info.shell,
352
+ homedir: info.homedir,
353
+ };
354
+ },
355
+ }),
356
+ action("process.cwd", {
357
+ description: "Get the current working directory",
358
+ execute: () => process.cwd(),
359
+ }),
360
+ action("process.env", {
361
+ description: "Read environment variables from the host process",
362
+ inputSchema: {
363
+ name: {
364
+ type: "string",
365
+ required: false,
366
+ description: "Optional variable name. Omit to return all env vars.",
367
+ },
368
+ },
369
+ execute(input) {
370
+ const name = input?.name;
371
+ return name ? process.env[name] : { ...process.env };
372
+ },
373
+ }),
374
+ action("process.exec", {
375
+ description: "Run a shell command on the host",
376
+ inputSchema: {
377
+ command: {
378
+ type: "string",
379
+ required: true,
380
+ description: "Shell command",
381
+ },
382
+ cwd: {
383
+ type: "string",
384
+ required: false,
385
+ description: "Working directory",
386
+ },
387
+ timeout: {
388
+ type: "number",
389
+ required: false,
390
+ description: "Timeout in milliseconds",
391
+ },
392
+ },
393
+ async execute(input) {
394
+ const { command, cwd, timeout } = objectInput(input);
395
+ const { stdout, stderr } = await exec(command, {
396
+ cwd,
397
+ timeout,
398
+ maxBuffer: 10 * 1024 * 1024,
399
+ });
400
+ return { stdout, stderr };
401
+ },
402
+ }),
403
+ action("process.execFile", {
404
+ description: "Run a host executable without a shell",
405
+ inputSchema: {
406
+ file: {
407
+ type: "string",
408
+ required: true,
409
+ description: "Executable path or name",
410
+ },
411
+ args: { type: "array", required: false, description: "Arguments" },
412
+ cwd: {
413
+ type: "string",
414
+ required: false,
415
+ description: "Working directory",
416
+ },
417
+ timeout: {
418
+ type: "number",
419
+ required: false,
420
+ description: "Timeout in milliseconds",
421
+ },
422
+ },
423
+ async execute(input) {
424
+ const { file, args = [], cwd, timeout, } = objectInput(input);
425
+ const { stdout, stderr } = await execFile(file, args, {
426
+ cwd,
427
+ timeout,
428
+ maxBuffer: 10 * 1024 * 1024,
429
+ });
430
+ return { stdout, stderr };
431
+ },
432
+ }),
433
+ action("crypto.randomUUID", {
434
+ description: "Generate a random UUID using the host crypto runtime",
435
+ execute() {
436
+ return randomUUID();
437
+ },
438
+ }),
439
+ action("crypto.randomBytes", {
440
+ description: "Generate cryptographically strong random bytes as hex or base64",
441
+ inputSchema: {
442
+ size: {
443
+ type: "number",
444
+ required: true,
445
+ description: "Number of bytes",
446
+ },
447
+ encoding: {
448
+ type: "string",
449
+ required: false,
450
+ description: "hex or base64. Defaults to hex.",
451
+ },
452
+ },
453
+ execute(input) {
454
+ const { size, encoding = "hex" } = objectInput(input);
455
+ return randomBytes(size).toString(encoding);
456
+ },
457
+ }),
458
+ action("crypto.hash", {
459
+ description: "Hash text data with a Node crypto digest algorithm",
460
+ inputSchema: {
461
+ algorithm: {
462
+ type: "string",
463
+ required: false,
464
+ description: "Digest algorithm. Defaults to sha256.",
465
+ },
466
+ data: { type: "string", required: true, description: "Data to hash" },
467
+ encoding: {
468
+ type: "string",
469
+ required: false,
470
+ description: "Digest encoding. Defaults to hex.",
471
+ },
472
+ },
473
+ execute(input) {
474
+ const { algorithm = "sha256", data, encoding = "hex", } = objectInput(input);
475
+ return createHash(algorithm).update(data).digest(encoding);
476
+ },
477
+ }),
478
+ action("fetch", {
479
+ description: "Perform an HTTP fetch from the host runtime",
480
+ inputSchema: {
481
+ url: { type: "string", required: true, description: "Request URL" },
482
+ method: { type: "string", required: false, description: "HTTP method" },
483
+ headers: {
484
+ type: "object",
485
+ required: false,
486
+ description: "Request headers",
487
+ },
488
+ body: { type: "string", required: false, description: "Request body" },
489
+ },
490
+ async execute(input) {
491
+ const { url, method, headers, body } = objectInput(input);
492
+ const res = await fetch(url, { method, headers, body });
493
+ const contentType = res.headers.get("content-type") ?? "";
494
+ const text = await res.text();
495
+ return {
496
+ ok: res.ok,
497
+ status: res.status,
498
+ statusText: res.statusText,
499
+ headers: Object.fromEntries(res.headers.entries()),
500
+ body: contentType.includes("application/json")
501
+ ? safeJson(text)
502
+ : text,
503
+ };
504
+ },
505
+ }),
506
+ ],
507
+ };
508
+ function objectInput(input) {
509
+ return (input && typeof input === "object" ? input : {});
510
+ }
511
+ function stringArrayInput(input) {
512
+ if (Array.isArray(input))
513
+ return input.map(String);
514
+ const { segments } = objectInput(input);
515
+ return Array.isArray(segments) ? segments.map(String) : [];
516
+ }
517
+ function ok(extra = {}) {
518
+ return { ok: true, ...extra };
519
+ }
520
+ function safeJson(text) {
521
+ try {
522
+ return JSON.parse(text);
523
+ }
524
+ catch {
525
+ return text;
526
+ }
527
+ }
528
+ function serializeStats(s) {
529
+ return {
530
+ size: s.size,
531
+ mode: s.mode,
532
+ uid: s.uid,
533
+ gid: s.gid,
534
+ atimeMs: s.atimeMs,
535
+ mtimeMs: s.mtimeMs,
536
+ ctimeMs: s.ctimeMs,
537
+ birthtimeMs: s.birthtimeMs,
538
+ isFile: s.isFile(),
539
+ isDirectory: s.isDirectory(),
540
+ isSymbolicLink: s.isSymbolicLink(),
541
+ isBlockDevice: s.isBlockDevice(),
542
+ isCharacterDevice: s.isCharacterDevice(),
543
+ isFIFO: s.isFIFO(),
544
+ isSocket: s.isSocket(),
545
+ };
546
+ }
@@ -31,6 +31,14 @@ const PROJECT_FIELDS = `id name description url icon color priority progress hea
31
31
  teams { nodes { id key } } createdAt updatedAt completedAt canceledAt`;
32
32
  const MILESTONE_FIELDS = `id name description targetDate sortOrder project { id name } createdAt updatedAt`;
33
33
  const PROJECT_UPDATE_FIELDS = `id body health url user { id name } project { id name } createdAt`;
34
+ const FEED_ITEM_FIELDS = `id createdAt updatedAt archivedAt team { id key name } user { id name }
35
+ projectUpdate { ${PROJECT_UPDATE_FIELDS} }
36
+ initiativeUpdate { id body health url user { id name } initiative { id name } createdAt }
37
+ post { id title body slugId type creator { id name } createdAt updatedAt }`;
38
+ const CUSTOM_VIEW_FIELDS = `id name description icon color shared slugId modelName
39
+ filterData projectFilterData initiativeFilterData feedItemFilterData
40
+ team { id key name } owner { id name } creator { id name }
41
+ createdAt updatedAt archivedAt`;
34
42
  const CYCLE_FIELDS = `id number name description startsAt endsAt completedAt progress team { id key } createdAt`;
35
43
  const INITIATIVE_FIELDS = `id name description url icon color status targetDate owner { id name }
36
44
  projects { nodes { id name } } createdAt updatedAt completedAt`;
@@ -93,7 +101,7 @@ const LIST_INPUT_SCHEMA = {
93
101
  // ---------- plugin ----------
94
102
  export default function linear(rl) {
95
103
  rl.setName("linear");
96
- rl.setVersion("0.3.0");
104
+ rl.setVersion("0.4.0");
97
105
  rl.setConnectionSchema({
98
106
  apiKey: {
99
107
  type: "string",
@@ -127,6 +135,35 @@ export default function linear(rl) {
127
135
  },
128
136
  });
129
137
  }
138
+ function customViewConnectionAction(name, description, connectionField, filterTypeName, selection, includeSubTeamsDescription) {
139
+ rl.registerAction(name, {
140
+ description,
141
+ inputSchema: {
142
+ viewId: { type: "string", required: true, description: "The custom view ID or slug" },
143
+ ...LIST_INPUT_SCHEMA,
144
+ ...(includeSubTeamsDescription
145
+ ? { includeSubTeams: { type: "boolean", required: false, description: includeSubTeamsDescription } }
146
+ : {}),
147
+ },
148
+ async execute(input, ctx) {
149
+ const opts = (input ?? {});
150
+ const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
151
+ const declParts = ["$id: String!", argsDecl.slice(1, -1)];
152
+ const callParts = [argsCall.slice(1, -1)];
153
+ const includeSubTeamsSet = includeSubTeamsDescription !== undefined && opts.includeSubTeams !== undefined;
154
+ if (includeSubTeamsSet) {
155
+ declParts.push("$includeSubTeams: Boolean");
156
+ callParts.push("includeSubTeams: $includeSubTeams");
157
+ vars.includeSubTeams = opts.includeSubTeams;
158
+ }
159
+ const data = await gql(key(ctx), `query(${declParts.join(", ")}) {
160
+ customView(id: $id) { ${connectionField}(${callParts.join(", ")}) { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }
161
+ }`, { id: opts.viewId, ...vars });
162
+ const conn = (data.customView?.[connectionField] ?? {});
163
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
164
+ },
165
+ });
166
+ }
130
167
  // =========================================================
131
168
  // Issues
132
169
  // =========================================================
@@ -712,6 +749,70 @@ export default function linear(rl) {
712
749
  },
713
750
  });
714
751
  // =========================================================
752
+ // Custom Views
753
+ // =========================================================
754
+ listAction("view.list", "List custom views accessible to the user, including personal and shared workspace views. Linear excludes views scoped to a specific project or initiative from this root query.", "customViews", "CustomViewFilter", CUSTOM_VIEW_FIELDS);
755
+ getAction("view.get", "Get a custom view by ID or slug.", "customView", CUSTOM_VIEW_FIELDS);
756
+ rl.registerAction("view.create", {
757
+ description: "Create a custom view. Set filterData for issue views; projectFilterData, initiativeFilterData, or feedItemFilterData for other view types.",
758
+ inputSchema: {
759
+ name: { type: "string", required: true, description: "The name of the custom view" },
760
+ description: { type: "string", required: false, description: "The description of the custom view" },
761
+ icon: { type: "string", required: false, description: "The icon of the custom view" },
762
+ color: { type: "string", required: false, description: "The color of the custom view icon (hex)" },
763
+ shared: { type: "boolean", required: false, description: "Whether the custom view is shared with everyone in the workspace" },
764
+ filterData: { type: "object", required: false, description: "IssueFilter for issue views" },
765
+ projectFilterData: { type: "object", required: false, description: "ProjectFilter for project views" },
766
+ initiativeFilterData: { type: "object", required: false, description: "InitiativeFilter for initiative views" },
767
+ feedItemFilterData: { type: "object", required: false, description: "FeedItemFilter for update/feed item views" },
768
+ teamId: { type: "string", required: false, description: "The team associated with the custom view" },
769
+ projectId: { type: "string", required: false, description: "The project associated with the custom view" },
770
+ initiativeId: { type: "string", required: false, description: "The initiative associated with the custom view" },
771
+ ownerId: { type: "string", required: false, description: "The owner of the custom view" },
772
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
773
+ },
774
+ async execute(input, ctx) {
775
+ const data = await gql(key(ctx), `mutation($input: CustomViewCreateInput!) { customViewCreate(input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { input: input });
776
+ return data.customViewCreate?.customView;
777
+ },
778
+ });
779
+ rl.registerAction("view.update", {
780
+ description: "Update a custom view. All fields optional; only provided fields are updated.",
781
+ inputSchema: {
782
+ id: { type: "string", required: true, description: "The identifier of the custom view to update" },
783
+ name: { type: "string", required: false, description: "The name of the custom view" },
784
+ description: { type: "string", required: false, description: "The description of the custom view" },
785
+ icon: { type: "string", required: false, description: "The icon of the custom view" },
786
+ color: { type: "string", required: false, description: "The color of the custom view icon (hex)" },
787
+ shared: { type: "boolean", required: false, description: "Whether the custom view is shared with everyone in the workspace" },
788
+ filterData: { type: "object", required: false, description: "IssueFilter for issue views" },
789
+ projectFilterData: { type: "object", required: false, description: "ProjectFilter for project views" },
790
+ initiativeFilterData: { type: "object", required: false, description: "InitiativeFilter for initiative views" },
791
+ feedItemFilterData: { type: "object", required: false, description: "FeedItemFilter for update/feed item views" },
792
+ teamId: { type: "string", required: false, description: "The team associated with the custom view" },
793
+ projectId: { type: "string", required: false, description: "The project associated with the custom view" },
794
+ initiativeId: { type: "string", required: false, description: "The initiative associated with the custom view" },
795
+ ownerId: { type: "string", required: false, description: "The owner of the custom view" },
796
+ },
797
+ async execute(input, ctx) {
798
+ const { id, ...fields } = input;
799
+ const data = await gql(key(ctx), `mutation($id: String!, $input: CustomViewUpdateInput!) { customViewUpdate(id: $id, input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { id, input: fields });
800
+ return data.customViewUpdate?.customView;
801
+ },
802
+ });
803
+ rl.registerAction("view.delete", {
804
+ description: "Delete a custom view.",
805
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the custom view to delete" } },
806
+ async execute(input, ctx) {
807
+ const data = await gql(key(ctx), `mutation($id: String!) { customViewDelete(id: $id) { success } }`, { id: input.id });
808
+ return data.customViewDelete;
809
+ },
810
+ });
811
+ customViewConnectionAction("view.issues", "List issues matching a custom view's issue filter. Returns an empty connection when the view's modelName is not Issue.", "issues", "IssueFilter", ISSUE_LITE, "Include issues from sub-teams when the custom view is associated with a team");
812
+ customViewConnectionAction("view.projects", "List projects matching a custom view's project filter. Returns an empty connection when the view's modelName is not Project.", "projects", "ProjectFilter", PROJECT_FIELDS, "Include projects from sub-teams when the custom view is associated with a team");
813
+ customViewConnectionAction("view.initiatives", "List initiatives matching a custom view's initiative filter. Returns an empty connection when the view's modelName is not Initiative.", "initiatives", "InitiativeFilter", INITIATIVE_FIELDS);
814
+ customViewConnectionAction("view.updates", "List feed items matching a custom view's feed item filter. Returns an empty connection when the view's modelName is not FeedItem.", "updates", "FeedItemFilter", FEED_ITEM_FIELDS, "Include updates from sub-teams when the custom view is associated with a team");
815
+ // =========================================================
715
816
  // Cycles
716
817
  // =========================================================
717
818
  listAction("cycle.list", "List cycles. Use filter for isActive/isNext/isPrevious.", "cycles", "CycleFilter", CYCLE_FIELDS);
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 sandbox. */
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
@@ -4,6 +4,7 @@ import { DEFAULT_CONFIG } from "./config/types.js";
4
4
  import { ExecutionEngine } from "./core/engine.js";
5
5
  import { resolvePluginExport } from "./plugin/api.js";
6
6
  import { discoverPlugins } from "./plugin/loader.js";
7
+ import { registerNodePlugin } from "./plugin/node-plugin.js";
7
8
  import { PluginRegistry } from "./plugin/registry.js";
8
9
  export class Runline {
9
10
  _registry;
@@ -14,6 +15,7 @@ export class Runline {
14
15
  const plugin = resolvePluginExport(pluginOrFn, "unknown");
15
16
  this._registry.register(plugin);
16
17
  }
18
+ registerNodePlugin(this._registry);
17
19
  this._config = {
18
20
  connections: options.connections ?? [],
19
21
  timeoutMs: options.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
@@ -23,7 +25,7 @@ export class Runline {
23
25
  static create(options = {}) {
24
26
  return new Runline(options);
25
27
  }
26
- /** Execute JavaScript code in the sandbox. */
28
+ /** Execute JavaScript code in the QuickJS runtime. */
27
29
  async execute(code) {
28
30
  const engine = new ExecutionEngine(this._registry, this._config);
29
31
  return engine.execute(code);
@@ -32,6 +34,7 @@ export class Runline {
32
34
  addPlugin(pluginOrFn, connections) {
33
35
  const plugin = resolvePluginExport(pluginOrFn, "unknown");
34
36
  this._registry.register(plugin);
37
+ registerNodePlugin(this._registry);
35
38
  if (connections) {
36
39
  this._config = {
37
40
  ...this._config,
@@ -101,6 +104,7 @@ export class Runline {
101
104
  for (const plugin of plugins) {
102
105
  rl._registry.register(plugin);
103
106
  }
107
+ registerNodePlugin(rl._registry);
104
108
  return rl;
105
109
  }
106
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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.3.14",
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",