@repogo/plugins 0.1.42 → 0.1.44
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 +213 -75
- package/dist/context.d.ts +20 -14
- package/dist/define.d.ts +54 -24
- package/dist/generated.d.ts +4 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +66 -20
- package/dist/remote.d.ts +7 -0
- package/dist/runtime.d.ts +15 -10
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +76 -60
- 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 +139 -83
- 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):
|
|
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,28 +16,28 @@ 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
|
|
22
23
|
|
|
23
24
|
```ts
|
|
24
|
-
//
|
|
25
|
+
// ~/.repogo/plugins/notes/app.ts
|
|
25
26
|
import { definePlugin, method, z } from "@repogo/plugins";
|
|
26
27
|
|
|
27
28
|
export default definePlugin({
|
|
28
|
-
description: "Notes for
|
|
29
|
-
namespace: "
|
|
29
|
+
description: "Notes for any project",
|
|
30
|
+
namespace: "notes",
|
|
30
31
|
collections: { notes: z.object({ text: z.string(), done: z.boolean().default(false) }) },
|
|
31
32
|
methods: {
|
|
32
33
|
open: method(z.object({}), async (_, ctx) => ctx.collections.notes.list({ where: { done: false } }), {
|
|
33
|
-
|
|
34
|
+
effect: "read",
|
|
34
35
|
description: "Notes not done yet, newest first",
|
|
35
36
|
}),
|
|
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
|
-
}, {
|
|
40
|
+
}, { effect: "destructive", approve: "Delete every note?" }),
|
|
40
41
|
},
|
|
41
42
|
});
|
|
42
43
|
```
|
|
@@ -46,9 +47,9 @@ a minute; at once when something calls it), and it gets:
|
|
|
46
47
|
|
|
47
48
|
- `notes.open` and `notes.clear`, the methods it wrote;
|
|
48
49
|
- `notes.notes.list`, `.get`, `.create`, `.update` and `.delete`, generated for its collection;
|
|
49
|
-
- a `data.db` in `~/.repogo/plugin-data/
|
|
50
|
+
- a `data.db` in `~/.repogo/plugin-data/notes/`.
|
|
50
51
|
|
|
51
|
-
Add the package to
|
|
52
|
+
Add the package to `~/.repogo/plugins/package.json` (`bun add @repogo/plugins`) for
|
|
52
53
|
types and editor help. The host runs plugins with its own copy of this package,
|
|
53
54
|
the version it shipped with, so a plugin always matches its host. Plugins run on
|
|
54
55
|
[Bun](https://bun.sh); install it on the environment.
|
|
@@ -57,30 +58,43 @@ the version it shipped with, so a plugin always matches its host. Plugins run on
|
|
|
57
58
|
|
|
58
59
|
| Folder | For | `ctx.root` |
|
|
59
60
|
| --- | --- | --- |
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
| `~/.repogo/plugins/<name>/app.ts` | Every project | The project that called it |
|
|
62
|
+
|
|
63
|
+
Plugins live only on the environment, in folders the user put there. A project
|
|
64
|
+
can't ship plugin code: the host never imports a repo's `.repogo/plugins`, so
|
|
65
|
+
cloning or opening a project never runs anything in it. A plugin about one
|
|
66
|
+
project (its keys, its database, its plans) is still an environment plugin: it
|
|
67
|
+
reads that project from `ctx.root`, and says so instead of acting when it's
|
|
68
|
+
called from a project it doesn't fit.
|
|
69
|
+
|
|
70
|
+
A project ships no pages either: the app loads `.ui` and `.html` pages only
|
|
71
|
+
from `~/.repogo/plugins`, so a cloned repo can't put its own page, and the
|
|
72
|
+
script an `.html` page runs, in front of the user. A page about one project
|
|
73
|
+
lives beside its plugin and takes the project as state (`?project=…`).
|
|
74
|
+
|
|
75
|
+
A plugin's name is its folder's: lowercase letters, digits and hyphens. The
|
|
76
|
+
environment's plugins run together in one Worker, so a plugin's blocking code
|
|
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.
|
|
67
81
|
|
|
68
82
|
Pages beside an environment plugin (`pages/*.ui`, or `pages/*.md`) open from
|
|
69
83
|
any project as `~/.repogo/plugins/<name>/pages/<page>.ui`. A
|
|
70
84
|
`.ui` page is card markup with no fence; a `.md` page is writing with cards.
|
|
71
85
|
|
|
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
|
|
86
|
+
In the full iOS app, `~/.repogo/plugins/plugin.ui` is the Plugins tab in
|
|
87
|
+
every project; a project's `.repogo/plugin.ui` is never read. If it doesn't
|
|
88
|
+
exist, the tab is absent. It is plain card markup without a code fence. Read and preserve existing
|
|
76
89
|
entries, and link to plugin pages with `page:` actions.
|
|
77
90
|
|
|
78
|
-
A user-facing plugin needs both its saved page and a link from
|
|
91
|
+
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
92
|
|
|
80
93
|
## Methods
|
|
81
94
|
|
|
82
95
|
```ts
|
|
83
|
-
method(input, run, {
|
|
96
|
+
method(input, run, { effect?, description?, output?, approve?, approveUpTo?, http? })
|
|
97
|
+
cached(input, run, { fresh, shared?, timeout?, description?, output?, http? })
|
|
84
98
|
stream(input, async function* run, { description? })
|
|
85
99
|
```
|
|
86
100
|
|
|
@@ -88,14 +102,19 @@ stream(input, async function* run, { description? })
|
|
|
88
102
|
filled in before `run` sees it. `run(input, ctx)` returns the result: anything
|
|
89
103
|
JSON can carry.
|
|
90
104
|
|
|
91
|
-
|
|
|
105
|
+
| Effect | Does | Who runs it |
|
|
92
106
|
| --- | --- | --- |
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
| `read` | Only reads; writing throws | Cards on their own, and again whenever data it read changes |
|
|
108
|
+
| `write` (the default) | Reads and writes | Only when someone acts: a tap, an agent, a cron, other code |
|
|
109
|
+
| `destructive` | Writes what can't be undone; needs an `approve` | Only after the user answers its question |
|
|
110
|
+
| `stream()` | A read that yields items as they happen | A card while it shows it |
|
|
111
|
+
|
|
112
|
+
Mark every method that only reads `effect: "read"`: that's what lets a card
|
|
113
|
+
show it and keep it current. Mark what can't be undone (a delete, a send, a
|
|
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
|
|
99
118
|
a method. `output`, a zod schema of the result, tells agents and cards its
|
|
100
119
|
field names.
|
|
101
120
|
|
|
@@ -108,13 +127,14 @@ or the voice agent reads the whole of one.
|
|
|
108
127
|
```ts
|
|
109
128
|
open: method(z.object({ limit: z.number().int().min(1).max(100).default(20) }), async ({ limit }, ctx) =>
|
|
110
129
|
(await ctx.collections.notes.list({ where: { done: false }, limit })).map(({ id, text }) => ({ id, text: text.slice(0, 200) })),
|
|
111
|
-
{
|
|
130
|
+
{ effect: "read", description: "Notes not done yet, newest first" }),
|
|
112
131
|
```
|
|
113
132
|
|
|
114
133
|
A **stream** sends each `yield` to the card as it happens, for as long as the
|
|
115
134
|
card shows it. `ctx.signal` aborts when the card goes, so hand it to what you
|
|
116
|
-
start. Give items an `id`. An agent calling a stream
|
|
117
|
-
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.
|
|
118
138
|
|
|
119
139
|
```ts
|
|
120
140
|
tail: stream(z.object({ file: z.string() }), async function* ({ file }, ctx) {
|
|
@@ -142,13 +162,93 @@ projects: cached(z.object({ limit: z.number().int().min(1).max(200).default(100)
|
|
|
142
162
|
|
|
143
163
|
- Answers are kept per method, input and project (`ctx.root`). `shared: true`
|
|
144
164
|
keeps one for every project, for answers that don't depend on it.
|
|
145
|
-
- `run` is read-only, like
|
|
165
|
+
- `run` is read-only, like any read's, and has `timeout` (a minute unless it says).
|
|
146
166
|
- A background check that fails keeps the saved answer, goes to the plugin's
|
|
147
167
|
logs, and is tried again after another `fresh`.
|
|
148
168
|
- Answers live in the namespace's `data.db`, so a plugin with a cached method
|
|
149
169
|
needs a `namespace`. An answer over 1 MB is returned but not saved, and one
|
|
150
170
|
nobody has asked for in 30 days is deleted.
|
|
151
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
|
+
|
|
218
|
+
A body that isn't JSON answers 400, as does one over 1 MB at the device
|
|
219
|
+
address (413 at the public one), and the public address answers 429 to a
|
|
220
|
+
caller sending too many. A plugin folder that fails to load answers 502. HEAD is a
|
|
221
|
+
GET, and a query key given twice keeps its first value. HTTP calls have no
|
|
222
|
+
project: `ctx.root` is the home folder. `ctx.caller` is `http` for a public call and `device`
|
|
223
|
+
for one at the device address.
|
|
224
|
+
|
|
225
|
+
When an HTTP caller hangs up, or the public address stops waiting after 60
|
|
226
|
+
seconds, `ctx.signal` aborts: hand it to `fetch` and `Bun.spawn` so the work
|
|
227
|
+
stops. The public address runs at most 32 calls at once and answers 503 to
|
|
228
|
+
another; a call counts until its method returns, so one that ignores the
|
|
229
|
+
signal keeps its place until it's done.
|
|
230
|
+
|
|
231
|
+
A method served over HTTP runs with all of `ctx`: one that starts an agent,
|
|
232
|
+
runs an action or calls another method lets its caller do the same. Every
|
|
233
|
+
container on this machine reaches every method open to `device`, and so does
|
|
234
|
+
every account and app on it. Open only what you'd show anyone who can reach
|
|
235
|
+
the address: reads, never a send or a delete.
|
|
236
|
+
|
|
237
|
+
**A plugin elsewhere that stands in for one here.** `GET /<plugin>` at the
|
|
238
|
+
device address lists the methods it serves there with no key, with their
|
|
239
|
+
schemas, and `remotePlugin` builds a plugin from it. In a container, a plugin
|
|
240
|
+
of the same name whose `app.ts` is only this serves the Mac's plugin, and its
|
|
241
|
+
pages, copied or mounted, work unchanged; the code, keys and data stay on the Mac:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { remotePlugin } from "@repogo/plugins";
|
|
245
|
+
export default await remotePlugin("http://192.168.64.1:47444", "shop");
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
It reads the list when the plugin loads, so it fails to load while the Mac
|
|
249
|
+
can't be reached. A page's button for a method the Mac doesn't serve there
|
|
250
|
+
fails, as it would for any missing method.
|
|
251
|
+
|
|
152
252
|
## Data
|
|
153
253
|
|
|
154
254
|
A plugin declares its data in four kinds, each with a schema, and gets typed
|
|
@@ -193,7 +293,9 @@ await ctx.collections.tasks.delete(task.id);
|
|
|
193
293
|
await ctx.collections.tasks.deleteMany({ where: { status: "done" }, offset: 100, limit: 500 }); // done ones past the newest 100, 500 at a time
|
|
194
294
|
```
|
|
195
295
|
|
|
196
|
-
`list` is newest first by default, at most 100 unless `limit` says
|
|
296
|
+
`list` is newest first by default, at most 100 unless `limit` says. The
|
|
297
|
+
generated `list` cards and agents call takes `limit` (up to 500), `offset`,
|
|
298
|
+
`orderBy` and `order` but no `where`: write a `read` method to filter.
|
|
197
299
|
`where` matches top-level fields exactly; `null` matches a missing one.
|
|
198
300
|
`orderBy` is `id`, `createdAt`, `updatedAt` or one of the record's own fields, and
|
|
199
301
|
`offset` passes over that many first. `deleteMany` takes the same options and
|
|
@@ -377,7 +479,7 @@ export default definePlugin({
|
|
|
377
479
|
await ctx.kv.reports.set("latest", report);
|
|
378
480
|
return report;
|
|
379
481
|
});
|
|
380
|
-
}, { timeout: "30d", maxOffline: "14d", retention: "30d" }),
|
|
482
|
+
}, { timeout: "30d", maxOffline: "14d", retention: "30d" }), // the defaults
|
|
381
483
|
},
|
|
382
484
|
kv: { reports: z.object({ title: z.string() }) },
|
|
383
485
|
});
|
|
@@ -385,7 +487,7 @@ export default definePlugin({
|
|
|
385
487
|
|
|
386
488
|
For a plugin folder named `reports`, the host generates:
|
|
387
489
|
|
|
388
|
-
- `reports.report.list({ limit?, status?, cursor? })`: bounded summaries and a next cursor.
|
|
490
|
+
- `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`.
|
|
389
491
|
- `reports.report.get({ id })`: one summary, input, result, error and the latest 20 operations (steps and sleeps).
|
|
390
492
|
- `reports.report.start({ input, key? })`: persist and queue a run; returns its summary immediately.
|
|
391
493
|
- `reports.report.cancel({ id })`: persist cancellation intent and abort active work.
|
|
@@ -394,9 +496,9 @@ For a plugin folder named `reports`, the host generates:
|
|
|
394
496
|
- `reports.report.restart({ id })`: start a run that needs attention over from the beginning, on the current code.
|
|
395
497
|
|
|
396
498
|
These use the existing plugin methods, `ctx.call`, cards and subscriptions.
|
|
397
|
-
The namespace is required. `
|
|
499
|
+
The namespace is required. `approve` on the workflow asks before its start;
|
|
398
500
|
it does not preapprove later host actions. An unattended workflow refuses
|
|
399
|
-
|
|
501
|
+
plugin methods that ask. The calling project and caller
|
|
400
502
|
are preserved. A start `key` deduplicates within that project, plugin and
|
|
401
503
|
workflow while its run is retained; reusing it with different input fails.
|
|
402
504
|
|
|
@@ -428,8 +530,10 @@ with `kind: "sleep"`. Terminal runs have no pending wake time.
|
|
|
428
530
|
`attempts` counts the first invocation and defaults to 1. Only steps declared
|
|
429
531
|
`kind: "query"` may automatically retry; they cannot write SDK data or call
|
|
430
532
|
mutations. Within query steps, use `ctx.mcp` for read-aware host access;
|
|
431
|
-
generic `ctx.host`, notifications and agent starts are refused.
|
|
432
|
-
|
|
533
|
+
generic `ctx.host`, notifications and agent starts are refused. A step's `kind`
|
|
534
|
+
defaults to `"mutation"`, and `attempts` is at most 100. Query backoff,
|
|
535
|
+
`backoff: { initial, factor, max }`, defaults to full jitter with initial `2s`,
|
|
536
|
+
factor 2 and maximum `1m`. Import `NonRetryable` and throw it for a permanent failure.
|
|
433
537
|
Step `timeout` defaults to `5m`. The callback receives `signal`, `attempt`,
|
|
434
538
|
`attempts`, `runId`, `stepId` and an `idempotencyKey` stable across its attempts.
|
|
435
539
|
Pass its signal to fetch or subprocess APIs. A timeout aborts cooperatively;
|
|
@@ -500,8 +604,8 @@ itself. Each line names a plugin, one of its methods and a schedule:
|
|
|
500
604
|
|
|
501
605
|
| Field | What |
|
|
502
606
|
| --- | --- |
|
|
503
|
-
| `plugin`, `method` | The plugin's folder name and one of
|
|
504
|
-
| `project` | The project
|
|
607
|
+
| `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 |
|
|
608
|
+
| `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
609
|
| `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
610
|
| `timezone` | Defaults to the environment's |
|
|
507
611
|
| `enabled` | `false` keeps the line without running it |
|
|
@@ -520,15 +624,15 @@ isn't valid JSON pauses every line until it's fixed. Each line also shows
|
|
|
520
624
|
has the same for a plugin's own, and a query reading them re-runs around each run.
|
|
521
625
|
|
|
522
626
|
Schedule something only when the user asks for it. A cron's `ctx.root` is its
|
|
523
|
-
project, or the home folder
|
|
524
|
-
|
|
525
|
-
|
|
627
|
+
line's project, or the home folder without one. Plugins share a thread: code
|
|
628
|
+
that blocks it holds up every other cron, and the host's log names the crons
|
|
629
|
+
that were running when it happened.
|
|
526
630
|
|
|
527
631
|
## Logging
|
|
528
632
|
|
|
529
633
|
Write `console.log` (or `warn`, `error`, `debug`); nothing else is needed. The
|
|
530
634
|
host keeps every line with the plugin, the method and the call that wrote it,
|
|
531
|
-
who called (`card`, `agent`, `cron`), and the chat for an agent's call, plus
|
|
635
|
+
who called (`card`, `agent`, `cron`, `code`, or `http` and `device` for HTTP), and the chat for an agent's call, plus
|
|
532
636
|
the arguments as JSON. An error a method throws, or one nothing caught, is
|
|
533
637
|
kept too, with its stack. Read them with the agent's `plugins_logs` tool, or on
|
|
534
638
|
a card with `<Data method="repogo:plugins.logs">`, which reloads as lines land:
|
|
@@ -545,18 +649,18 @@ Every method, handler and cron gets `ctx`:
|
|
|
545
649
|
| Field | What |
|
|
546
650
|
| --- | --- |
|
|
547
651
|
| `plugin` | The plugin's name |
|
|
548
|
-
| `caller` | Who started the call: `card`, `agent`, `cron`, or `
|
|
652
|
+
| `caller` | Who started the call: `card`, `agent`, `cron`, `code`, or a request at the public (`http`) or device (`device`) address |
|
|
549
653
|
| `root` | The calling project's checkout |
|
|
550
654
|
| `collections`, `kv`, `streams`, `buckets` | The plugin's data |
|
|
551
655
|
| `call(method, input)` | Runs a method of a plugin in the same folder; `"open"` is this plugin's own |
|
|
552
656
|
| `plugins()` | The plugins in the same folder, with their methods |
|
|
553
|
-
| `host(method, params)` | Runs one of the host's methods that agents and cards may call (`git.status`, `chats.list`, `actions.run`, …); others fail with `FORBIDDEN` |
|
|
657
|
+
| `host(method, params)` | Runs one of the host's methods that agents and cards may call (`git.status`, `chats.list`, `actions.run`, …); others fail with `FORBIDDEN`, and one that doesn't exist with `INTERNAL` |
|
|
554
658
|
| `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 |
|
|
555
659
|
| `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 |
|
|
556
660
|
| `activity({ id, title, body?, page?, symbol?, tint?, label?, compactLabel?, progress?, timer?, status? })` | Shows or updates a Live Activity on every paired phone; see below |
|
|
557
661
|
| `agent(prompt, { agent, project?, title?, timeout? })` | Starts a chat with `prompt` and waits for the agent's whole answer: `{ chatId, text }`; see below |
|
|
558
662
|
| `emit(event, payload)` | Sends `<plugin>.<event>` to handlers |
|
|
559
|
-
| `signal` | Aborts when a stream's card goes |
|
|
663
|
+
| `signal` | Aborts when a stream's card goes, or when an HTTP caller leaves before a read or write answers |
|
|
560
664
|
| `log(...)` | The same as `console.log` |
|
|
561
665
|
|
|
562
666
|
A query doesn't re-run when what a `ctx.host` method read changes.
|
|
@@ -593,7 +697,7 @@ ends it with that state, left up for 15 minutes:
|
|
|
593
697
|
```ts
|
|
594
698
|
deploy: method(z.object({ project: z.string() }), async ({ project }, ctx) => {
|
|
595
699
|
const steps = ["Build", "Test", "Deploy", "Verify"];
|
|
596
|
-
const page =
|
|
700
|
+
const page = `~/.repogo/plugins/deploys/pages/index.ui?project=${project}`;
|
|
597
701
|
const show = (current: number, body: string) =>
|
|
598
702
|
ctx.activity({ id: `deploy:${project}`, title: `Deploying ${project}`, body, page,
|
|
599
703
|
symbol: "shippingbox", progress: { kind: "steps", steps, current } });
|
|
@@ -633,11 +737,11 @@ or CLI of its own. It answers the tool's structured content, else its text
|
|
|
633
737
|
customers: method(z.object({}), async (_, ctx) => {
|
|
634
738
|
const list = await ctx.mcp("stripe", "stripe_api_read", { stripe_api_operation_id: "GetCustomers", /* … */ });
|
|
635
739
|
return list.data.map((c) => ({ id: c.id, email: c.email }));
|
|
636
|
-
}, {
|
|
740
|
+
}, { effect: "read" }),
|
|
637
741
|
```
|
|
638
742
|
|
|
639
|
-
A
|
|
640
|
-
tool marked destructive runs only from a method with
|
|
743
|
+
A read may run only the server's read-only tools, as a card's `Data` may. A
|
|
744
|
+
tool marked destructive runs only from a method with an `approve`, or one
|
|
641
745
|
called from it with `ctx.call`: the user's answer to that question is its
|
|
642
746
|
yes. A refusal throws `FORBIDDEN`; a tool's own error throws `INTERNAL` with
|
|
643
747
|
its message. Only servers switched on for `ctx.root` answer, so a cron of an
|
|
@@ -661,23 +765,25 @@ on: {
|
|
|
661
765
|
| Bucket | `<name>.created`, `<name>.changed` (the file's bytes), `<name>.metaChanged`, `<name>.deleted` (the file) |
|
|
662
766
|
| Any method | `<method>` (`{ input, result }`) |
|
|
663
767
|
|
|
664
|
-
Changes to a bucket's folder made by hand emit too, once the host sees them
|
|
768
|
+
Changes to a bucket's folder made by hand emit too, once the host sees them;
|
|
769
|
+
streams emit none. A plugin's `on` hears only its own events, and runs inside
|
|
770
|
+
the call that emitted them, so a slow handler delays that call's answer.
|
|
665
771
|
Events run in memory: none is retried, and a handler that throws is logged.
|
|
666
772
|
|
|
667
773
|
## How cards and agents reach a plugin
|
|
668
774
|
|
|
669
775
|
**Agents** use two tools. `plugins` lists every plugin with a line per method
|
|
670
|
-
(its
|
|
776
|
+
(its effect, whether it asks first, and its input's fields, optional ones ending
|
|
671
777
|
in `?`), and every line of `~/.repogo/crons.json` with how it's going.
|
|
672
778
|
`plugins` with `plugin` set answers that plugin in full: each method's input
|
|
673
|
-
and output JSON Schema and
|
|
779
|
+
and output JSON Schema, `approve` and `approveUpTo`, its `storage` (each bucket's and stream's
|
|
674
780
|
folder), the pages that show its methods, and the cron lines naming it.
|
|
675
781
|
`plugin_call` runs one. Inspect a method's schema before naming it in a card.
|
|
676
|
-
Verify
|
|
782
|
+
Verify a read's fields with a read-only call. Do not execute writes just to
|
|
677
783
|
check a card; run them only when the user's task calls for that action.
|
|
678
784
|
|
|
679
785
|
**Cards** name methods as `plugin:<plugin>.<method>`. A `Data` element runs a
|
|
680
|
-
|
|
786
|
+
read, or follows a stream, and redraws when its data changes. `Button`,
|
|
681
787
|
`Item` and `Swipe` actions call methods; on iOS, `Toggle` can call one on a
|
|
682
788
|
user flip. `Upload` calls a plugin method with staged files. Params must match
|
|
683
789
|
the discovered input schema. These fragments assume `q` and `note` are supplied
|
|
@@ -701,10 +807,10 @@ by the enclosing card:
|
|
|
701
807
|
### Saving from a switch
|
|
702
808
|
|
|
703
809
|
Prefer SDK storage for plugin-owned data. A storage declaration generates
|
|
704
|
-
methods; ordinary saves do not need a custom
|
|
810
|
+
methods; ordinary saves do not need a custom write method. For example:
|
|
705
811
|
|
|
706
812
|
```ts
|
|
707
|
-
//
|
|
813
|
+
// ~/.repogo/plugins/preferences/app.ts
|
|
708
814
|
import { definePlugin, z } from "@repogo/plugins";
|
|
709
815
|
|
|
710
816
|
export default definePlugin({
|
|
@@ -738,19 +844,47 @@ Inside plugin code, `ctx.kv.flags.set("alerts", true)` saves the same value.
|
|
|
738
844
|
The generated `preferences.flags.get({ key: "alerts" })` returns an entry with
|
|
739
845
|
`value` and timestamps, while `ctx.kv.flags.get("alerts")` returns the value alone.
|
|
740
846
|
|
|
741
|
-
## Errors and
|
|
847
|
+
## Errors and approvals
|
|
848
|
+
|
|
849
|
+
Throw `new MethodError(code, message, fieldErrors?)` for a failure the caller
|
|
850
|
+
should read: `VALIDATION`, `NOT_FOUND`, `FORBIDDEN` or `INTERNAL`.
|
|
851
|
+
`fieldErrors` maps a JSON pointer (`"/minutes"`) to its message, and reaches an
|
|
852
|
+
HTTP caller as `fields`. Input that fails its schema answers `VALIDATION` with
|
|
853
|
+
a message per field. Anything else thrown is
|
|
854
|
+
`INTERNAL` with its message. An HTTP caller gets only the first three in their
|
|
855
|
+
words; `INTERNAL` reaches it as `Internal server error`
|
|
856
|
+
([Serving over HTTP](#serving-over-http)).
|
|
857
|
+
|
|
858
|
+
A method's `approve` is the question asked before it runs. `{{field}}` in it
|
|
859
|
+
is filled from the call's input, so it can say what it acts on:
|
|
860
|
+
`approve: "Restart {{machine}}?"`. A `destructive` method must have one.
|
|
861
|
+
|
|
862
|
+
- **A card's tap** always asks it first.
|
|
863
|
+
- **An agent's `plugin_call`** asks it in the chat, as a tool approval, and runs
|
|
864
|
+
the method only on the user's yes. `approveUpTo` names the least strict chat
|
|
865
|
+
mode that still asks; the method asks in that mode and every stricter one,
|
|
866
|
+
and runs unasked in the rest. The modes, strictest first, are
|
|
867
|
+
`approval-required`, `auto-accept-edits` and `full-access`; unset is
|
|
868
|
+
`full-access`, so it asks in every mode. A chat whose mode the host can't
|
|
869
|
+
tell asks.
|
|
870
|
+
- **Plan mode:** an agent's call to any method that isn't a `read` is refused,
|
|
871
|
+
with or without an `approve`.
|
|
872
|
+
- **Code:** `ctx.call` from plugin code skips it, and an unattended workflow
|
|
873
|
+
or cron refuses methods that ask.
|
|
742
874
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
875
|
+
```ts
|
|
876
|
+
upload: method(Build, run, {
|
|
877
|
+
effect: "destructive",
|
|
878
|
+
approve: "Upload build {{buildNumber}}?", // asks in every mode
|
|
879
|
+
}),
|
|
880
|
+
rename: method(Rename, run, {
|
|
881
|
+
approve: "Rename {{name}}?",
|
|
882
|
+
approveUpTo: "auto-accept-edits", // full access runs it unasked
|
|
883
|
+
}),
|
|
884
|
+
```
|
|
747
885
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
`confirmed: true`, once the user agrees. Ordinary `ctx.call` from plugin code skips
|
|
751
|
-
it; an unattended workflow refuses confirmation-gated methods. Give one to anything that can't be undone. `{{field}}` in the question is
|
|
752
|
-
filled from the call's input, so it can say what it acts on:
|
|
753
|
-
`confirm: "Restart {{machine}}?"`.
|
|
886
|
+
In full access an agent can run commands without asking, so there the
|
|
887
|
+
question catches a careless call rather than stopping a determined one.
|
|
754
888
|
|
|
755
889
|
## Testing
|
|
756
890
|
|
|
@@ -762,12 +896,16 @@ user's MCP servers:
|
|
|
762
896
|
import { testRuntime } from "@repogo/plugins/testing";
|
|
763
897
|
import notes from "./app";
|
|
764
898
|
|
|
765
|
-
const app = await testRuntime({ notes });
|
|
766
|
-
await app.call("notes.
|
|
899
|
+
const app = await testRuntime({ notes }); // { root?, mcp?, host? } as a second argument
|
|
900
|
+
await app.call("notes.notes.create", { text: "hi" });
|
|
767
901
|
await app.call("notes.clear", {}, { confirmed: false }); // throws CONFIRM, as an unanswered tap
|
|
768
|
-
app.setCrons([{ plugin: "notes", method: "
|
|
902
|
+
app.setCrons([{ plugin: "notes", method: "open", schedule: "* * * * *" }]); // a crons.json line, with root for project
|
|
769
903
|
await app.tickCrons(Date.now() + 60_000); // runs whatever is due by then
|
|
770
904
|
app.notices; // every ctx.notify so far
|
|
771
905
|
app.logs; // every console line so far, with its method and caller
|
|
906
|
+
app.manifest(); // the plugins and methods, as the host lists them
|
|
772
907
|
await app.close();
|
|
773
908
|
```
|
|
909
|
+
|
|
910
|
+
`call` takes `{ confirmed?, caller?, root?, chat?, files? }`; leaving
|
|
911
|
+
`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 {
|
|
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;
|
|
@@ -74,12 +74,16 @@ 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
|
-
|
|
81
|
-
/**
|
|
82
|
-
|
|
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;
|
|
83
87
|
/** JSON Schema of the input, and of the output when the method declares one. */
|
|
84
88
|
input: unknown;
|
|
85
89
|
output: unknown;
|
|
@@ -113,13 +117,15 @@ export type ManifestStorage = {
|
|
|
113
117
|
};
|
|
114
118
|
/**
|
|
115
119
|
* 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)
|
|
120
|
+
* `<plugin>.<method>` with `{}` when `schedule` comes due in `timezone` (the environment's),
|
|
121
|
+
* with `root` (the line's project) as `ctx.root`, else the Worker's own.
|
|
117
122
|
*/
|
|
118
123
|
export type CronEntry = {
|
|
119
124
|
plugin: string;
|
|
120
125
|
method: string;
|
|
121
126
|
schedule: string;
|
|
122
127
|
timezone?: string;
|
|
128
|
+
root?: string;
|
|
123
129
|
};
|
|
124
130
|
/** A schedule and how it's going; `error` says why it couldn't be set, and then nothing runs. */
|
|
125
131
|
export type CronStatus = ManifestCron & {
|
|
@@ -135,8 +141,8 @@ export type Manifest = {
|
|
|
135
141
|
storage: ManifestStorage;
|
|
136
142
|
}[];
|
|
137
143
|
};
|
|
138
|
-
/** Who started a call: a tap or a card's query, an agent's `plugin_call`, a cron,
|
|
139
|
-
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";
|
|
140
146
|
/** How `ctx.agent` runs a chat: which agent, in which project, and how long to wait for its answer. */
|
|
141
147
|
export type AgentOptions = {
|
|
142
148
|
/** `claude` or `codex`, as `agents.list` names them. */
|
|
@@ -173,15 +179,15 @@ export type Context = {
|
|
|
173
179
|
* Runs a tool of an MCP server the user switched on for the calling project:
|
|
174
180
|
* `ctx.mcp("stripe", "stripe_api_read", { … })`, answering the tool's result.
|
|
175
181
|
* A query may run only read-only tools; a destructive tool runs only from a
|
|
176
|
-
* method with
|
|
182
|
+
* method with an `approve`. Its answer doesn't re-run a query.
|
|
177
183
|
*/
|
|
178
184
|
mcp: (server: string, tool: string, input?: unknown) => Promise<unknown>;
|
|
179
185
|
/**
|
|
180
|
-
* The plugins beside this one
|
|
181
|
-
*
|
|
186
|
+
* The plugins beside this one in the environment's plugins folder, with
|
|
187
|
+
* their methods: what `call` can reach.
|
|
182
188
|
*/
|
|
183
189
|
plugins: () => Manifest["plugins"];
|
|
184
|
-
/** 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. */
|
|
185
191
|
signal: AbortSignal;
|
|
186
192
|
/** Sends an alert to every phone paired with this environment. */
|
|
187
193
|
notify: (notice: Notice) => void;
|