@repogo/plugins 0.1.43 → 0.1.45
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 +169 -44
- package/dist/context.d.ts +14 -12
- package/dist/define.d.ts +33 -11
- package/dist/generated.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +61 -17
- package/dist/remote.d.ts +7 -0
- package/dist/runtime.d.ts +7 -5
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +25 -19
- package/dist/workflow/contracts.d.ts +3 -1
- package/dist/workflow/definition.d.ts +1 -1
- package/dist/workflow/events.d.ts +1 -0
- package/dist/workflow-executor.js +83 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
Plugins are small apps that run on your environment, the machine the RepoGo
|
|
4
4
|
host runs on. A plugin keeps its own data, does work with your tools and APIs,
|
|
5
5
|
and is reached two ways: agents call its methods as tools, and cards on your
|
|
6
|
-
phone draw its data live and run its methods when you tap.
|
|
6
|
+
phone draw its data live and run its methods when you tap. A method may also
|
|
7
|
+
opt into plain HTTP, for this machine's containers or the public.
|
|
7
8
|
|
|
8
9
|
- [Quick start](#quick-start)
|
|
9
10
|
- [Where plugins live](#where-plugins-live)
|
|
10
|
-
- [Methods](#methods): reads, writes, destructive methods, streams, cached reads
|
|
11
|
+
- [Methods](#methods): reads, writes, destructive methods, streams, cached reads, [serving over HTTP](#serving-over-http)
|
|
11
12
|
- [Data](#data): namespaces, collections, kv, streams, buckets, uploads
|
|
12
13
|
- [Workflows](#workflows)
|
|
13
14
|
- [Crons](#crons)
|
|
@@ -15,7 +16,7 @@ phone draw its data live and run its methods when you tap.
|
|
|
15
16
|
- [The context](#the-context)
|
|
16
17
|
- [Events](#events)
|
|
17
18
|
- [How cards and agents reach a plugin](#how-cards-and-agents-reach-a-plugin)
|
|
18
|
-
- [Errors and
|
|
19
|
+
- [Errors and approvals](#errors-and-approvals)
|
|
19
20
|
- [Testing](#testing)
|
|
20
21
|
|
|
21
22
|
## Quick start
|
|
@@ -36,7 +37,7 @@ export default definePlugin({
|
|
|
36
37
|
clear: method(z.object({}), async (_, ctx) => {
|
|
37
38
|
for (const note of await ctx.collections.notes.list({ limit: 500 })) await ctx.collections.notes.delete(note.id);
|
|
38
39
|
return null;
|
|
39
|
-
}, { effect: "destructive",
|
|
40
|
+
}, { effect: "destructive", approve: "Delete every note?" }),
|
|
40
41
|
},
|
|
41
42
|
});
|
|
42
43
|
```
|
|
@@ -73,7 +74,10 @@ lives beside its plugin and takes the project as state (`?project=…`).
|
|
|
73
74
|
|
|
74
75
|
A plugin's name is its folder's: lowercase letters, digits and hyphens. The
|
|
75
76
|
environment's plugins run together in one Worker, so a plugin's blocking code
|
|
76
|
-
stalls the others: use async APIs.
|
|
77
|
+
stalls the others: use async APIs. They load together too: one that fails to
|
|
78
|
+
load (a throw at import, a destructive method without an `approve`, data without
|
|
79
|
+
a `namespace`, a method named like a generated one) stops every plugin in the
|
|
80
|
+
folder, and each call answers its error, until it's fixed.
|
|
77
81
|
|
|
78
82
|
Pages beside an environment plugin (`pages/*.ui`, or `pages/*.md`) open from
|
|
79
83
|
any project as `~/.repogo/plugins/<name>/pages/<page>.ui`. A
|
|
@@ -89,7 +93,8 @@ A user-facing plugin needs both its saved page and a link from `~/.repogo/plugin
|
|
|
89
93
|
## Methods
|
|
90
94
|
|
|
91
95
|
```ts
|
|
92
|
-
method(input, run, { effect?, description?, output?,
|
|
96
|
+
method(input, run, { effect?, description?, output?, approve?, approveUpTo?, http? })
|
|
97
|
+
cached(input, run, { fresh, shared?, timeout?, description?, output?, http? })
|
|
93
98
|
stream(input, async function* run, { description? })
|
|
94
99
|
```
|
|
95
100
|
|
|
@@ -101,14 +106,15 @@ JSON can carry.
|
|
|
101
106
|
| --- | --- | --- |
|
|
102
107
|
| `read` | Only reads; writing throws | Cards on their own, and again whenever data it read changes |
|
|
103
108
|
| `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
|
|
109
|
+
| `destructive` | Writes what can't be undone; needs an `approve` | Only after the user answers its question |
|
|
105
110
|
| `stream()` | A read that yields items as they happen | A card while it shows it |
|
|
106
111
|
|
|
107
112
|
Mark every method that only reads `effect: "read"`: that's what lets a card
|
|
108
113
|
show it and keep it current. Mark what can't be undone (a delete, a send, a
|
|
109
|
-
deploy) `effect: "destructive"` with
|
|
110
|
-
without one doesn't load. A method with
|
|
111
|
-
|
|
114
|
+
deploy) `effect: "destructive"` with an `approve` question; a destructive method
|
|
115
|
+
without one doesn't load. A method with an `approve` asks before it runs, from
|
|
116
|
+
a tap or an agent ([Errors and approvals](#errors-and-approvals)), and never
|
|
117
|
+
runs from a cron. Give each a `description`; agents read it to choose
|
|
112
118
|
a method. `output`, a zod schema of the result, tells agents and cards its
|
|
113
119
|
field names.
|
|
114
120
|
|
|
@@ -126,8 +132,9 @@ open: method(z.object({ limit: z.number().int().min(1).max(100).default(20) }),
|
|
|
126
132
|
|
|
127
133
|
A **stream** sends each `yield` to the card as it happens, for as long as the
|
|
128
134
|
card shows it. `ctx.signal` aborts when the card goes, so hand it to what you
|
|
129
|
-
start. Give items an `id`. An agent calling a stream
|
|
130
|
-
5 seconds, at most 200 items
|
|
135
|
+
start. Give items an `id`. An agent calling a stream, or `ctx.call` on one,
|
|
136
|
+
gets `{ items, done }`: what arrives within 5 seconds, at most 200 items, with
|
|
137
|
+
`done` false when it was cut off.
|
|
131
138
|
|
|
132
139
|
```ts
|
|
133
140
|
tail: stream(z.object({ file: z.string() }), async function* ({ file }, ctx) {
|
|
@@ -162,6 +169,87 @@ projects: cached(z.object({ limit: z.number().int().min(1).max(200).default(100)
|
|
|
162
169
|
needs a `namespace`. An answer over 1 MB is returned but not saved, and one
|
|
163
170
|
nobody has asked for in 30 days is deleted.
|
|
164
171
|
|
|
172
|
+
### Serving over HTTP
|
|
173
|
+
|
|
174
|
+
Cards, agents and crons reach every method. A method answers plain HTTP too
|
|
175
|
+
when its `http` option names an address, with who may call it there. `method`
|
|
176
|
+
and `cached` take it; a stream or a method with an `approve` is never served,
|
|
177
|
+
since no one is there to watch or answer.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
today: cached(z.object({}), async () => sales(), { fresh: "5m", http: { device: "none" } }),
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Address | Where | Reached at |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `device` | This machine's loopback and its Apple containers' gateways, port 47444; Docker Desktop's containers reach it through `host.docker.internal` | `http://127.0.0.1:47444/<plugin>/<method>` |
|
|
186
|
+
| `public` | The environment's own name, through the tunnel | `https://h-<host id>.repogo.dev/<plugin>/<method>` |
|
|
187
|
+
|
|
188
|
+
| Access | Who may call |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `key` | Only a caller sending the plugin's key as `Authorization: Bearer <key>`, which the host checks before the method runs |
|
|
191
|
+
| `none` | Anyone who reaches the address. On `device` the network is the boundary; on `public` it's anyone with the URL, since a method doesn't see the request's headers to check a signature |
|
|
192
|
+
|
|
193
|
+
`http: true` is `{ public: "key" }`. An address a method doesn't name answers
|
|
194
|
+
404, as do a method that doesn't exist and a missing or wrong key, so callers
|
|
195
|
+
can't learn which plugins are installed.
|
|
196
|
+
|
|
197
|
+
`repogo urls shop` lists each of a plugin's methods served over HTTP with its
|
|
198
|
+
URLs, filled in with this host's id.
|
|
199
|
+
|
|
200
|
+
Each plugin has one key, for all its methods that take one, at both
|
|
201
|
+
addresses. `repogo keys shop` prints it, making it the first time; running it
|
|
202
|
+
again shows the same key. `repogo keys shop --new` replaces it, and the old
|
|
203
|
+
one stops working at once; `repogo keys shop --delete` leaves the methods
|
|
204
|
+
answering no one. RepoGo's tools for agents and a plugin's `ctx.host` can't
|
|
205
|
+
reach keys, though anything that runs commands as you can run `repogo keys`,
|
|
206
|
+
as it can read your plugins' own secrets. The method itself never sees the key.
|
|
207
|
+
|
|
208
|
+
A POST's JSON body is the input as it is. A GET's query is the input, as
|
|
209
|
+
strings, and only a `read` may be called that way:
|
|
210
|
+
link previews and crawlers fetch any URL they're shown, so a write answers
|
|
211
|
+
405 to a GET. The answer is the result as JSON, or `{ error, fields }` with
|
|
212
|
+
400, 403 or 404 for a `MethodError` with those codes, in its own words. Any
|
|
213
|
+
other error, such as a failed `fetch`, answers 500 `Internal server error`,
|
|
214
|
+
since its text may hold a token or a path; the host logs the real one. The
|
|
215
|
+
device address takes calls only from this machine and its containers'
|
|
216
|
+
networks, and refuses a browser's, since any web page can reach loopback:
|
|
217
|
+
call it by an address, `localhost` or `host.docker.internal`, not another name.
|
|
218
|
+
|
|
219
|
+
A body that isn't JSON answers 400, as does one over 1 MB at the device
|
|
220
|
+
address (413 at the public one), and the public address answers 429 to a
|
|
221
|
+
caller sending too many. A plugin folder that fails to load answers 502. HEAD is a
|
|
222
|
+
GET, and a query key given twice keeps its first value. HTTP calls have no
|
|
223
|
+
project: `ctx.root` is the home folder. `ctx.caller` is `http` for a public call and `device`
|
|
224
|
+
for one at the device address.
|
|
225
|
+
|
|
226
|
+
When an HTTP caller hangs up, or the public address stops waiting after 60
|
|
227
|
+
seconds, `ctx.signal` aborts: hand it to `fetch` and `Bun.spawn` so the work
|
|
228
|
+
stops. The public address runs at most 32 calls at once and answers 503 to
|
|
229
|
+
another; a call counts until its method returns, so one that ignores the
|
|
230
|
+
signal keeps its place until it's done.
|
|
231
|
+
|
|
232
|
+
A method served over HTTP runs with all of `ctx`: one that starts an agent,
|
|
233
|
+
runs an action or calls another method lets its caller do the same. Every
|
|
234
|
+
container on this machine reaches every method open to `device`, and so does
|
|
235
|
+
every account and app on it. Open only what you'd show anyone who can reach
|
|
236
|
+
the address: reads, never a send or a delete.
|
|
237
|
+
|
|
238
|
+
**A plugin elsewhere that stands in for one here.** `GET /<plugin>` at the
|
|
239
|
+
device address lists the methods it serves there with no key, with their
|
|
240
|
+
schemas, and `remotePlugin` builds a plugin from it. In a container, a plugin
|
|
241
|
+
of the same name whose `app.ts` is only this serves the Mac's plugin, and its
|
|
242
|
+
pages, copied or mounted, work unchanged; the code, keys and data stay on the Mac:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { remotePlugin } from "@repogo/plugins";
|
|
246
|
+
export default await remotePlugin("http://192.168.64.1:47444", "shop");
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
It reads the list when the plugin loads, so it fails to load while the Mac
|
|
250
|
+
can't be reached. A page's button for a method the Mac doesn't serve there
|
|
251
|
+
fails, as it would for any missing method.
|
|
252
|
+
|
|
165
253
|
## Data
|
|
166
254
|
|
|
167
255
|
A plugin declares its data in four kinds, each with a schema, and gets typed
|
|
@@ -206,7 +294,9 @@ await ctx.collections.tasks.delete(task.id);
|
|
|
206
294
|
await ctx.collections.tasks.deleteMany({ where: { status: "done" }, offset: 100, limit: 500 }); // done ones past the newest 100, 500 at a time
|
|
207
295
|
```
|
|
208
296
|
|
|
209
|
-
`list` is newest first by default, at most 100 unless `limit` says
|
|
297
|
+
`list` is newest first by default, at most 100 unless `limit` says. The
|
|
298
|
+
generated `list` cards and agents call takes `limit` (up to 500), `offset`,
|
|
299
|
+
`orderBy` and `order` but no `where`: write a `read` method to filter.
|
|
210
300
|
`where` matches top-level fields exactly; `null` matches a missing one.
|
|
211
301
|
`orderBy` is `id`, `createdAt`, `updatedAt` or one of the record's own fields, and
|
|
212
302
|
`offset` passes over that many first. `deleteMany` takes the same options and
|
|
@@ -390,7 +480,7 @@ export default definePlugin({
|
|
|
390
480
|
await ctx.kv.reports.set("latest", report);
|
|
391
481
|
return report;
|
|
392
482
|
});
|
|
393
|
-
}, { timeout: "30d", maxOffline: "14d", retention: "30d" }),
|
|
483
|
+
}, { timeout: "30d", maxOffline: "14d", retention: "30d" }), // the defaults
|
|
394
484
|
},
|
|
395
485
|
kv: { reports: z.object({ title: z.string() }) },
|
|
396
486
|
});
|
|
@@ -398,7 +488,7 @@ export default definePlugin({
|
|
|
398
488
|
|
|
399
489
|
For a plugin folder named `reports`, the host generates:
|
|
400
490
|
|
|
401
|
-
- `reports.report.list({ limit?, status?, cursor? })`: bounded summaries and a next cursor.
|
|
491
|
+
- `reports.report.list({ limit?, status?, cursor? })`: bounded summaries (20 unless `limit` says, at most 100) and a next cursor. A status is `pending`, `running`, `waiting`, `attention`, `completed`, `failed`, `cancelled`, `expired`, `unknown` or `blocked`.
|
|
402
492
|
- `reports.report.get({ id })`: one summary, input, result, error and the latest 20 operations (steps and sleeps).
|
|
403
493
|
- `reports.report.start({ input, key? })`: persist and queue a run; returns its summary immediately.
|
|
404
494
|
- `reports.report.cancel({ id })`: persist cancellation intent and abort active work.
|
|
@@ -407,9 +497,9 @@ For a plugin folder named `reports`, the host generates:
|
|
|
407
497
|
- `reports.report.restart({ id })`: start a run that needs attention over from the beginning, on the current code.
|
|
408
498
|
|
|
409
499
|
These use the existing plugin methods, `ctx.call`, cards and subscriptions.
|
|
410
|
-
The namespace is required. `
|
|
500
|
+
The namespace is required. `approve` on the workflow asks before its start;
|
|
411
501
|
it does not preapprove later host actions. An unattended workflow refuses
|
|
412
|
-
|
|
502
|
+
plugin methods that ask. The calling project and caller
|
|
413
503
|
are preserved. A start `key` deduplicates within that project, plugin and
|
|
414
504
|
workflow while its run is retained; reusing it with different input fails.
|
|
415
505
|
|
|
@@ -441,8 +531,10 @@ with `kind: "sleep"`. Terminal runs have no pending wake time.
|
|
|
441
531
|
`attempts` counts the first invocation and defaults to 1. Only steps declared
|
|
442
532
|
`kind: "query"` may automatically retry; they cannot write SDK data or call
|
|
443
533
|
mutations. Within query steps, use `ctx.mcp` for read-aware host access;
|
|
444
|
-
generic `ctx.host`, notifications and agent starts are refused.
|
|
445
|
-
|
|
534
|
+
generic `ctx.host`, notifications and agent starts are refused. A step's `kind`
|
|
535
|
+
defaults to `"mutation"`, and `attempts` is at most 100. Query backoff,
|
|
536
|
+
`backoff: { initial, factor, max }`, defaults to full jitter with initial `2s`,
|
|
537
|
+
factor 2 and maximum `1m`. Import `NonRetryable` and throw it for a permanent failure.
|
|
446
538
|
Step `timeout` defaults to `5m`. The callback receives `signal`, `attempt`,
|
|
447
539
|
`attempts`, `runId`, `stepId` and an `idempotencyKey` stable across its attempts.
|
|
448
540
|
Pass its signal to fetch or subprocess APIs. A timeout aborts cooperatively;
|
|
@@ -513,7 +605,7 @@ itself. Each line names a plugin, one of its methods and a schedule:
|
|
|
513
605
|
|
|
514
606
|
| Field | What |
|
|
515
607
|
| --- | --- |
|
|
516
|
-
| `plugin`, `method` | The plugin's folder name and one of
|
|
608
|
+
| `plugin`, `method` | The plugin's folder name and one of the methods it wrote (not a generated or workflow one), called with `{}` as a tap would |
|
|
517
609
|
| `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 |
|
|
518
610
|
| `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 |
|
|
519
611
|
| `timezone` | Defaults to the environment's |
|
|
@@ -541,10 +633,10 @@ that were running when it happened.
|
|
|
541
633
|
|
|
542
634
|
Write `console.log` (or `warn`, `error`, `debug`); nothing else is needed. The
|
|
543
635
|
host keeps every line with the plugin, the method and the call that wrote it,
|
|
544
|
-
who called (`card`, `agent`, `cron`), and the chat for an agent's call, plus
|
|
636
|
+
who called (`card`, `agent`, `cron`, `code`, or `http` and `device` for HTTP), and the chat for an agent's call, plus
|
|
545
637
|
the arguments as JSON. An error a method throws, or one nothing caught, is
|
|
546
|
-
kept too, with its stack. Read them with the agent's `plugins_logs` tool, or
|
|
547
|
-
a
|
|
638
|
+
kept too, with its stack. Read them with the agent's `plugins_logs` tool, or
|
|
639
|
+
from a plugin with `ctx.host("plugins.logs", …)` for a card to show:
|
|
548
640
|
`plugin`, `level` (that level and worse), `method`, `since`, `until`, `query`
|
|
549
641
|
(a substring) and `limit` narrow it. The `plugins` tool shows each plugin's
|
|
550
642
|
`errors` in the last hour. Lines older than 7 days go, and the oldest hours of
|
|
@@ -558,18 +650,18 @@ Every method, handler and cron gets `ctx`:
|
|
|
558
650
|
| Field | What |
|
|
559
651
|
| --- | --- |
|
|
560
652
|
| `plugin` | The plugin's name |
|
|
561
|
-
| `caller` | Who started the call: `card`, `agent`, `cron`, or `
|
|
653
|
+
| `caller` | Who started the call: `card`, `agent`, `cron`, `code`, or a request at the public (`http`) or device (`device`) address |
|
|
562
654
|
| `root` | The calling project's checkout |
|
|
563
655
|
| `collections`, `kv`, `streams`, `buckets` | The plugin's data |
|
|
564
656
|
| `call(method, input)` | Runs a method of a plugin in the same folder; `"open"` is this plugin's own |
|
|
565
657
|
| `plugins()` | The plugins in the same folder, with their methods |
|
|
566
|
-
| `host(method, params)` | Runs one of the host's methods that agents
|
|
658
|
+
| `host(method, params)` | Runs one of the host's methods that agents may call, and `actions.*` (`git.status`, `chats.list`, `actions.run`, …); others fail with `FORBIDDEN`, and one that doesn't exist with `INTERNAL`. It's how a host read reaches a card, which can't call the host's methods itself |
|
|
567
659
|
| `mcp(server, tool, input)` | Runs a tool of an MCP server the user connected in RepoGo and switched on for the calling project; see below |
|
|
568
660
|
| `notify({ title, body, page?, url?, collapse? })` | Alerts every paired phone; a tap opens `url` (an `https` or `itms-services` link) if set, else `page`, and alerts sharing `collapse` replace each other |
|
|
569
661
|
| `activity({ id, title, body?, page?, symbol?, tint?, label?, compactLabel?, progress?, timer?, status? })` | Shows or updates a Live Activity on every paired phone; see below |
|
|
570
662
|
| `agent(prompt, { agent, project?, title?, timeout? })` | Starts a chat with `prompt` and waits for the agent's whole answer: `{ chatId, text }`; see below |
|
|
571
663
|
| `emit(event, payload)` | Sends `<plugin>.<event>` to handlers |
|
|
572
|
-
| `signal` | Aborts when a stream's card goes |
|
|
664
|
+
| `signal` | Aborts when a stream's card goes, or when an HTTP caller leaves before a read or write answers |
|
|
573
665
|
| `log(...)` | The same as `console.log` |
|
|
574
666
|
|
|
575
667
|
A query doesn't re-run when what a `ctx.host` method read changes.
|
|
@@ -650,7 +742,7 @@ customers: method(z.object({}), async (_, ctx) => {
|
|
|
650
742
|
```
|
|
651
743
|
|
|
652
744
|
A read may run only the server's read-only tools, as a card's `Data` may. A
|
|
653
|
-
tool marked destructive runs only from a method with
|
|
745
|
+
tool marked destructive runs only from a method with an `approve`, or one
|
|
654
746
|
called from it with `ctx.call`: the user's answer to that question is its
|
|
655
747
|
yes. A refusal throws `FORBIDDEN`; a tool's own error throws `INTERNAL` with
|
|
656
748
|
its message. Only servers switched on for `ctx.root` answer, so a cron of an
|
|
@@ -674,7 +766,9 @@ on: {
|
|
|
674
766
|
| Bucket | `<name>.created`, `<name>.changed` (the file's bytes), `<name>.metaChanged`, `<name>.deleted` (the file) |
|
|
675
767
|
| Any method | `<method>` (`{ input, result }`) |
|
|
676
768
|
|
|
677
|
-
Changes to a bucket's folder made by hand emit too, once the host sees them
|
|
769
|
+
Changes to a bucket's folder made by hand emit too, once the host sees them;
|
|
770
|
+
streams emit none. A plugin's `on` hears only its own events, and runs inside
|
|
771
|
+
the call that emitted them, so a slow handler delays that call's answer.
|
|
678
772
|
Events run in memory: none is retried, and a handler that throws is logged.
|
|
679
773
|
|
|
680
774
|
## How cards and agents reach a plugin
|
|
@@ -683,7 +777,7 @@ Events run in memory: none is retried, and a handler that throws is logged.
|
|
|
683
777
|
(its effect, whether it asks first, and its input's fields, optional ones ending
|
|
684
778
|
in `?`), and every line of `~/.repogo/crons.json` with how it's going.
|
|
685
779
|
`plugins` with `plugin` set answers that plugin in full: each method's input
|
|
686
|
-
and output JSON Schema and
|
|
780
|
+
and output JSON Schema, `approve` and `approveUpTo`, its `storage` (each bucket's and stream's
|
|
687
781
|
folder), the pages that show its methods, and the cron lines naming it.
|
|
688
782
|
`plugin_call` runs one. Inspect a method's schema before naming it in a card.
|
|
689
783
|
Verify a read's fields with a read-only call. Do not execute writes just to
|
|
@@ -751,20 +845,47 @@ Inside plugin code, `ctx.kv.flags.set("alerts", true)` saves the same value.
|
|
|
751
845
|
The generated `preferences.flags.get({ key: "alerts" })` returns an entry with
|
|
752
846
|
`value` and timestamps, while `ctx.kv.flags.get("alerts")` returns the value alone.
|
|
753
847
|
|
|
754
|
-
## Errors and
|
|
848
|
+
## Errors and approvals
|
|
849
|
+
|
|
850
|
+
Throw `new MethodError(code, message, fieldErrors?)` for a failure the caller
|
|
851
|
+
should read: `VALIDATION`, `NOT_FOUND`, `FORBIDDEN` or `INTERNAL`.
|
|
852
|
+
`fieldErrors` maps a JSON pointer (`"/minutes"`) to its message, and reaches an
|
|
853
|
+
HTTP caller as `fields`. Input that fails its schema answers `VALIDATION` with
|
|
854
|
+
a message per field. Anything else thrown is
|
|
855
|
+
`INTERNAL` with its message. An HTTP caller gets only the first three in their
|
|
856
|
+
words; `INTERNAL` reaches it as `Internal server error`
|
|
857
|
+
([Serving over HTTP](#serving-over-http)).
|
|
858
|
+
|
|
859
|
+
A method's `approve` is the question asked before it runs. `{{field}}` in it
|
|
860
|
+
is filled from the call's input, so it can say what it acts on:
|
|
861
|
+
`approve: "Restart {{machine}}?"`. A `destructive` method must have one.
|
|
862
|
+
|
|
863
|
+
- **A card's tap** always asks it first.
|
|
864
|
+
- **An agent's `plugin_call`** asks it in the chat, as a tool approval, and runs
|
|
865
|
+
the method only on the user's yes. `approveUpTo` names the least strict chat
|
|
866
|
+
mode that still asks; the method asks in that mode and every stricter one,
|
|
867
|
+
and runs unasked in the rest. The modes, strictest first, are
|
|
868
|
+
`approval-required`, `auto-accept-edits` and `full-access`; unset is
|
|
869
|
+
`full-access`, so it asks in every mode. A chat whose mode the host can't
|
|
870
|
+
tell asks.
|
|
871
|
+
- **Plan mode:** an agent's call to any method that isn't a `read` is refused,
|
|
872
|
+
with or without an `approve`.
|
|
873
|
+
- **Code:** `ctx.call` from plugin code skips it, and an unattended workflow
|
|
874
|
+
or cron refuses methods that ask.
|
|
755
875
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
876
|
+
```ts
|
|
877
|
+
upload: method(Build, run, {
|
|
878
|
+
effect: "destructive",
|
|
879
|
+
approve: "Upload build {{buildNumber}}?", // asks in every mode
|
|
880
|
+
}),
|
|
881
|
+
rename: method(Rename, run, {
|
|
882
|
+
approve: "Rename {{name}}?",
|
|
883
|
+
approveUpTo: "auto-accept-edits", // full access runs it unasked
|
|
884
|
+
}),
|
|
885
|
+
```
|
|
760
886
|
|
|
761
|
-
|
|
762
|
-
|
|
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
|
|
766
|
-
filled from the call's input, so it can say what it acts on:
|
|
767
|
-
`confirm: "Restart {{machine}}?"`.
|
|
887
|
+
In full access an agent can run commands without asking, so there the
|
|
888
|
+
question catches a careless call rather than stopping a determined one.
|
|
768
889
|
|
|
769
890
|
## Testing
|
|
770
891
|
|
|
@@ -776,12 +897,16 @@ user's MCP servers:
|
|
|
776
897
|
import { testRuntime } from "@repogo/plugins/testing";
|
|
777
898
|
import notes from "./app";
|
|
778
899
|
|
|
779
|
-
const app = await testRuntime({ notes });
|
|
780
|
-
await app.call("notes.
|
|
900
|
+
const app = await testRuntime({ notes }); // { root?, mcp?, host? } as a second argument
|
|
901
|
+
await app.call("notes.notes.create", { text: "hi" });
|
|
781
902
|
await app.call("notes.clear", {}, { confirmed: false }); // throws CONFIRM, as an unanswered tap
|
|
782
|
-
app.setCrons([{ plugin: "notes", method: "
|
|
903
|
+
app.setCrons([{ plugin: "notes", method: "open", schedule: "* * * * *" }]); // a crons.json line, with root for project
|
|
783
904
|
await app.tickCrons(Date.now() + 60_000); // runs whatever is due by then
|
|
784
905
|
app.notices; // every ctx.notify so far
|
|
785
906
|
app.logs; // every console line so far, with its method and caller
|
|
907
|
+
app.manifest(); // the plugins and methods, as the host lists them
|
|
786
908
|
await app.close();
|
|
787
909
|
```
|
|
910
|
+
|
|
911
|
+
`call` takes `{ confirmed?, caller?, root?, chat?, files? }`; leaving
|
|
912
|
+
`confirmed` out runs a `confirm` method without asking, as `ctx.call` does.
|
package/dist/context.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ 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 { Effect,
|
|
5
|
+
import type { Effect, HTTPAddresses, PermissionMode } 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 = {
|
|
9
9
|
title: string;
|
|
10
10
|
body: string;
|
|
11
|
-
/** A page target,
|
|
11
|
+
/** A page target, `~/.repogo/plugins/vercel/pages/index.ui?project=a`; a tap opens it. */
|
|
12
12
|
page?: string;
|
|
13
13
|
/** An `https` or `itms-services` link a tap opens instead of the page, such as a build's install. */
|
|
14
14
|
url?: string;
|
|
@@ -53,7 +53,7 @@ export type Activity = {
|
|
|
53
53
|
title: string;
|
|
54
54
|
/** At most 120 characters, as a notice's body. */
|
|
55
55
|
body?: string;
|
|
56
|
-
/** A page target
|
|
56
|
+
/** A page target, `~/.repogo/plugins/deploys/pages/index.ui?project=web`; a tap opens it. */
|
|
57
57
|
page?: string;
|
|
58
58
|
/** An SF Symbol name, `shippingbox`; `circle.dotted`, or a check or a cross once ended, unless said. */
|
|
59
59
|
symbol?: string;
|
|
@@ -79,9 +79,11 @@ export type ManifestMethod = {
|
|
|
79
79
|
stream: boolean;
|
|
80
80
|
description: string | null;
|
|
81
81
|
/** What a card asks before running it; null runs on tap. */
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
|
|
82
|
+
approve: string | null;
|
|
83
|
+
/** The least strict chat mode an agent's call still asks in; null with no approve. */
|
|
84
|
+
approveUpTo: PermissionMode | null;
|
|
85
|
+
/** Who may call it over HTTP, at each address; null, no one anywhere. */
|
|
86
|
+
http: HTTPAddresses | null;
|
|
85
87
|
/** JSON Schema of the input, and of the output when the method declares one. */
|
|
86
88
|
input: unknown;
|
|
87
89
|
output: unknown;
|
|
@@ -139,8 +141,8 @@ export type Manifest = {
|
|
|
139
141
|
storage: ManifestStorage;
|
|
140
142
|
}[];
|
|
141
143
|
};
|
|
142
|
-
/** Who started a call: a tap or a card's query, an agent's `plugin_call`, a cron,
|
|
143
|
-
export type Caller = "card" | "agent" | "cron" | "code" | "http";
|
|
144
|
+
/** Who started a call: a tap or a card's query, an agent's `plugin_call`, a cron, plugin code with none of those behind it, or a request at the public (`http`) or device address. */
|
|
145
|
+
export type Caller = "card" | "agent" | "cron" | "code" | "http" | "device";
|
|
144
146
|
/** How `ctx.agent` runs a chat: which agent, in which project, and how long to wait for its answer. */
|
|
145
147
|
export type AgentOptions = {
|
|
146
148
|
/** `claude` or `codex`, as `agents.list` names them. */
|
|
@@ -177,15 +179,15 @@ export type Context = {
|
|
|
177
179
|
* Runs a tool of an MCP server the user switched on for the calling project:
|
|
178
180
|
* `ctx.mcp("stripe", "stripe_api_read", { … })`, answering the tool's result.
|
|
179
181
|
* A query may run only read-only tools; a destructive tool runs only from a
|
|
180
|
-
* method with
|
|
182
|
+
* method with an `approve`. Its answer doesn't re-run a query.
|
|
181
183
|
*/
|
|
182
184
|
mcp: (server: string, tool: string, input?: unknown) => Promise<unknown>;
|
|
183
185
|
/**
|
|
184
|
-
* The plugins beside this one
|
|
185
|
-
*
|
|
186
|
+
* The plugins beside this one in the environment's plugins folder, with
|
|
187
|
+
* their methods: what `call` can reach.
|
|
186
188
|
*/
|
|
187
189
|
plugins: () => Manifest["plugins"];
|
|
188
|
-
/** Aborts when a stream's last watcher leaves
|
|
190
|
+
/** Aborts when a stream's last watcher leaves, or when an HTTP caller of a read or write goes away first. */
|
|
189
191
|
signal: AbortSignal;
|
|
190
192
|
/** Sends an alert to every phone paired with this environment. */
|
|
191
193
|
notify: (notice: Notice) => void;
|
package/dist/define.d.ts
CHANGED
|
@@ -13,10 +13,16 @@ export type StreamDefinition = z.ZodType | {
|
|
|
13
13
|
/**
|
|
14
14
|
* What a method does. `read` only reads, so cards may run it on their own and
|
|
15
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 (`
|
|
16
|
+
* user acts. `destructive` can't be undone, and must ask first (`approve`).
|
|
17
17
|
* Unmarked methods write: repeating one is never assumed safe.
|
|
18
18
|
*/
|
|
19
19
|
export type Effect = "read" | "write" | "destructive";
|
|
20
|
+
/**
|
|
21
|
+
* The chat's permission modes, strictest first, as RepoGo names them for every
|
|
22
|
+
* agent. `approveUpTo` names the least strict one a method still asks in.
|
|
23
|
+
*/
|
|
24
|
+
export declare const permissionModes: readonly ["approval-required", "auto-accept-edits", "full-access"];
|
|
25
|
+
export type PermissionMode = (typeof permissionModes)[number];
|
|
20
26
|
export type Method<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> = {
|
|
21
27
|
input: I;
|
|
22
28
|
/** What it returns; the agent and cards read field names from it. */
|
|
@@ -29,18 +35,34 @@ export type Method<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodT
|
|
|
29
35
|
* The question asked before it runs, such as "Delete {{name}}?", filled from
|
|
30
36
|
* its input; required for a destructive method. Methods without one run on tap.
|
|
31
37
|
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
38
|
+
approve?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The least strict chat mode in which an agent's call still asks `approve`:
|
|
41
|
+
* it asks in that mode and every stricter one, and runs unasked in the rest.
|
|
42
|
+
* Unset is "full-access", every mode. A card's tap always asks.
|
|
43
|
+
*/
|
|
44
|
+
approveUpTo?: PermissionMode;
|
|
45
|
+
/** Who may call it over HTTP, at each address; an address it doesn't name serves it to no one. */
|
|
46
|
+
http?: HTTPAddresses;
|
|
35
47
|
/** Set by `cached`: the read answers from what it saved, and checks again when that's old. */
|
|
36
48
|
cache?: CacheOptions;
|
|
37
49
|
run: (input: z.infer<I>, ctx: Context) => unknown | Promise<unknown>;
|
|
38
50
|
};
|
|
39
51
|
/**
|
|
40
|
-
* `key`: the
|
|
41
|
-
* method
|
|
52
|
+
* `key`: the caller sends the plugin's key as a bearer token, which the host
|
|
53
|
+
* checks before the method runs. `none`: anyone who reaches the address may call.
|
|
42
54
|
*/
|
|
43
55
|
export type HTTPAccess = "key" | "none";
|
|
56
|
+
/**
|
|
57
|
+
* `public`: the environment's own address, through the tunnel. `device`: this
|
|
58
|
+
* machine's loopback and its containers' gateway, where the network is the boundary.
|
|
59
|
+
*/
|
|
60
|
+
export type HTTPAddresses = {
|
|
61
|
+
device?: HTTPAccess;
|
|
62
|
+
public?: HTTPAccess;
|
|
63
|
+
};
|
|
64
|
+
/** `true` is `{ public: "key" }`. */
|
|
65
|
+
export type HTTPOption = true | HTTPAddresses;
|
|
44
66
|
export type Handler = (payload: any, ctx: Context) => unknown | Promise<unknown>;
|
|
45
67
|
export type PluginDefinition = {
|
|
46
68
|
/** One line, shown to the agent in the index of plugins. */
|
|
@@ -72,15 +94,14 @@ export declare function definePlugin<T extends PluginDefinition>(plugin: T): T;
|
|
|
72
94
|
type MethodOptions<O extends z.ZodType> = {
|
|
73
95
|
output?: O;
|
|
74
96
|
description?: string;
|
|
75
|
-
http?:
|
|
76
|
-
|
|
77
|
-
};
|
|
97
|
+
http?: HTTPOption;
|
|
98
|
+
approveUpTo?: PermissionMode;
|
|
78
99
|
} & ({
|
|
79
100
|
effect: "destructive";
|
|
80
|
-
|
|
101
|
+
approve: string;
|
|
81
102
|
} | {
|
|
82
103
|
effect?: "read" | "write";
|
|
83
|
-
|
|
104
|
+
approve?: string;
|
|
84
105
|
});
|
|
85
106
|
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>;
|
|
86
107
|
/**
|
|
@@ -97,6 +118,7 @@ export declare function cached<I extends z.ZodType, O extends z.ZodType = z.ZodT
|
|
|
97
118
|
timeout?: Duration;
|
|
98
119
|
output?: O;
|
|
99
120
|
description?: string;
|
|
121
|
+
http?: HTTPOption;
|
|
100
122
|
}): Method<I, O>;
|
|
101
123
|
/**
|
|
102
124
|
* A method that sends items as they happen: a log tail, a build's output. Each
|
package/dist/generated.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type Generated = {
|
|
|
9
9
|
description: string;
|
|
10
10
|
input: z.ZodType;
|
|
11
11
|
output?: z.ZodType;
|
|
12
|
-
|
|
12
|
+
approve?: string;
|
|
13
13
|
run: (input: any, signal: AbortSignal) => unknown;
|
|
14
14
|
};
|
|
15
15
|
export declare function kvMethods(name: string, kv: Kv<any>, schema: z.ZodType): Record<string, Generated>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { cached, definePlugin, method, stream } from "./define";
|
|
2
|
+
export { remotePlugin } from "./remote";
|
|
2
3
|
export { upload, type UploadedFile } from "./uploads";
|
|
3
|
-
export type { PluginDefinition, Method, Effect, HTTPAccess, StreamDefinition } from "./define";
|
|
4
|
+
export type { PluginDefinition, Method, Effect, PermissionMode, HTTPAccess, HTTPAddresses, HTTPOption, StreamDefinition } from "./define";
|
|
4
5
|
export type { Activity, ActivityProgress, ActivityTimer, ActivityTint, AgentOptions, AgentReply, Caller, Context, CronEntry, CronStatus, Manifest, ManifestCron, ManifestMethod, ManifestStorage } from "./context";
|
|
5
6
|
export type { LogLevel, LogRecord } from "./logs";
|
|
6
7
|
export type { BucketFile, FileStatus, PutFile } from "./buckets";
|