@repogo/plugins 0.1.41 → 0.1.43
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 +61 -47
- package/dist/context.d.ts +8 -4
- package/dist/define.d.ts +24 -16
- package/dist/generated.d.ts +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -3
- package/dist/runtime.d.ts +9 -6
- package/dist/testing.js +57 -47
- package/dist/workflow-executor.js +62 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ phone draw its data live and run its methods when you tap.
|
|
|
7
7
|
|
|
8
8
|
- [Quick start](#quick-start)
|
|
9
9
|
- [Where plugins live](#where-plugins-live)
|
|
10
|
-
- [Methods](#methods):
|
|
10
|
+
- [Methods](#methods): reads, writes, destructive methods, streams, cached reads
|
|
11
11
|
- [Data](#data): namespaces, collections, kv, streams, buckets, uploads
|
|
12
12
|
- [Workflows](#workflows)
|
|
13
13
|
- [Crons](#crons)
|
|
@@ -21,22 +21,22 @@ phone draw its data live and run its methods when you tap.
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
//
|
|
24
|
+
// ~/.repogo/plugins/notes/app.ts
|
|
25
25
|
import { definePlugin, method, z } from "@repogo/plugins";
|
|
26
26
|
|
|
27
27
|
export default definePlugin({
|
|
28
|
-
description: "Notes for
|
|
29
|
-
namespace: "
|
|
28
|
+
description: "Notes for any project",
|
|
29
|
+
namespace: "notes",
|
|
30
30
|
collections: { notes: z.object({ text: z.string(), done: z.boolean().default(false) }) },
|
|
31
31
|
methods: {
|
|
32
32
|
open: method(z.object({}), async (_, ctx) => ctx.collections.notes.list({ where: { done: false } }), {
|
|
33
|
-
|
|
33
|
+
effect: "read",
|
|
34
34
|
description: "Notes not done yet, newest first",
|
|
35
35
|
}),
|
|
36
36
|
clear: method(z.object({}), async (_, ctx) => {
|
|
37
37
|
for (const note of await ctx.collections.notes.list({ limit: 500 })) await ctx.collections.notes.delete(note.id);
|
|
38
38
|
return null;
|
|
39
|
-
}, { confirm: "Delete every note?" }),
|
|
39
|
+
}, { effect: "destructive", confirm: "Delete every note?" }),
|
|
40
40
|
},
|
|
41
41
|
});
|
|
42
42
|
```
|
|
@@ -46,9 +46,9 @@ a minute; at once when something calls it), and it gets:
|
|
|
46
46
|
|
|
47
47
|
- `notes.open` and `notes.clear`, the methods it wrote;
|
|
48
48
|
- `notes.notes.list`, `.get`, `.create`, `.update` and `.delete`, generated for its collection;
|
|
49
|
-
- a `data.db` in `~/.repogo/plugin-data/
|
|
49
|
+
- a `data.db` in `~/.repogo/plugin-data/notes/`.
|
|
50
50
|
|
|
51
|
-
Add the package to
|
|
51
|
+
Add the package to `~/.repogo/plugins/package.json` (`bun add @repogo/plugins`) for
|
|
52
52
|
types and editor help. The host runs plugins with its own copy of this package,
|
|
53
53
|
the version it shipped with, so a plugin always matches its host. Plugins run on
|
|
54
54
|
[Bun](https://bun.sh); install it on the environment.
|
|
@@ -57,30 +57,39 @@ the version it shipped with, so a plugin always matches its host. Plugins run on
|
|
|
57
57
|
|
|
58
58
|
| Folder | For | `ctx.root` |
|
|
59
59
|
| --- | --- | --- |
|
|
60
|
-
|
|
|
61
|
-
| `~/.repogo/plugins/<name>/app.ts` | Every project (Vercel, Gmail, builds) | The project that called it |
|
|
60
|
+
| `~/.repogo/plugins/<name>/app.ts` | Every project | The project that called it |
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
Plugins live only on the environment, in folders the user put there. A project
|
|
63
|
+
can't ship plugin code: the host never imports a repo's `.repogo/plugins`, so
|
|
64
|
+
cloning or opening a project never runs anything in it. A plugin about one
|
|
65
|
+
project (its keys, its database, its plans) is still an environment plugin: it
|
|
66
|
+
reads that project from `ctx.root`, and says so instead of acting when it's
|
|
67
|
+
called from a project it doesn't fit.
|
|
68
|
+
|
|
69
|
+
A project ships no pages either: the app loads `.ui` and `.html` pages only
|
|
70
|
+
from `~/.repogo/plugins`, so a cloned repo can't put its own page, and the
|
|
71
|
+
script an `.html` page runs, in front of the user. A page about one project
|
|
72
|
+
lives beside its plugin and takes the project as state (`?project=…`).
|
|
73
|
+
|
|
74
|
+
A plugin's name is its folder's: lowercase letters, digits and hyphens. The
|
|
75
|
+
environment's plugins run together in one Worker, so a plugin's blocking code
|
|
76
|
+
stalls the others: use async APIs.
|
|
67
77
|
|
|
68
78
|
Pages beside an environment plugin (`pages/*.ui`, or `pages/*.md`) open from
|
|
69
79
|
any project as `~/.repogo/plugins/<name>/pages/<page>.ui`. A
|
|
70
80
|
`.ui` page is card markup with no fence; a `.md` page is writing with cards.
|
|
71
81
|
|
|
72
|
-
In the full iOS app, `~/.repogo/plugins/plugin.ui`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Both are plain card markup without a code fence. Read and preserve existing
|
|
82
|
+
In the full iOS app, `~/.repogo/plugins/plugin.ui` is the Plugins tab in
|
|
83
|
+
every project; a project's `.repogo/plugin.ui` is never read. If it doesn't
|
|
84
|
+
exist, the tab is absent. It is plain card markup without a code fence. Read and preserve existing
|
|
76
85
|
entries, and link to plugin pages with `page:` actions.
|
|
77
86
|
|
|
78
|
-
A user-facing plugin needs both its saved page and a link from
|
|
87
|
+
A user-facing plugin needs both its saved page and a link from `~/.repogo/plugins/plugin.ui`. Saving a file in a pages folder or naming it `index.ui` does not add it to the tab. Plugin discovery can list a page without a navigation entry; verify `plugin.ui` links to the existing page.
|
|
79
88
|
|
|
80
89
|
## Methods
|
|
81
90
|
|
|
82
91
|
```ts
|
|
83
|
-
method(input, run, {
|
|
92
|
+
method(input, run, { effect?, description?, output?, confirm? })
|
|
84
93
|
stream(input, async function* run, { description? })
|
|
85
94
|
```
|
|
86
95
|
|
|
@@ -88,14 +97,18 @@ stream(input, async function* run, { description? })
|
|
|
88
97
|
filled in before `run` sees it. `run(input, ctx)` returns the result: anything
|
|
89
98
|
JSON can carry.
|
|
90
99
|
|
|
91
|
-
|
|
|
100
|
+
| Effect | Does | Who runs it |
|
|
92
101
|
| --- | --- | --- |
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
| `read` | Only reads; writing throws | Cards on their own, and again whenever data it read changes |
|
|
103
|
+
| `write` (the default) | Reads and writes | Only when someone acts: a tap, an agent, a cron, other code |
|
|
104
|
+
| `destructive` | Writes what can't be undone; needs a `confirm` | Only after the user answers its question, on a tap |
|
|
105
|
+
| `stream()` | A read that yields items as they happen | A card while it shows it |
|
|
106
|
+
|
|
107
|
+
Mark every method that only reads `effect: "read"`: that's what lets a card
|
|
108
|
+
show it and keep it current. Mark what can't be undone (a delete, a send, a
|
|
109
|
+
deploy) `effect: "destructive"` with a `confirm` question; a destructive method
|
|
110
|
+
without one doesn't load. A method with a `confirm` never runs from an agent's
|
|
111
|
+
`plugin_call` or a cron: the agent draws a button, and the user's tap asks. Give each a `description`; agents read it to choose
|
|
99
112
|
a method. `output`, a zod schema of the result, tells agents and cards its
|
|
100
113
|
field names.
|
|
101
114
|
|
|
@@ -108,7 +121,7 @@ or the voice agent reads the whole of one.
|
|
|
108
121
|
```ts
|
|
109
122
|
open: method(z.object({ limit: z.number().int().min(1).max(100).default(20) }), async ({ limit }, ctx) =>
|
|
110
123
|
(await ctx.collections.notes.list({ where: { done: false }, limit })).map(({ id, text }) => ({ id, text: text.slice(0, 200) })),
|
|
111
|
-
{
|
|
124
|
+
{ effect: "read", description: "Notes not done yet, newest first" }),
|
|
112
125
|
```
|
|
113
126
|
|
|
114
127
|
A **stream** sends each `yield` to the card as it happens, for as long as the
|
|
@@ -142,7 +155,7 @@ projects: cached(z.object({ limit: z.number().int().min(1).max(200).default(100)
|
|
|
142
155
|
|
|
143
156
|
- Answers are kept per method, input and project (`ctx.root`). `shared: true`
|
|
144
157
|
keeps one for every project, for answers that don't depend on it.
|
|
145
|
-
- `run` is read-only, like
|
|
158
|
+
- `run` is read-only, like any read's, and has `timeout` (a minute unless it says).
|
|
146
159
|
- A background check that fails keeps the saved answer, goes to the plugin's
|
|
147
160
|
logs, and is tried again after another `fresh`.
|
|
148
161
|
- Answers live in the namespace's `data.db`, so a plugin with a cached method
|
|
@@ -193,7 +206,7 @@ await ctx.collections.tasks.delete(task.id);
|
|
|
193
206
|
await ctx.collections.tasks.deleteMany({ where: { status: "done" }, offset: 100, limit: 500 }); // done ones past the newest 100, 500 at a time
|
|
194
207
|
```
|
|
195
208
|
|
|
196
|
-
`list` is newest first by default, at most 100 unless `limit` says (up to 500).
|
|
209
|
+
`list` is newest first by default, at most 100 unless `limit` says (the generated `list` takes up to 500).
|
|
197
210
|
`where` matches top-level fields exactly; `null` matches a missing one.
|
|
198
211
|
`orderBy` is `id`, `createdAt`, `updatedAt` or one of the record's own fields, and
|
|
199
212
|
`offset` passes over that many first. `deleteMany` takes the same options and
|
|
@@ -480,7 +493,7 @@ For integration tests, import `testWorkflowRuntime` from
|
|
|
480
493
|
`@repogo/plugins/testing` and pass the fixture's plugins directory. It uses real
|
|
481
494
|
temporary storage and a subprocess per run, as the host does. Call the same lifecycle methods,
|
|
482
495
|
use `await app.wait("reports.report", id)` to await a terminal/blocked run, and
|
|
483
|
-
`await app.close()` afterward. The
|
|
496
|
+
`await app.close()` afterward. The plain `testRuntime({ plugin })` helper
|
|
484
497
|
does not execute durable workflows.
|
|
485
498
|
|
|
486
499
|
|
|
@@ -501,7 +514,7 @@ itself. Each line names a plugin, one of its methods and a schedule:
|
|
|
501
514
|
| Field | What |
|
|
502
515
|
| --- | --- |
|
|
503
516
|
| `plugin`, `method` | The plugin's folder name and one of its methods, called with `{}` as a tap would |
|
|
504
|
-
| `project` | The project
|
|
517
|
+
| `project` | The project the run gets as `ctx.root`, an absolute path or one starting `~/`; left out, the home folder. The plugin is always the environment's |
|
|
505
518
|
| `schedule` | A cron: five fields (minute hour day-of-month month day-of-week), or six with seconds first (`*/5 * * * * *`); each `*`, a number, a range, a list or a step |
|
|
506
519
|
| `timezone` | Defaults to the environment's |
|
|
507
520
|
| `enabled` | `false` keeps the line without running it |
|
|
@@ -520,9 +533,9 @@ isn't valid JSON pauses every line until it's fixed. Each line also shows
|
|
|
520
533
|
has the same for a plugin's own, and a query reading them re-runs around each run.
|
|
521
534
|
|
|
522
535
|
Schedule something only when the user asks for it. A cron's `ctx.root` is its
|
|
523
|
-
project, or the home folder
|
|
524
|
-
|
|
525
|
-
|
|
536
|
+
line's project, or the home folder without one. Plugins share a thread: code
|
|
537
|
+
that blocks it holds up every other cron, and the host's log names the crons
|
|
538
|
+
that were running when it happened.
|
|
526
539
|
|
|
527
540
|
## Logging
|
|
528
541
|
|
|
@@ -530,7 +543,7 @@ Write `console.log` (or `warn`, `error`, `debug`); nothing else is needed. The
|
|
|
530
543
|
host keeps every line with the plugin, the method and the call that wrote it,
|
|
531
544
|
who called (`card`, `agent`, `cron`), and the chat for an agent's call, plus
|
|
532
545
|
the arguments as JSON. An error a method throws, or one nothing caught, is
|
|
533
|
-
kept too, with its stack. Read them with the agent's `
|
|
546
|
+
kept too, with its stack. Read them with the agent's `plugins_logs` tool, or on
|
|
534
547
|
a card with `<Data method="repogo:plugins.logs">`, which reloads as lines land:
|
|
535
548
|
`plugin`, `level` (that level and worse), `method`, `since`, `until`, `query`
|
|
536
549
|
(a substring) and `limit` narrow it. The `plugins` tool shows each plugin's
|
|
@@ -593,7 +606,7 @@ ends it with that state, left up for 15 minutes:
|
|
|
593
606
|
```ts
|
|
594
607
|
deploy: method(z.object({ project: z.string() }), async ({ project }, ctx) => {
|
|
595
608
|
const steps = ["Build", "Test", "Deploy", "Verify"];
|
|
596
|
-
const page =
|
|
609
|
+
const page = `~/.repogo/plugins/deploys/pages/index.ui?project=${project}`;
|
|
597
610
|
const show = (current: number, body: string) =>
|
|
598
611
|
ctx.activity({ id: `deploy:${project}`, title: `Deploying ${project}`, body, page,
|
|
599
612
|
symbol: "shippingbox", progress: { kind: "steps", steps, current } });
|
|
@@ -633,10 +646,10 @@ or CLI of its own. It answers the tool's structured content, else its text
|
|
|
633
646
|
customers: method(z.object({}), async (_, ctx) => {
|
|
634
647
|
const list = await ctx.mcp("stripe", "stripe_api_read", { stripe_api_operation_id: "GetCustomers", /* … */ });
|
|
635
648
|
return list.data.map((c) => ({ id: c.id, email: c.email }));
|
|
636
|
-
}, {
|
|
649
|
+
}, { effect: "read" }),
|
|
637
650
|
```
|
|
638
651
|
|
|
639
|
-
A
|
|
652
|
+
A read may run only the server's read-only tools, as a card's `Data` may. A
|
|
640
653
|
tool marked destructive runs only from a method with a `confirm`, or one
|
|
641
654
|
called from it with `ctx.call`: the user's answer to that question is its
|
|
642
655
|
yes. A refusal throws `FORBIDDEN`; a tool's own error throws `INTERNAL` with
|
|
@@ -667,17 +680,17 @@ Events run in memory: none is retried, and a handler that throws is logged.
|
|
|
667
680
|
## How cards and agents reach a plugin
|
|
668
681
|
|
|
669
682
|
**Agents** use two tools. `plugins` lists every plugin with a line per method
|
|
670
|
-
(its
|
|
683
|
+
(its effect, whether it asks first, and its input's fields, optional ones ending
|
|
671
684
|
in `?`), and every line of `~/.repogo/crons.json` with how it's going.
|
|
672
685
|
`plugins` with `plugin` set answers that plugin in full: each method's input
|
|
673
686
|
and output JSON Schema and confirm, its `storage` (each bucket's and stream's
|
|
674
687
|
folder), the pages that show its methods, and the cron lines naming it.
|
|
675
688
|
`plugin_call` runs one. Inspect a method's schema before naming it in a card.
|
|
676
|
-
Verify
|
|
689
|
+
Verify a read's fields with a read-only call. Do not execute writes just to
|
|
677
690
|
check a card; run them only when the user's task calls for that action.
|
|
678
691
|
|
|
679
692
|
**Cards** name methods as `plugin:<plugin>.<method>`. A `Data` element runs a
|
|
680
|
-
|
|
693
|
+
read, or follows a stream, and redraws when its data changes. `Button`,
|
|
681
694
|
`Item` and `Swipe` actions call methods; on iOS, `Toggle` can call one on a
|
|
682
695
|
user flip. `Upload` calls a plugin method with staged files. Params must match
|
|
683
696
|
the discovered input schema. These fragments assume `q` and `note` are supplied
|
|
@@ -701,10 +714,10 @@ by the enclosing card:
|
|
|
701
714
|
### Saving from a switch
|
|
702
715
|
|
|
703
716
|
Prefer SDK storage for plugin-owned data. A storage declaration generates
|
|
704
|
-
methods; ordinary saves do not need a custom
|
|
717
|
+
methods; ordinary saves do not need a custom write method. For example:
|
|
705
718
|
|
|
706
719
|
```ts
|
|
707
|
-
//
|
|
720
|
+
// ~/.repogo/plugins/preferences/app.ts
|
|
708
721
|
import { definePlugin, z } from "@repogo/plugins";
|
|
709
722
|
|
|
710
723
|
export default definePlugin({
|
|
@@ -746,9 +759,10 @@ schema answers `VALIDATION` with a message per field. Anything else thrown is
|
|
|
746
759
|
`INTERNAL` with its message.
|
|
747
760
|
|
|
748
761
|
A method's `confirm` is asked before a card's tap runs it. An agent's
|
|
749
|
-
`plugin_call`
|
|
750
|
-
|
|
751
|
-
it; an unattended workflow refuses confirmation-gated methods.
|
|
762
|
+
`plugin_call` never runs it: it gets the question back as a `CONFIRM` error and
|
|
763
|
+
draws a button, and the user's tap asks. Ordinary `ctx.call` from plugin code skips
|
|
764
|
+
it; an unattended workflow refuses confirmation-gated methods. A `destructive`
|
|
765
|
+
method must have one. `{{field}}` in the question is
|
|
752
766
|
filled from the call's input, so it can say what it acts on:
|
|
753
767
|
`confirm: "Restart {{machine}}?"`.
|
|
754
768
|
|
package/dist/context.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Bucket } from "./buckets";
|
|
|
2
2
|
import type { Collection } from "./collections";
|
|
3
3
|
import type { Kv } from "./kv";
|
|
4
4
|
import type { Stream } from "./streams";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Effect, HTTPAccess } from "./define";
|
|
6
6
|
import type { Duration } from "./duration";
|
|
7
7
|
/** An alert for the user's phones: what it says, and the page a tap opens. */
|
|
8
8
|
export type Notice = {
|
|
@@ -74,7 +74,9 @@ export type Activity = {
|
|
|
74
74
|
export type ManifestMethod = {
|
|
75
75
|
/** `<plugin>.<method>`, the name `ctx.call` takes. */
|
|
76
76
|
name: string;
|
|
77
|
-
|
|
77
|
+
effect: Effect;
|
|
78
|
+
/** A read that sends items as they happen. */
|
|
79
|
+
stream: boolean;
|
|
78
80
|
description: string | null;
|
|
79
81
|
/** What a card asks before running it; null runs on tap. */
|
|
80
82
|
confirm: string | null;
|
|
@@ -113,13 +115,15 @@ export type ManifestStorage = {
|
|
|
113
115
|
};
|
|
114
116
|
/**
|
|
115
117
|
* One schedule the host gives a Worker, from a line of `~/.repogo/crons.json`: call
|
|
116
|
-
* `<plugin>.<method>` with `{}` when `schedule` comes due in `timezone` (the environment's)
|
|
118
|
+
* `<plugin>.<method>` with `{}` when `schedule` comes due in `timezone` (the environment's),
|
|
119
|
+
* with `root` (the line's project) as `ctx.root`, else the Worker's own.
|
|
117
120
|
*/
|
|
118
121
|
export type CronEntry = {
|
|
119
122
|
plugin: string;
|
|
120
123
|
method: string;
|
|
121
124
|
schedule: string;
|
|
122
125
|
timezone?: string;
|
|
126
|
+
root?: string;
|
|
123
127
|
};
|
|
124
128
|
/** A schedule and how it's going; `error` says why it couldn't be set, and then nothing runs. */
|
|
125
129
|
export type CronStatus = ManifestCron & {
|
|
@@ -181,7 +185,7 @@ export type Context = {
|
|
|
181
185
|
* environment's), with their methods: what `call` can reach.
|
|
182
186
|
*/
|
|
183
187
|
plugins: () => Manifest["plugins"];
|
|
184
|
-
/** Aborts when a stream's last watcher leaves; a
|
|
188
|
+
/** Aborts when a stream's last watcher leaves; a read or write's never does. */
|
|
185
189
|
signal: AbortSignal;
|
|
186
190
|
/** Sends an alert to every phone paired with this environment. */
|
|
187
191
|
notify: (notice: Notice) => void;
|
package/dist/define.d.ts
CHANGED
|
@@ -11,26 +11,28 @@ export type StreamDefinition = z.ZodType | {
|
|
|
11
11
|
retain: Retain;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* changes.
|
|
16
|
-
*
|
|
17
|
-
* Unmarked methods
|
|
14
|
+
* What a method does. `read` only reads, so cards may run it on their own and
|
|
15
|
+
* again when its data changes. `write` changes data, and runs only when the
|
|
16
|
+
* user acts. `destructive` can't be undone, and must ask first (`confirm`).
|
|
17
|
+
* Unmarked methods write: repeating one is never assumed safe.
|
|
18
18
|
*/
|
|
19
|
-
export type
|
|
19
|
+
export type Effect = "read" | "write" | "destructive";
|
|
20
20
|
export type Method<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> = {
|
|
21
21
|
input: I;
|
|
22
22
|
/** What it returns; the agent and cards read field names from it. */
|
|
23
23
|
output?: O;
|
|
24
|
-
|
|
24
|
+
effect: Effect;
|
|
25
|
+
/** Set by `stream`: a read that sends items as they happen. */
|
|
26
|
+
stream?: true;
|
|
25
27
|
description?: string;
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* The question asked before it runs, such as "Delete {{name}}?", filled from
|
|
30
|
+
* its input; required for a destructive method. Methods without one run on tap.
|
|
29
31
|
*/
|
|
30
32
|
confirm?: string;
|
|
31
33
|
/** Who may call it at the environment's own address; unset, no one. */
|
|
32
34
|
http?: HTTPAccess;
|
|
33
|
-
/** Set by `cached`: the
|
|
35
|
+
/** Set by `cached`: the read answers from what it saved, and checks again when that's old. */
|
|
34
36
|
cache?: CacheOptions;
|
|
35
37
|
run: (input: z.infer<I>, ctx: Context) => unknown | Promise<unknown>;
|
|
36
38
|
};
|
|
@@ -67,22 +69,27 @@ export type PluginDefinition = {
|
|
|
67
69
|
private?: string[];
|
|
68
70
|
};
|
|
69
71
|
export declare function definePlugin<T extends PluginDefinition>(plugin: T): T;
|
|
70
|
-
|
|
71
|
-
kind?: MethodKind;
|
|
72
|
+
type MethodOptions<O extends z.ZodType> = {
|
|
72
73
|
output?: O;
|
|
73
74
|
description?: string;
|
|
74
|
-
confirm?: string;
|
|
75
75
|
http?: true | {
|
|
76
76
|
auth: HTTPAccess;
|
|
77
77
|
};
|
|
78
|
-
}
|
|
78
|
+
} & ({
|
|
79
|
+
effect: "destructive";
|
|
80
|
+
confirm: string;
|
|
81
|
+
} | {
|
|
82
|
+
effect?: "read" | "write";
|
|
83
|
+
confirm?: string;
|
|
84
|
+
});
|
|
85
|
+
export declare function method<I extends z.ZodType, O extends z.ZodType = z.ZodType>(input: I, run: (input: z.infer<I>, ctx: Context) => unknown | Promise<unknown>, options?: MethodOptions<O>): Method<I, O>;
|
|
79
86
|
/**
|
|
80
|
-
* A
|
|
87
|
+
* A read that answers at once from what it saved last time, for answers that
|
|
81
88
|
* are slow to get: a CLI's list, another service's API. When the saved answer
|
|
82
89
|
* is older than `fresh`, `run` goes again in the background, and cards showing
|
|
83
90
|
* it redraw only if the answer changed. With nothing saved, the first call
|
|
84
91
|
* waits for `run`. Each project keeps its own answers unless `shared`.
|
|
85
|
-
* `run` is read-only, like
|
|
92
|
+
* `run` is read-only, like any read's; it has `timeout` (a minute unless it says).
|
|
86
93
|
*/
|
|
87
94
|
export declare function cached<I extends z.ZodType, O extends z.ZodType = z.ZodType>(input: I, run: (input: z.infer<I>, ctx: Context) => unknown | Promise<unknown>, options: {
|
|
88
95
|
fresh: Duration;
|
|
@@ -95,8 +102,9 @@ export declare function cached<I extends z.ZodType, O extends z.ZodType = z.ZodT
|
|
|
95
102
|
* A method that sends items as they happen: a log tail, a build's output. Each
|
|
96
103
|
* `yield` is one item; a card shows them as a list that grows. It runs while
|
|
97
104
|
* someone watches and `ctx.signal` aborts when the last one leaves, so pass it
|
|
98
|
-
* on (`Bun.spawn(cmd, { signal: ctx.signal })`). Like
|
|
105
|
+
* on (`Bun.spawn(cmd, { signal: ctx.signal })`). Like any read, it may not write.
|
|
99
106
|
*/
|
|
100
107
|
export declare function stream<I extends z.ZodType>(input: I, run: (input: z.infer<I>, ctx: Context) => AsyncIterable<unknown>, options?: {
|
|
101
108
|
description?: string;
|
|
102
109
|
}): Method<I>;
|
|
110
|
+
export {};
|
package/dist/generated.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Bucket } from "./buckets";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Effect } from "./define";
|
|
4
4
|
import type { Kv } from "./kv";
|
|
5
5
|
import type { Stream } from "./streams";
|
|
6
6
|
export type Generated = {
|
|
7
|
-
|
|
7
|
+
effect: Effect;
|
|
8
|
+
stream?: true;
|
|
8
9
|
description: string;
|
|
9
10
|
input: z.ZodType;
|
|
10
11
|
output?: z.ZodType;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { cached, definePlugin, method, stream } from "./define";
|
|
2
2
|
export { upload, type UploadedFile } from "./uploads";
|
|
3
|
-
export type { PluginDefinition, Method,
|
|
3
|
+
export type { PluginDefinition, Method, Effect, HTTPAccess, StreamDefinition } from "./define";
|
|
4
4
|
export type { Activity, ActivityProgress, ActivityTimer, ActivityTint, AgentOptions, AgentReply, Caller, Context, CronEntry, CronStatus, Manifest, ManifestCron, ManifestMethod, ManifestStorage } from "./context";
|
|
5
5
|
export type { LogLevel, LogRecord } from "./logs";
|
|
6
6
|
export type { BucketFile, FileStatus, PutFile } from "./buckets";
|
package/dist/index.js
CHANGED
|
@@ -47,10 +47,12 @@ function definePlugin(plugin) {
|
|
|
47
47
|
return plugin;
|
|
48
48
|
}
|
|
49
49
|
function method(input, run, options = {}) {
|
|
50
|
+
if ("kind" in options)
|
|
51
|
+
throw new Error('method: kind is now effect: "read", "write" or "destructive"');
|
|
50
52
|
return {
|
|
51
53
|
input,
|
|
52
54
|
run,
|
|
53
|
-
|
|
55
|
+
effect: options.effect ?? "write",
|
|
54
56
|
output: options.output,
|
|
55
57
|
description: options.description,
|
|
56
58
|
confirm: options.confirm,
|
|
@@ -61,14 +63,14 @@ function cached(input, run, options) {
|
|
|
61
63
|
return {
|
|
62
64
|
input,
|
|
63
65
|
run,
|
|
64
|
-
|
|
66
|
+
effect: "read",
|
|
65
67
|
output: options.output,
|
|
66
68
|
description: options.description,
|
|
67
69
|
cache: { fresh: parseDuration(options.fresh), shared: options.shared ?? false, timeout: parseDuration(options.timeout ?? "1m") }
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
function stream(input, run, options = {}) {
|
|
71
|
-
return { input, run,
|
|
73
|
+
return { input, run, effect: "read", stream: true, description: options.description };
|
|
72
74
|
}
|
|
73
75
|
// src/uploads.ts
|
|
74
76
|
import { AsyncLocalStorage } from "async_hooks";
|
package/dist/runtime.d.ts
CHANGED
|
@@ -2,14 +2,15 @@ import { z } from "zod";
|
|
|
2
2
|
import { type Schedule } from "./cron";
|
|
3
3
|
import type { Caller, CronEntry, CronStatus, Manifest, Notice } from "./context";
|
|
4
4
|
export type { Manifest, ManifestCron, ManifestMethod } from "./context";
|
|
5
|
-
import type { HTTPAccess, PluginDefinition
|
|
5
|
+
import type { Effect, HTTPAccess, PluginDefinition } from "./define";
|
|
6
6
|
import { EventBus } from "./events";
|
|
7
7
|
import type { WorkflowBridge } from "./workflow/contracts";
|
|
8
8
|
import { type UploadedFile } from "./uploads";
|
|
9
9
|
export type RegisteredMethod = {
|
|
10
10
|
name: string;
|
|
11
11
|
plugin: string;
|
|
12
|
-
|
|
12
|
+
effect: Effect;
|
|
13
|
+
stream?: true;
|
|
13
14
|
description?: string;
|
|
14
15
|
confirm?: string;
|
|
15
16
|
http?: HTTPAccess;
|
|
@@ -35,9 +36,11 @@ type NoticeListener = (notice: SentNotice) => void;
|
|
|
35
36
|
export type HostCaller = (plugin: string, method: string, params: unknown) => Promise<unknown>;
|
|
36
37
|
/** A schedule as the runtime keeps it: read once, with its last run. */
|
|
37
38
|
type CronJob = {
|
|
38
|
-
/** The plugin, method, schedule and
|
|
39
|
+
/** The plugin, method, schedule, zone and root: an entry the host sends again keeps its job, and its state. */
|
|
39
40
|
signature: string;
|
|
40
41
|
plugin: string;
|
|
42
|
+
/** The project the run gets as `ctx.root`; none keeps the Worker's own. */
|
|
43
|
+
root?: string;
|
|
41
44
|
/** `<plugin>.<method>`. */
|
|
42
45
|
method: string;
|
|
43
46
|
schedule: string;
|
|
@@ -98,9 +101,9 @@ export declare class Runtime {
|
|
|
98
101
|
caller: Caller;
|
|
99
102
|
}): Promise<unknown>;
|
|
100
103
|
/**
|
|
101
|
-
* Calls a method and reports what it read and wrote. `expect: "
|
|
104
|
+
* Calls a method and reports what it read and wrote. `expect: "read"` is how
|
|
102
105
|
* a card runs a method on its own: it refuses anything that might write, and
|
|
103
|
-
* so does everything the
|
|
106
|
+
* so does everything the read calls.
|
|
104
107
|
*/
|
|
105
108
|
/**
|
|
106
109
|
* `confirmed` is false for a tap the user hasn't answered; `ctx.call` leaves it out.
|
|
@@ -108,7 +111,7 @@ export declare class Runtime {
|
|
|
108
111
|
* pass on to the code the method calls.
|
|
109
112
|
*/
|
|
110
113
|
call(name: string, input?: unknown, options?: {
|
|
111
|
-
expect?:
|
|
114
|
+
expect?: "read";
|
|
112
115
|
confirmed?: boolean;
|
|
113
116
|
root?: string;
|
|
114
117
|
caller?: Caller;
|
package/dist/testing.js
CHANGED
|
@@ -91,6 +91,7 @@ function carryable(value) {
|
|
|
91
91
|
|
|
92
92
|
// src/runtime.ts
|
|
93
93
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
94
|
+
import { existsSync } from "fs";
|
|
94
95
|
import { mkdir as mkdir3, readdir as readdir3 } from "fs/promises";
|
|
95
96
|
import { join as join3 } from "path";
|
|
96
97
|
import { z as z5 } from "zod";
|
|
@@ -155,7 +156,7 @@ function recordRead(tag) {
|
|
|
155
156
|
}
|
|
156
157
|
function assertWritable(tag) {
|
|
157
158
|
if (current.getStore()?.readOnly) {
|
|
158
|
-
throw new MethodError("FORBIDDEN", `A
|
|
159
|
+
throw new MethodError("FORBIDDEN", `A read can't change data (it tried to write ${tag}).`);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
function recordWrite(tag) {
|
|
@@ -953,28 +954,28 @@ function kvMethods(name, kv, schema) {
|
|
|
953
954
|
const entry = z.object({ key: z.string(), value: schema, expiresAt: z.string().nullable(), createdAt: z.string(), updatedAt: z.string() });
|
|
954
955
|
return {
|
|
955
956
|
get: {
|
|
956
|
-
|
|
957
|
+
effect: "read",
|
|
957
958
|
description: `Get one ${name} entry by key, or null`,
|
|
958
959
|
input: z.object({ key }),
|
|
959
960
|
output: entry.nullable(),
|
|
960
961
|
run: ({ key: key2 }) => kv.entry(key2)
|
|
961
962
|
},
|
|
962
963
|
list: {
|
|
963
|
-
|
|
964
|
+
effect: "read",
|
|
964
965
|
description: `List ${name} entries in key order, optionally by prefix`,
|
|
965
966
|
input: z.object({ prefix: z.string().optional(), limit }),
|
|
966
967
|
output: z.array(entry),
|
|
967
968
|
run: (input) => kv.list(input)
|
|
968
969
|
},
|
|
969
970
|
set: {
|
|
970
|
-
|
|
971
|
+
effect: "write",
|
|
971
972
|
description: `Set a ${name} key; ttl ("5m", "1h", "7d" or ms) makes it expire`,
|
|
972
973
|
input: z.object({ key, value: schema, ttl }),
|
|
973
974
|
output: entry,
|
|
974
975
|
run: ({ key: key2, value, ttl: ttl2 }) => kv.set(key2, value, { ttl: ttl2 })
|
|
975
976
|
},
|
|
976
977
|
delete: {
|
|
977
|
-
|
|
978
|
+
effect: "destructive",
|
|
978
979
|
description: `Delete a ${name} key`,
|
|
979
980
|
input: z.object({ key }),
|
|
980
981
|
output: z.null(),
|
|
@@ -988,41 +989,42 @@ function streamMethods(name, stream, schema) {
|
|
|
988
989
|
const line = z.object({ id: z.string(), key: z.string(), offset: z.number(), at: z.string(), data: schema });
|
|
989
990
|
return {
|
|
990
991
|
tail: {
|
|
991
|
-
|
|
992
|
+
effect: "read",
|
|
993
|
+
stream: true,
|
|
992
994
|
description: `Follow a ${name} key live: its last lines (100 unless last says), then each new one, until it ends`,
|
|
993
995
|
input: z.object({ key, from: z.number().int().min(0).optional(), last: z.number().int().positive().max(500).optional() }),
|
|
994
996
|
run: ({ key: key2, from, last }, signal) => stream.follow(key2, { from, last, signal })
|
|
995
997
|
},
|
|
996
998
|
read: {
|
|
997
|
-
|
|
999
|
+
effect: "read",
|
|
998
1000
|
description: `Read a ${name} key's lines from an offset`,
|
|
999
1001
|
input: z.object({ key, from: z.number().int().min(0).optional(), limit }),
|
|
1000
1002
|
output: z.array(line),
|
|
1001
1003
|
run: ({ key: key2, ...options }) => stream.read(key2, options)
|
|
1002
1004
|
},
|
|
1003
1005
|
keys: {
|
|
1004
|
-
|
|
1006
|
+
effect: "read",
|
|
1005
1007
|
description: `List ${name} keys, most recently written first`,
|
|
1006
1008
|
input: z.object({ limit }),
|
|
1007
1009
|
output: z.array(z.object({ key: z.string(), lines: z.number(), bytes: z.number(), endedAt: z.string().nullable(), updatedAt: z.string() })),
|
|
1008
1010
|
run: (input) => stream.keys(input)
|
|
1009
1011
|
},
|
|
1010
1012
|
append: {
|
|
1011
|
-
|
|
1013
|
+
effect: "write",
|
|
1012
1014
|
description: `Append a line to a ${name} key`,
|
|
1013
1015
|
input: z.object({ key, data: schema }),
|
|
1014
1016
|
output: line,
|
|
1015
1017
|
run: ({ key: key2, data }) => stream.append(key2, data)
|
|
1016
1018
|
},
|
|
1017
1019
|
end: {
|
|
1018
|
-
|
|
1020
|
+
effect: "write",
|
|
1019
1021
|
description: `End a ${name} key: followers stop and appends fail`,
|
|
1020
1022
|
input: z.object({ key }),
|
|
1021
1023
|
output: z.null(),
|
|
1022
1024
|
run: async ({ key: key2 }) => (await stream.end(key2), null)
|
|
1023
1025
|
},
|
|
1024
1026
|
delete: {
|
|
1025
|
-
|
|
1027
|
+
effect: "destructive",
|
|
1026
1028
|
description: `Delete a ${name} key and its lines`,
|
|
1027
1029
|
input: z.object({ key }),
|
|
1028
1030
|
output: z.null(),
|
|
@@ -1051,28 +1053,28 @@ function bucketMethods(name, bucket, schema) {
|
|
|
1051
1053
|
const revision = z.number().int().optional();
|
|
1052
1054
|
return {
|
|
1053
1055
|
list: {
|
|
1054
|
-
|
|
1056
|
+
effect: "read",
|
|
1055
1057
|
description: `List ${name} files, newest first, optionally in one folder or with one status`,
|
|
1056
1058
|
input: z.object({ folder: z.string().optional(), status: status.optional(), limit }),
|
|
1057
1059
|
output: z.array(file),
|
|
1058
1060
|
run: (input) => bucket.list(input)
|
|
1059
1061
|
},
|
|
1060
1062
|
search: {
|
|
1061
|
-
|
|
1063
|
+
effect: "read",
|
|
1062
1064
|
description: `Search ${name} files by name, folder and meta`,
|
|
1063
1065
|
input: z.object({ query: z.string(), limit }),
|
|
1064
1066
|
output: z.array(file),
|
|
1065
1067
|
run: ({ query, limit: limit2 }) => bucket.search(query, { limit: limit2 })
|
|
1066
1068
|
},
|
|
1067
1069
|
get: {
|
|
1068
|
-
|
|
1070
|
+
effect: "read",
|
|
1069
1071
|
description: `Get one ${name} file by id, or null`,
|
|
1070
1072
|
input: z.object({ id }),
|
|
1071
1073
|
output: file.nullable(),
|
|
1072
1074
|
run: ({ id: id2 }) => bucket.get(id2)
|
|
1073
1075
|
},
|
|
1074
1076
|
put: {
|
|
1075
|
-
|
|
1077
|
+
effect: "write",
|
|
1076
1078
|
description: `Add a file to ${name}: its name, and text or an absolute path to copy`,
|
|
1077
1079
|
input: z.object({
|
|
1078
1080
|
name: z.string(),
|
|
@@ -1089,21 +1091,21 @@ function bucketMethods(name, bucket, schema) {
|
|
|
1089
1091
|
}
|
|
1090
1092
|
},
|
|
1091
1093
|
update: {
|
|
1092
|
-
|
|
1094
|
+
effect: "write",
|
|
1093
1095
|
description: `Change fields of a ${name} file's meta; null removes one`,
|
|
1094
1096
|
input: z.object({ id, meta: z.record(z.string(), z.unknown()), revision }),
|
|
1095
1097
|
output: file,
|
|
1096
1098
|
run: ({ id: id2, meta, revision: revision2 }) => bucket.update(id2, meta, { revision: revision2 })
|
|
1097
1099
|
},
|
|
1098
1100
|
move: {
|
|
1099
|
-
|
|
1101
|
+
effect: "write",
|
|
1100
1102
|
description: `Rename a ${name} file or move it to another folder`,
|
|
1101
1103
|
input: z.object({ id, name: z.string().optional(), folder: z.string().optional(), revision }),
|
|
1102
1104
|
output: file,
|
|
1103
1105
|
run: ({ id: id2, name: name2, folder, revision: revision2 }) => bucket.move(id2, { name: name2, folder }, { revision: revision2 })
|
|
1104
1106
|
},
|
|
1105
1107
|
delete: {
|
|
1106
|
-
|
|
1108
|
+
effect: "destructive",
|
|
1107
1109
|
description: `Delete a ${name} file and its meta`,
|
|
1108
1110
|
input: z.object({ id }),
|
|
1109
1111
|
output: z.null(),
|
|
@@ -1524,29 +1526,29 @@ function workflowMethods(plugin, namespace, name, definition, ctx, bridge, direc
|
|
|
1524
1526
|
return bridge({ operation, dir, plugin, namespace, workflow: name, root: ctx.root, caller: ctx.caller, input, settings: workflowSettings(definition) });
|
|
1525
1527
|
};
|
|
1526
1528
|
const id = z4.object({ id: z4.string().uuid() }).strict();
|
|
1527
|
-
const spec = (method,
|
|
1529
|
+
const spec = (method, effect, input, description, confirm, output) => ({
|
|
1528
1530
|
name: `${plugin}.${name}.${method}`,
|
|
1529
1531
|
plugin,
|
|
1530
|
-
|
|
1532
|
+
effect,
|
|
1531
1533
|
input,
|
|
1532
1534
|
description,
|
|
1533
1535
|
confirm,
|
|
1534
1536
|
output,
|
|
1535
1537
|
run: async (input2, raw) => {
|
|
1536
|
-
if (
|
|
1538
|
+
if (effect === "read")
|
|
1537
1539
|
recordRead(workflowTag(namespace));
|
|
1538
1540
|
const request = method === "start" ? { ...input2, input: raw.input } : input2;
|
|
1539
1541
|
return invoke(method, request);
|
|
1540
1542
|
}
|
|
1541
1543
|
});
|
|
1542
1544
|
return [
|
|
1543
|
-
spec("list", "
|
|
1544
|
-
spec("get", "
|
|
1545
|
-
spec("start", "
|
|
1546
|
-
spec("retry", "
|
|
1547
|
-
spec("restart", "
|
|
1548
|
-
spec("cancel", "
|
|
1549
|
-
spec("cancelAll", "
|
|
1545
|
+
spec("list", "read", z4.object({ limit: z4.number().int().min(1).max(100).default(20), status: z4.string().optional(), cursor: z4.string().optional() }).strict(), "List workflow runs", undefined, z4.object({ items: z4.array(RunSummarySchema), cursor: z4.string().nullable() })),
|
|
1546
|
+
spec("get", "read", id, "Get one workflow run"),
|
|
1547
|
+
spec("start", "write", z4.object({ input: definition.input, key: z4.string().min(1).max(200).optional() }).strict(), definition.description ?? "Start a durable workflow", definition.confirm, RunSummarySchema),
|
|
1548
|
+
spec("retry", "write", id, "Retry a safely failed query step", undefined, RunSummarySchema),
|
|
1549
|
+
spec("restart", "write", id, "Start a run that needs attention over from the beginning, on the current code", undefined, RunSummarySchema),
|
|
1550
|
+
spec("cancel", "write", id, "Request cancellation of a workflow", undefined, RunSummarySchema),
|
|
1551
|
+
spec("cancelAll", "write", z4.object({ status: z4.string().optional() }).strict(), "Cancel every unfinished run, or only those in one status", undefined, z4.object({ cancelled: z4.number().int() }))
|
|
1550
1552
|
];
|
|
1551
1553
|
}
|
|
1552
1554
|
function workflowSettings(definition) {
|
|
@@ -1606,6 +1608,8 @@ class Runtime {
|
|
|
1606
1608
|
continue;
|
|
1607
1609
|
if (only && !only.includes(entry.name))
|
|
1608
1610
|
continue;
|
|
1611
|
+
if (!existsSync(join3(pluginsDir, entry.name, "app.ts")))
|
|
1612
|
+
continue;
|
|
1609
1613
|
const module = await logContext.run({ plugin: entry.name }, () => import(join3(pluginsDir, entry.name, "app.ts")));
|
|
1610
1614
|
await this.add(entry.name, module.default);
|
|
1611
1615
|
}
|
|
@@ -1731,7 +1735,8 @@ class Runtime {
|
|
|
1731
1735
|
this.register({
|
|
1732
1736
|
name: fullName,
|
|
1733
1737
|
plugin: name,
|
|
1734
|
-
|
|
1738
|
+
effect: method.effect,
|
|
1739
|
+
stream: method.stream,
|
|
1735
1740
|
description: method.description,
|
|
1736
1741
|
confirm: method.confirm,
|
|
1737
1742
|
http: method.http,
|
|
@@ -1787,17 +1792,17 @@ class Runtime {
|
|
|
1787
1792
|
return runWith(where, () => this.call(name, input, options));
|
|
1788
1793
|
if (where !== logContext.getStore())
|
|
1789
1794
|
return logContext.run(where, () => this.call(name, input, options));
|
|
1790
|
-
if ((options.expect === "
|
|
1795
|
+
if ((options.expect === "read" || currentAccess()?.readOnly) && method.effect !== "read") {
|
|
1791
1796
|
throw new MethodError("FORBIDDEN", `${name} changes data, so it only runs when the user acts.`);
|
|
1792
1797
|
}
|
|
1793
1798
|
const raw = input ?? {};
|
|
1794
1799
|
const parsed = method.input.parse(raw);
|
|
1795
1800
|
if (method.confirm && (options.confirmed === false || workflowScope.getStore()))
|
|
1796
1801
|
throw new MethodError("CONFIRM", fillConfirm(method.confirm, parsed));
|
|
1797
|
-
if (method.
|
|
1802
|
+
if (method.stream)
|
|
1798
1803
|
return { result: await this.sample(name, input), reads: [], invalidates: [] };
|
|
1799
1804
|
const confirmed = this.callConfirmed.getStore() ?? false;
|
|
1800
|
-
const run = await tracked(method.
|
|
1805
|
+
const run = await tracked(method.effect === "read", (tags) => this.notifyInvalidated(tags), () => this.callConfirmed.run(confirmed || method.confirm !== undefined, async () => {
|
|
1801
1806
|
const result = await method.run(parsed, raw);
|
|
1802
1807
|
await this.events.emit(name, { input: parsed, result });
|
|
1803
1808
|
return result;
|
|
@@ -1816,8 +1821,8 @@ class Runtime {
|
|
|
1816
1821
|
const method = this.methods.get(name);
|
|
1817
1822
|
if (!method)
|
|
1818
1823
|
throw new MethodError("NOT_FOUND", `There is no method ${name}.`);
|
|
1819
|
-
if (method.
|
|
1820
|
-
throw new MethodError("VALIDATION", `${name} is
|
|
1824
|
+
if (!method.stream)
|
|
1825
|
+
throw new MethodError("VALIDATION", `${name} is not a stream.`);
|
|
1821
1826
|
const raw = input ?? {};
|
|
1822
1827
|
const parsed = method.input.parse(raw);
|
|
1823
1828
|
const root = options.root ?? this.callRoot.getStore() ?? this.root;
|
|
@@ -1872,7 +1877,7 @@ class Runtime {
|
|
|
1872
1877
|
const statuses = [];
|
|
1873
1878
|
for (const entry of entries) {
|
|
1874
1879
|
const timezone = entry.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1875
|
-
const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone}`;
|
|
1880
|
+
const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone} ${entry.root ?? ""}`;
|
|
1876
1881
|
try {
|
|
1877
1882
|
const job = kept.get(signature) ?? this.cronJob(entry, timezone, signature);
|
|
1878
1883
|
jobs.push(job);
|
|
@@ -1904,8 +1909,8 @@ class Runtime {
|
|
|
1904
1909
|
const method = this.methods.get(name);
|
|
1905
1910
|
if (!method || method.plugin !== entry.plugin)
|
|
1906
1911
|
throw new Error(`${entry.method} isn't one of ${entry.plugin}'s methods`);
|
|
1907
|
-
if (method.
|
|
1908
|
-
throw new Error(`${name} is a stream; a cron runs a
|
|
1912
|
+
if (method.stream)
|
|
1913
|
+
throw new Error(`${name} is a stream; a cron runs a read or a write`);
|
|
1909
1914
|
if (method.confirm)
|
|
1910
1915
|
throw new Error(`${name} asks before it runs, so nothing may run it on a schedule`);
|
|
1911
1916
|
if (!method.input.safeParse({}).success)
|
|
@@ -1922,6 +1927,7 @@ class Runtime {
|
|
|
1922
1927
|
return {
|
|
1923
1928
|
signature,
|
|
1924
1929
|
plugin: entry.plugin,
|
|
1930
|
+
root: entry.root,
|
|
1925
1931
|
method: name,
|
|
1926
1932
|
schedule: entry.schedule,
|
|
1927
1933
|
timezone,
|
|
@@ -1946,7 +1952,7 @@ class Runtime {
|
|
|
1946
1952
|
job.running = true;
|
|
1947
1953
|
job.lastRunAt = now;
|
|
1948
1954
|
const startedAt = Date.now();
|
|
1949
|
-
started.push(this.call(job.method, {}, { caller: "cron" }).then(() => {
|
|
1955
|
+
started.push(this.call(job.method, {}, { caller: "cron", root: job.root }).then(() => {
|
|
1950
1956
|
job.lastError = null;
|
|
1951
1957
|
}, (error) => {
|
|
1952
1958
|
job.lastError = toCallError(error).message;
|
|
@@ -2007,7 +2013,8 @@ class Runtime {
|
|
|
2007
2013
|
description: plugin.description ?? null,
|
|
2008
2014
|
methods: [...this.methods.values()].filter((method) => method.plugin === name).map((method) => ({
|
|
2009
2015
|
name: method.name,
|
|
2010
|
-
|
|
2016
|
+
effect: method.effect,
|
|
2017
|
+
stream: method.stream ?? false,
|
|
2011
2018
|
description: method.description ?? null,
|
|
2012
2019
|
confirm: method.confirm ?? null,
|
|
2013
2020
|
http: method.http ?? null,
|
|
@@ -2083,6 +2090,9 @@ class Runtime {
|
|
|
2083
2090
|
register(method) {
|
|
2084
2091
|
if (this.methods.has(method.name))
|
|
2085
2092
|
throw new Error(`${method.name} is defined twice; rename the method or the data`);
|
|
2093
|
+
if (method.effect === "destructive" && !method.confirm) {
|
|
2094
|
+
throw new Error(`${method.name} is destructive, so it needs a confirm question, such as { confirm: "Delete {{id}}?" }`);
|
|
2095
|
+
}
|
|
2086
2096
|
this.methods.set(method.name, method);
|
|
2087
2097
|
}
|
|
2088
2098
|
registerGenerated(plugin, name, methods) {
|
|
@@ -2099,26 +2109,26 @@ class Runtime {
|
|
|
2099
2109
|
const prefix = `${plugin}.${name}`;
|
|
2100
2110
|
const id = z5.object({ id: z5.string() });
|
|
2101
2111
|
const record2 = schema.extend({ id: z5.string(), createdAt: z5.string(), updatedAt: z5.string() });
|
|
2102
|
-
const crud = (method,
|
|
2112
|
+
const crud = (method, effect, input, output, description, run, confirm) => this.register({
|
|
2103
2113
|
name: `${prefix}.${method}`,
|
|
2104
2114
|
plugin,
|
|
2105
|
-
|
|
2115
|
+
effect,
|
|
2106
2116
|
description,
|
|
2107
2117
|
confirm,
|
|
2108
2118
|
input,
|
|
2109
2119
|
output,
|
|
2110
2120
|
run: async (value, raw) => run(value, raw)
|
|
2111
2121
|
});
|
|
2112
|
-
crud("list", "
|
|
2122
|
+
crud("list", "read", z5.object({
|
|
2113
2123
|
limit: z5.number().int().positive().max(500).optional(),
|
|
2114
2124
|
offset: z5.number().int().min(0).optional(),
|
|
2115
2125
|
orderBy: z5.string().optional(),
|
|
2116
2126
|
order: z5.enum(["asc", "desc"]).optional()
|
|
2117
2127
|
}), z5.array(record2), `List ${name} records, newest first unless orderBy says`, (input) => collection.list(input));
|
|
2118
|
-
crud("get", "
|
|
2119
|
-
crud("create", "
|
|
2120
|
-
crud("update", "
|
|
2121
|
-
crud("delete", "
|
|
2128
|
+
crud("get", "read", id, record2.nullable(), `Get one ${name} record by id, or null`, (input) => collection.get(input.id));
|
|
2129
|
+
crud("create", "write", schema, record2, `Create a ${name} record`, (input) => collection.create(input));
|
|
2130
|
+
crud("update", "write", id.extend(schema.partial().shape), record2, `Update fields on a ${name} record`, ({ id: id2, ...fields }, raw) => collection.update(id2, Object.fromEntries(Object.entries(fields).filter(([key]) => Object.hasOwn(raw ?? {}, key)))));
|
|
2131
|
+
crud("delete", "destructive", id, z5.null(), `Delete a ${name} record by id`, async (input) => {
|
|
2122
2132
|
await collection.delete(input.id);
|
|
2123
2133
|
return null;
|
|
2124
2134
|
}, "Delete this record?");
|
|
@@ -2147,7 +2157,7 @@ function cronState(job) {
|
|
|
2147
2157
|
function mcpFailure(server, tool, error) {
|
|
2148
2158
|
switch (error.code) {
|
|
2149
2159
|
case "NOT_READ_ONLY":
|
|
2150
|
-
return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a
|
|
2160
|
+
return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a write, not a read.`);
|
|
2151
2161
|
case "CONFIRM":
|
|
2152
2162
|
return new MethodError("FORBIDDEN", `${server}.${tool} may destroy data, so it runs only from a method with a confirm.`);
|
|
2153
2163
|
}
|
|
@@ -47,10 +47,12 @@ function definePlugin(plugin) {
|
|
|
47
47
|
return plugin;
|
|
48
48
|
}
|
|
49
49
|
function method(input, run, options = {}) {
|
|
50
|
+
if ("kind" in options)
|
|
51
|
+
throw new Error('method: kind is now effect: "read", "write" or "destructive"');
|
|
50
52
|
return {
|
|
51
53
|
input,
|
|
52
54
|
run,
|
|
53
|
-
|
|
55
|
+
effect: options.effect ?? "write",
|
|
54
56
|
output: options.output,
|
|
55
57
|
description: options.description,
|
|
56
58
|
confirm: options.confirm,
|
|
@@ -61,14 +63,14 @@ function cached(input, run, options) {
|
|
|
61
63
|
return {
|
|
62
64
|
input,
|
|
63
65
|
run,
|
|
64
|
-
|
|
66
|
+
effect: "read",
|
|
65
67
|
output: options.output,
|
|
66
68
|
description: options.description,
|
|
67
69
|
cache: { fresh: parseDuration(options.fresh), shared: options.shared ?? false, timeout: parseDuration(options.timeout ?? "1m") }
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
function stream(input, run, options = {}) {
|
|
71
|
-
return { input, run,
|
|
73
|
+
return { input, run, effect: "read", stream: true, description: options.description };
|
|
72
74
|
}
|
|
73
75
|
// src/uploads.ts
|
|
74
76
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -19669,6 +19671,7 @@ function carryable(value) {
|
|
|
19669
19671
|
|
|
19670
19672
|
// src/runtime.ts
|
|
19671
19673
|
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
19674
|
+
import { existsSync } from "fs";
|
|
19672
19675
|
import { mkdir as mkdir3, readdir as readdir3 } from "fs/promises";
|
|
19673
19676
|
import { join as join3 } from "path";
|
|
19674
19677
|
|
|
@@ -19732,7 +19735,7 @@ function recordRead(tag) {
|
|
|
19732
19735
|
}
|
|
19733
19736
|
function assertWritable(tag) {
|
|
19734
19737
|
if (current.getStore()?.readOnly) {
|
|
19735
|
-
throw new MethodError("FORBIDDEN", `A
|
|
19738
|
+
throw new MethodError("FORBIDDEN", `A read can't change data (it tried to write ${tag}).`);
|
|
19736
19739
|
}
|
|
19737
19740
|
}
|
|
19738
19741
|
function recordWrite(tag) {
|
|
@@ -20529,28 +20532,28 @@ function kvMethods(name, kv, schema) {
|
|
|
20529
20532
|
const entry = exports_external.object({ key: exports_external.string(), value: schema, expiresAt: exports_external.string().nullable(), createdAt: exports_external.string(), updatedAt: exports_external.string() });
|
|
20530
20533
|
return {
|
|
20531
20534
|
get: {
|
|
20532
|
-
|
|
20535
|
+
effect: "read",
|
|
20533
20536
|
description: `Get one ${name} entry by key, or null`,
|
|
20534
20537
|
input: exports_external.object({ key }),
|
|
20535
20538
|
output: entry.nullable(),
|
|
20536
20539
|
run: ({ key: key2 }) => kv.entry(key2)
|
|
20537
20540
|
},
|
|
20538
20541
|
list: {
|
|
20539
|
-
|
|
20542
|
+
effect: "read",
|
|
20540
20543
|
description: `List ${name} entries in key order, optionally by prefix`,
|
|
20541
20544
|
input: exports_external.object({ prefix: exports_external.string().optional(), limit }),
|
|
20542
20545
|
output: exports_external.array(entry),
|
|
20543
20546
|
run: (input2) => kv.list(input2)
|
|
20544
20547
|
},
|
|
20545
20548
|
set: {
|
|
20546
|
-
|
|
20549
|
+
effect: "write",
|
|
20547
20550
|
description: `Set a ${name} key; ttl ("5m", "1h", "7d" or ms) makes it expire`,
|
|
20548
20551
|
input: exports_external.object({ key, value: schema, ttl }),
|
|
20549
20552
|
output: entry,
|
|
20550
20553
|
run: ({ key: key2, value, ttl: ttl2 }) => kv.set(key2, value, { ttl: ttl2 })
|
|
20551
20554
|
},
|
|
20552
20555
|
delete: {
|
|
20553
|
-
|
|
20556
|
+
effect: "destructive",
|
|
20554
20557
|
description: `Delete a ${name} key`,
|
|
20555
20558
|
input: exports_external.object({ key }),
|
|
20556
20559
|
output: exports_external.null(),
|
|
@@ -20564,41 +20567,42 @@ function streamMethods(name, stream2, schema) {
|
|
|
20564
20567
|
const line = exports_external.object({ id: exports_external.string(), key: exports_external.string(), offset: exports_external.number(), at: exports_external.string(), data: schema });
|
|
20565
20568
|
return {
|
|
20566
20569
|
tail: {
|
|
20567
|
-
|
|
20570
|
+
effect: "read",
|
|
20571
|
+
stream: true,
|
|
20568
20572
|
description: `Follow a ${name} key live: its last lines (100 unless last says), then each new one, until it ends`,
|
|
20569
20573
|
input: exports_external.object({ key, from: exports_external.number().int().min(0).optional(), last: exports_external.number().int().positive().max(500).optional() }),
|
|
20570
20574
|
run: ({ key: key2, from, last }, signal) => stream2.follow(key2, { from, last, signal })
|
|
20571
20575
|
},
|
|
20572
20576
|
read: {
|
|
20573
|
-
|
|
20577
|
+
effect: "read",
|
|
20574
20578
|
description: `Read a ${name} key's lines from an offset`,
|
|
20575
20579
|
input: exports_external.object({ key, from: exports_external.number().int().min(0).optional(), limit }),
|
|
20576
20580
|
output: exports_external.array(line),
|
|
20577
20581
|
run: ({ key: key2, ...options }) => stream2.read(key2, options)
|
|
20578
20582
|
},
|
|
20579
20583
|
keys: {
|
|
20580
|
-
|
|
20584
|
+
effect: "read",
|
|
20581
20585
|
description: `List ${name} keys, most recently written first`,
|
|
20582
20586
|
input: exports_external.object({ limit }),
|
|
20583
20587
|
output: exports_external.array(exports_external.object({ key: exports_external.string(), lines: exports_external.number(), bytes: exports_external.number(), endedAt: exports_external.string().nullable(), updatedAt: exports_external.string() })),
|
|
20584
20588
|
run: (input2) => stream2.keys(input2)
|
|
20585
20589
|
},
|
|
20586
20590
|
append: {
|
|
20587
|
-
|
|
20591
|
+
effect: "write",
|
|
20588
20592
|
description: `Append a line to a ${name} key`,
|
|
20589
20593
|
input: exports_external.object({ key, data: schema }),
|
|
20590
20594
|
output: line,
|
|
20591
20595
|
run: ({ key: key2, data }) => stream2.append(key2, data)
|
|
20592
20596
|
},
|
|
20593
20597
|
end: {
|
|
20594
|
-
|
|
20598
|
+
effect: "write",
|
|
20595
20599
|
description: `End a ${name} key: followers stop and appends fail`,
|
|
20596
20600
|
input: exports_external.object({ key }),
|
|
20597
20601
|
output: exports_external.null(),
|
|
20598
20602
|
run: async ({ key: key2 }) => (await stream2.end(key2), null)
|
|
20599
20603
|
},
|
|
20600
20604
|
delete: {
|
|
20601
|
-
|
|
20605
|
+
effect: "destructive",
|
|
20602
20606
|
description: `Delete a ${name} key and its lines`,
|
|
20603
20607
|
input: exports_external.object({ key }),
|
|
20604
20608
|
output: exports_external.null(),
|
|
@@ -20627,28 +20631,28 @@ function bucketMethods(name, bucket, schema) {
|
|
|
20627
20631
|
const revision = exports_external.number().int().optional();
|
|
20628
20632
|
return {
|
|
20629
20633
|
list: {
|
|
20630
|
-
|
|
20634
|
+
effect: "read",
|
|
20631
20635
|
description: `List ${name} files, newest first, optionally in one folder or with one status`,
|
|
20632
20636
|
input: exports_external.object({ folder: exports_external.string().optional(), status: status.optional(), limit }),
|
|
20633
20637
|
output: exports_external.array(file2),
|
|
20634
20638
|
run: (input2) => bucket.list(input2)
|
|
20635
20639
|
},
|
|
20636
20640
|
search: {
|
|
20637
|
-
|
|
20641
|
+
effect: "read",
|
|
20638
20642
|
description: `Search ${name} files by name, folder and meta`,
|
|
20639
20643
|
input: exports_external.object({ query: exports_external.string(), limit }),
|
|
20640
20644
|
output: exports_external.array(file2),
|
|
20641
20645
|
run: ({ query, limit: limit2 }) => bucket.search(query, { limit: limit2 })
|
|
20642
20646
|
},
|
|
20643
20647
|
get: {
|
|
20644
|
-
|
|
20648
|
+
effect: "read",
|
|
20645
20649
|
description: `Get one ${name} file by id, or null`,
|
|
20646
20650
|
input: exports_external.object({ id }),
|
|
20647
20651
|
output: file2.nullable(),
|
|
20648
20652
|
run: ({ id: id2 }) => bucket.get(id2)
|
|
20649
20653
|
},
|
|
20650
20654
|
put: {
|
|
20651
|
-
|
|
20655
|
+
effect: "write",
|
|
20652
20656
|
description: `Add a file to ${name}: its name, and text or an absolute path to copy`,
|
|
20653
20657
|
input: exports_external.object({
|
|
20654
20658
|
name: exports_external.string(),
|
|
@@ -20665,21 +20669,21 @@ function bucketMethods(name, bucket, schema) {
|
|
|
20665
20669
|
}
|
|
20666
20670
|
},
|
|
20667
20671
|
update: {
|
|
20668
|
-
|
|
20672
|
+
effect: "write",
|
|
20669
20673
|
description: `Change fields of a ${name} file's meta; null removes one`,
|
|
20670
20674
|
input: exports_external.object({ id, meta: exports_external.record(exports_external.string(), exports_external.unknown()), revision }),
|
|
20671
20675
|
output: file2,
|
|
20672
20676
|
run: ({ id: id2, meta: meta3, revision: revision2 }) => bucket.update(id2, meta3, { revision: revision2 })
|
|
20673
20677
|
},
|
|
20674
20678
|
move: {
|
|
20675
|
-
|
|
20679
|
+
effect: "write",
|
|
20676
20680
|
description: `Rename a ${name} file or move it to another folder`,
|
|
20677
20681
|
input: exports_external.object({ id, name: exports_external.string().optional(), folder: exports_external.string().optional(), revision }),
|
|
20678
20682
|
output: file2,
|
|
20679
20683
|
run: ({ id: id2, name: name2, folder, revision: revision2 }) => bucket.move(id2, { name: name2, folder }, { revision: revision2 })
|
|
20680
20684
|
},
|
|
20681
20685
|
delete: {
|
|
20682
|
-
|
|
20686
|
+
effect: "destructive",
|
|
20683
20687
|
description: `Delete a ${name} file and its meta`,
|
|
20684
20688
|
input: exports_external.object({ id }),
|
|
20685
20689
|
output: exports_external.null(),
|
|
@@ -21115,29 +21119,29 @@ function workflowMethods(plugin, namespace, name, definition, ctx, bridge, direc
|
|
|
21115
21119
|
return bridge({ operation, dir, plugin, namespace, workflow: name, root: ctx.root, caller: ctx.caller, input: input2, settings: workflowSettings(definition) });
|
|
21116
21120
|
};
|
|
21117
21121
|
const id = exports_external.object({ id: exports_external.string().uuid() }).strict();
|
|
21118
|
-
const spec = (method2,
|
|
21122
|
+
const spec = (method2, effect, input2, description, confirm, output2) => ({
|
|
21119
21123
|
name: `${plugin}.${name}.${method2}`,
|
|
21120
21124
|
plugin,
|
|
21121
|
-
|
|
21125
|
+
effect,
|
|
21122
21126
|
input: input2,
|
|
21123
21127
|
description,
|
|
21124
21128
|
confirm,
|
|
21125
21129
|
output: output2,
|
|
21126
21130
|
run: async (input3, raw) => {
|
|
21127
|
-
if (
|
|
21131
|
+
if (effect === "read")
|
|
21128
21132
|
recordRead(workflowTag(namespace));
|
|
21129
21133
|
const request = method2 === "start" ? { ...input3, input: raw.input } : input3;
|
|
21130
21134
|
return invoke(method2, request);
|
|
21131
21135
|
}
|
|
21132
21136
|
});
|
|
21133
21137
|
return [
|
|
21134
|
-
spec("list", "
|
|
21135
|
-
spec("get", "
|
|
21136
|
-
spec("start", "
|
|
21137
|
-
spec("retry", "
|
|
21138
|
-
spec("restart", "
|
|
21139
|
-
spec("cancel", "
|
|
21140
|
-
spec("cancelAll", "
|
|
21138
|
+
spec("list", "read", exports_external.object({ limit: exports_external.number().int().min(1).max(100).default(20), status: exports_external.string().optional(), cursor: exports_external.string().optional() }).strict(), "List workflow runs", undefined, exports_external.object({ items: exports_external.array(RunSummarySchema), cursor: exports_external.string().nullable() })),
|
|
21139
|
+
spec("get", "read", id, "Get one workflow run"),
|
|
21140
|
+
spec("start", "write", exports_external.object({ input: definition.input, key: exports_external.string().min(1).max(200).optional() }).strict(), definition.description ?? "Start a durable workflow", definition.confirm, RunSummarySchema),
|
|
21141
|
+
spec("retry", "write", id, "Retry a safely failed query step", undefined, RunSummarySchema),
|
|
21142
|
+
spec("restart", "write", id, "Start a run that needs attention over from the beginning, on the current code", undefined, RunSummarySchema),
|
|
21143
|
+
spec("cancel", "write", id, "Request cancellation of a workflow", undefined, RunSummarySchema),
|
|
21144
|
+
spec("cancelAll", "write", exports_external.object({ status: exports_external.string().optional() }).strict(), "Cancel every unfinished run, or only those in one status", undefined, exports_external.object({ cancelled: exports_external.number().int() }))
|
|
21141
21145
|
];
|
|
21142
21146
|
}
|
|
21143
21147
|
function workflowSettings(definition) {
|
|
@@ -21197,6 +21201,8 @@ class Runtime {
|
|
|
21197
21201
|
continue;
|
|
21198
21202
|
if (only && !only.includes(entry.name))
|
|
21199
21203
|
continue;
|
|
21204
|
+
if (!existsSync(join3(pluginsDir, entry.name, "app.ts")))
|
|
21205
|
+
continue;
|
|
21200
21206
|
const module = await logContext.run({ plugin: entry.name }, () => import(join3(pluginsDir, entry.name, "app.ts")));
|
|
21201
21207
|
await this.add(entry.name, module.default);
|
|
21202
21208
|
}
|
|
@@ -21322,7 +21328,8 @@ class Runtime {
|
|
|
21322
21328
|
this.register({
|
|
21323
21329
|
name: fullName,
|
|
21324
21330
|
plugin: name,
|
|
21325
|
-
|
|
21331
|
+
effect: method2.effect,
|
|
21332
|
+
stream: method2.stream,
|
|
21326
21333
|
description: method2.description,
|
|
21327
21334
|
confirm: method2.confirm,
|
|
21328
21335
|
http: method2.http,
|
|
@@ -21378,17 +21385,17 @@ class Runtime {
|
|
|
21378
21385
|
return runWith(where, () => this.call(name, input2, options));
|
|
21379
21386
|
if (where !== logContext.getStore())
|
|
21380
21387
|
return logContext.run(where, () => this.call(name, input2, options));
|
|
21381
|
-
if ((options.expect === "
|
|
21388
|
+
if ((options.expect === "read" || currentAccess()?.readOnly) && method2.effect !== "read") {
|
|
21382
21389
|
throw new MethodError("FORBIDDEN", `${name} changes data, so it only runs when the user acts.`);
|
|
21383
21390
|
}
|
|
21384
21391
|
const raw = input2 ?? {};
|
|
21385
21392
|
const parsed = method2.input.parse(raw);
|
|
21386
21393
|
if (method2.confirm && (options.confirmed === false || workflowScope.getStore()))
|
|
21387
21394
|
throw new MethodError("CONFIRM", fillConfirm(method2.confirm, parsed));
|
|
21388
|
-
if (method2.
|
|
21395
|
+
if (method2.stream)
|
|
21389
21396
|
return { result: await this.sample(name, input2), reads: [], invalidates: [] };
|
|
21390
21397
|
const confirmed = this.callConfirmed.getStore() ?? false;
|
|
21391
|
-
const run = await tracked(method2.
|
|
21398
|
+
const run = await tracked(method2.effect === "read", (tags) => this.notifyInvalidated(tags), () => this.callConfirmed.run(confirmed || method2.confirm !== undefined, async () => {
|
|
21392
21399
|
const result = await method2.run(parsed, raw);
|
|
21393
21400
|
await this.events.emit(name, { input: parsed, result });
|
|
21394
21401
|
return result;
|
|
@@ -21407,8 +21414,8 @@ class Runtime {
|
|
|
21407
21414
|
const method2 = this.methods.get(name);
|
|
21408
21415
|
if (!method2)
|
|
21409
21416
|
throw new MethodError("NOT_FOUND", `There is no method ${name}.`);
|
|
21410
|
-
if (method2.
|
|
21411
|
-
throw new MethodError("VALIDATION", `${name} is
|
|
21417
|
+
if (!method2.stream)
|
|
21418
|
+
throw new MethodError("VALIDATION", `${name} is not a stream.`);
|
|
21412
21419
|
const raw = input2 ?? {};
|
|
21413
21420
|
const parsed = method2.input.parse(raw);
|
|
21414
21421
|
const root = options.root ?? this.callRoot.getStore() ?? this.root;
|
|
@@ -21463,7 +21470,7 @@ class Runtime {
|
|
|
21463
21470
|
const statuses = [];
|
|
21464
21471
|
for (const entry of entries) {
|
|
21465
21472
|
const timezone = entry.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
21466
|
-
const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone}`;
|
|
21473
|
+
const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone} ${entry.root ?? ""}`;
|
|
21467
21474
|
try {
|
|
21468
21475
|
const job = kept.get(signature) ?? this.cronJob(entry, timezone, signature);
|
|
21469
21476
|
jobs.push(job);
|
|
@@ -21495,8 +21502,8 @@ class Runtime {
|
|
|
21495
21502
|
const method2 = this.methods.get(name);
|
|
21496
21503
|
if (!method2 || method2.plugin !== entry.plugin)
|
|
21497
21504
|
throw new Error(`${entry.method} isn't one of ${entry.plugin}'s methods`);
|
|
21498
|
-
if (method2.
|
|
21499
|
-
throw new Error(`${name} is a stream; a cron runs a
|
|
21505
|
+
if (method2.stream)
|
|
21506
|
+
throw new Error(`${name} is a stream; a cron runs a read or a write`);
|
|
21500
21507
|
if (method2.confirm)
|
|
21501
21508
|
throw new Error(`${name} asks before it runs, so nothing may run it on a schedule`);
|
|
21502
21509
|
if (!method2.input.safeParse({}).success)
|
|
@@ -21513,6 +21520,7 @@ class Runtime {
|
|
|
21513
21520
|
return {
|
|
21514
21521
|
signature,
|
|
21515
21522
|
plugin: entry.plugin,
|
|
21523
|
+
root: entry.root,
|
|
21516
21524
|
method: name,
|
|
21517
21525
|
schedule: entry.schedule,
|
|
21518
21526
|
timezone,
|
|
@@ -21537,7 +21545,7 @@ class Runtime {
|
|
|
21537
21545
|
job.running = true;
|
|
21538
21546
|
job.lastRunAt = now;
|
|
21539
21547
|
const startedAt = Date.now();
|
|
21540
|
-
started.push(this.call(job.method, {}, { caller: "cron" }).then(() => {
|
|
21548
|
+
started.push(this.call(job.method, {}, { caller: "cron", root: job.root }).then(() => {
|
|
21541
21549
|
job.lastError = null;
|
|
21542
21550
|
}, (error62) => {
|
|
21543
21551
|
job.lastError = toCallError(error62).message;
|
|
@@ -21598,7 +21606,8 @@ class Runtime {
|
|
|
21598
21606
|
description: plugin.description ?? null,
|
|
21599
21607
|
methods: [...this.methods.values()].filter((method2) => method2.plugin === name).map((method2) => ({
|
|
21600
21608
|
name: method2.name,
|
|
21601
|
-
|
|
21609
|
+
effect: method2.effect,
|
|
21610
|
+
stream: method2.stream ?? false,
|
|
21602
21611
|
description: method2.description ?? null,
|
|
21603
21612
|
confirm: method2.confirm ?? null,
|
|
21604
21613
|
http: method2.http ?? null,
|
|
@@ -21674,6 +21683,9 @@ class Runtime {
|
|
|
21674
21683
|
register(method2) {
|
|
21675
21684
|
if (this.methods.has(method2.name))
|
|
21676
21685
|
throw new Error(`${method2.name} is defined twice; rename the method or the data`);
|
|
21686
|
+
if (method2.effect === "destructive" && !method2.confirm) {
|
|
21687
|
+
throw new Error(`${method2.name} is destructive, so it needs a confirm question, such as { confirm: "Delete {{id}}?" }`);
|
|
21688
|
+
}
|
|
21677
21689
|
this.methods.set(method2.name, method2);
|
|
21678
21690
|
}
|
|
21679
21691
|
registerGenerated(plugin, name, methods) {
|
|
@@ -21690,26 +21702,26 @@ class Runtime {
|
|
|
21690
21702
|
const prefix = `${plugin}.${name}`;
|
|
21691
21703
|
const id = exports_external.object({ id: exports_external.string() });
|
|
21692
21704
|
const record3 = schema.extend({ id: exports_external.string(), createdAt: exports_external.string(), updatedAt: exports_external.string() });
|
|
21693
|
-
const crud = (method2,
|
|
21705
|
+
const crud = (method2, effect, input2, output2, description, run, confirm) => this.register({
|
|
21694
21706
|
name: `${prefix}.${method2}`,
|
|
21695
21707
|
plugin,
|
|
21696
|
-
|
|
21708
|
+
effect,
|
|
21697
21709
|
description,
|
|
21698
21710
|
confirm,
|
|
21699
21711
|
input: input2,
|
|
21700
21712
|
output: output2,
|
|
21701
21713
|
run: async (value, raw) => run(value, raw)
|
|
21702
21714
|
});
|
|
21703
|
-
crud("list", "
|
|
21715
|
+
crud("list", "read", exports_external.object({
|
|
21704
21716
|
limit: exports_external.number().int().positive().max(500).optional(),
|
|
21705
21717
|
offset: exports_external.number().int().min(0).optional(),
|
|
21706
21718
|
orderBy: exports_external.string().optional(),
|
|
21707
21719
|
order: exports_external.enum(["asc", "desc"]).optional()
|
|
21708
21720
|
}), exports_external.array(record3), `List ${name} records, newest first unless orderBy says`, (input2) => collection.list(input2));
|
|
21709
|
-
crud("get", "
|
|
21710
|
-
crud("create", "
|
|
21711
|
-
crud("update", "
|
|
21712
|
-
crud("delete", "
|
|
21721
|
+
crud("get", "read", id, record3.nullable(), `Get one ${name} record by id, or null`, (input2) => collection.get(input2.id));
|
|
21722
|
+
crud("create", "write", schema, record3, `Create a ${name} record`, (input2) => collection.create(input2));
|
|
21723
|
+
crud("update", "write", id.extend(schema.partial().shape), record3, `Update fields on a ${name} record`, ({ id: id2, ...fields }, raw) => collection.update(id2, Object.fromEntries(Object.entries(fields).filter(([key]) => Object.hasOwn(raw ?? {}, key)))));
|
|
21724
|
+
crud("delete", "destructive", id, exports_external.null(), `Delete a ${name} record by id`, async (input2) => {
|
|
21713
21725
|
await collection.delete(input2.id);
|
|
21714
21726
|
return null;
|
|
21715
21727
|
}, "Delete this record?");
|
|
@@ -21738,7 +21750,7 @@ function cronState(job) {
|
|
|
21738
21750
|
function mcpFailure(server, tool, error62) {
|
|
21739
21751
|
switch (error62.code) {
|
|
21740
21752
|
case "NOT_READ_ONLY":
|
|
21741
|
-
return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a
|
|
21753
|
+
return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a write, not a read.`);
|
|
21742
21754
|
case "CONFIRM":
|
|
21743
21755
|
return new MethodError("FORBIDDEN", `${server}.${tool} may destroy data, so it runs only from a method with a confirm.`);
|
|
21744
21756
|
}
|