@repogo/plugins 0.1.35 → 0.1.37
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 +243 -3
- package/dist/cache.d.ts +27 -0
- package/dist/context.d.ts +67 -2
- package/dist/define.d.ts +31 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +123 -7
- package/dist/runtime.d.ts +16 -1
- package/dist/self.d.ts +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.js +1562 -49
- package/dist/tracking.d.ts +5 -0
- package/dist/uploads.d.ts +24 -0
- package/dist/workflow/attempts.d.ts +25 -0
- package/dist/workflow/availability.d.ts +8 -0
- package/dist/workflow/catalog.d.ts +21 -0
- package/dist/workflow/clock.d.ts +6 -0
- package/dist/workflow/contracts.d.ts +121 -0
- package/dist/workflow/definition.d.ts +35 -0
- package/dist/workflow/errors.d.ts +19 -0
- package/dist/workflow/events.d.ts +132 -0
- package/dist/workflow/interrupted.d.ts +3 -0
- package/dist/workflow/journal.d.ts +28 -0
- package/dist/workflow/lease.d.ts +2 -0
- package/dist/workflow/lifetime.d.ts +27 -0
- package/dist/workflow/policy.d.ts +36 -0
- package/dist/workflow/register.d.ts +11 -0
- package/dist/workflow/retry.d.ts +14 -0
- package/dist/workflow/runner.d.ts +29 -0
- package/dist/workflow/scope.d.ts +4 -0
- package/dist/workflow/service.d.ts +48 -0
- package/dist/workflow/sleep.d.ts +6 -0
- package/dist/workflow/state.d.ts +41 -0
- package/dist/workflow/storage.d.ts +22 -0
- package/dist/workflow/summary.d.ts +80 -0
- package/dist/workflow/testing.d.ts +12 -0
- package/dist/workflow-executor.js +22917 -0
- package/package.json +3 -3
- package/dist/index-83w1fk10.js +0 -28
package/README.md
CHANGED
|
@@ -7,8 +7,9 @@ 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): queries, mutations, streams
|
|
11
|
-
- [Data](#data): namespaces, collections, kv, streams, buckets
|
|
10
|
+
- [Methods](#methods): queries, mutations, streams, cached queries
|
|
11
|
+
- [Data](#data): namespaces, collections, kv, streams, buckets, uploads
|
|
12
|
+
- [Workflows](#workflows)
|
|
12
13
|
- [Crons](#crons)
|
|
13
14
|
- [Logging](#logging)
|
|
14
15
|
- [The context](#the-context)
|
|
@@ -90,6 +91,18 @@ show it and keep it current. Give each a `description`; agents read it to choose
|
|
|
90
91
|
a method. `output`, a zod schema of the result, tells agents and cards its
|
|
91
92
|
field names.
|
|
92
93
|
|
|
94
|
+
A query that returns a list takes `limit`, with a default and a maximum in its
|
|
95
|
+
schema, and returns only what a card or the voice agent shows of each item:
|
|
96
|
+
ids, titles, status. A `get` returns one in full, and long text (a body, a log)
|
|
97
|
+
is cut short in the list. Answers stay small, so cards draw fast and an agent
|
|
98
|
+
or the voice agent reads the whole of one.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
open: method(z.object({ limit: z.number().int().min(1).max(100).default(20) }), async ({ limit }, ctx) =>
|
|
102
|
+
(await ctx.collections.notes.list({ where: { done: false }, limit })).map(({ id, text }) => ({ id, text: text.slice(0, 200) })),
|
|
103
|
+
{ kind: "query", description: "Notes not done yet, newest first" }),
|
|
104
|
+
```
|
|
105
|
+
|
|
93
106
|
A **stream** sends each `yield` to the card as it happens, for as long as the
|
|
94
107
|
card shows it. `ctx.signal` aborts when the card goes, so hand it to what you
|
|
95
108
|
start. Give items an `id`. An agent calling a stream gets what arrives within
|
|
@@ -107,6 +120,27 @@ tail: stream(z.object({ file: z.string() }), async function* ({ file }, ctx) {
|
|
|
107
120
|
}),
|
|
108
121
|
```
|
|
109
122
|
|
|
123
|
+
A **cached** query is for answers that are slow to get, such as a CLI's list
|
|
124
|
+
or another service's API. It answers at once with what it saved last time.
|
|
125
|
+
When that answer is older than `fresh`, `run` goes again in the background,
|
|
126
|
+
and a card showing it redraws only if the answer changed. With nothing saved
|
|
127
|
+
yet, the first call waits for `run`, and its error is the card's.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
projects: cached(z.object({ limit: z.number().int().min(1).max(200).default(100) }),
|
|
131
|
+
async ({ limit }) => listProjects(limit),
|
|
132
|
+
{ fresh: "1m", shared: true, description: "Every project the login can see" }),
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- Answers are kept per method, input and project (`ctx.root`). `shared: true`
|
|
136
|
+
keeps one for every project, for answers that don't depend on it.
|
|
137
|
+
- `run` is read-only, like a query's, and has `timeout` (a minute unless it says).
|
|
138
|
+
- A background check that fails keeps the saved answer, goes to the plugin's
|
|
139
|
+
logs, and is tried again after another `fresh`.
|
|
140
|
+
- Answers live in the namespace's `data.db`, so a plugin with a cached method
|
|
141
|
+
needs a `namespace`. An answer over 1 MB is returned but not saved, and one
|
|
142
|
+
nobody has asked for in 30 days is deleted.
|
|
143
|
+
|
|
110
144
|
## Data
|
|
111
145
|
|
|
112
146
|
A plugin declares its data in four kinds, each with a schema, and gets typed
|
|
@@ -255,6 +289,37 @@ checks it against the schema; with `revision`, it fails if the file changed
|
|
|
255
289
|
since you read it. Writes through `ctx` are checked; files written by hand are
|
|
256
290
|
indexed with their `status` instead.
|
|
257
291
|
|
|
292
|
+
### Uploads
|
|
293
|
+
|
|
294
|
+
A card's `<Upload>` lets the user take a photo, record a video, or pick
|
|
295
|
+
photos or files on their phone, and hands them to one of your methods as
|
|
296
|
+
`files`. Declare the field with `upload()`:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { definePlugin, method, upload, z } from "@repogo/plugins";
|
|
300
|
+
|
|
301
|
+
ingest: method(z.object({ files: z.array(upload()), month: z.string() }), async ({ files, month }, ctx) => {
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
await ctx.buckets.receipts.put({ body: Bun.file(file.path), name: file.name, folder: month });
|
|
304
|
+
}
|
|
305
|
+
return { added: files.length };
|
|
306
|
+
}),
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Each file is `{ id, name, type, size, path }`. `type` is its MIME type, and
|
|
310
|
+
`path` is where the host staged it on the environment. The phone sends the
|
|
311
|
+
bytes before the call, encrypted like every call, and the call starts once
|
|
312
|
+
they've all arrived. A staged file is deleted a day after its last use, so
|
|
313
|
+
copy what you keep: into a bucket, as above, or wherever you like. `files` is
|
|
314
|
+
always an array, even for one file.
|
|
315
|
+
|
|
316
|
+
`upload()` accepts only files the host attached to the running call. Input
|
|
317
|
+
written by hand, such as an agent's `plugin_call` with a made-up `path`, fails
|
|
318
|
+
validation, and so does a call that sends its own `files` alongside the
|
|
319
|
+
upload's. A method the upload's method calls with `ctx.call` may pass the
|
|
320
|
+
files on. In tests, `app.call("receipts.ingest", { month: "may" }, { files })`
|
|
321
|
+
stands in for the host.
|
|
322
|
+
|
|
258
323
|
### Generated methods
|
|
259
324
|
|
|
260
325
|
Every collection, kv, stream and bucket gets methods named
|
|
@@ -277,6 +342,140 @@ through the plugin's own methods: `private: ["actions", "lines"]` for an audit
|
|
|
277
342
|
log no tap can edit. It hides methods; it isn't a lock. Another plugin naming
|
|
278
343
|
the same namespace and store can still read and write it.
|
|
279
344
|
|
|
345
|
+
## Workflows
|
|
346
|
+
|
|
347
|
+
> **Experimental.** Workflows are new and may change in ways that need edits
|
|
348
|
+
> to your plugins: their methods, statuses and options aren't settled yet.
|
|
349
|
+
|
|
350
|
+
A workflow keeps progress across host restarts. Define it beside `methods`;
|
|
351
|
+
`workflow(input, run, options)` follows the same shape as `method`:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { definePlugin, workflow, z } from "@repogo/plugins";
|
|
355
|
+
|
|
356
|
+
export default definePlugin({
|
|
357
|
+
namespace: "myproject.reports",
|
|
358
|
+
workflows: {
|
|
359
|
+
report: workflow(z.object({ url: z.url() }), async ({ url }, ctx) => {
|
|
360
|
+
const report = await ctx.step.run("fetch", async ({ signal }) => {
|
|
361
|
+
const response = await fetch(url, { signal });
|
|
362
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
363
|
+
return { title: await response.text() };
|
|
364
|
+
}, { kind: "query", attempts: 3, timeout: "30s" });
|
|
365
|
+
|
|
366
|
+
await ctx.sleep("5m");
|
|
367
|
+
|
|
368
|
+
return await ctx.step.run("save", async () => {
|
|
369
|
+
await ctx.kv.reports.set("latest", report);
|
|
370
|
+
return report;
|
|
371
|
+
});
|
|
372
|
+
}, { timeout: "30d", maxOffline: "14d", retention: "30d" }),
|
|
373
|
+
},
|
|
374
|
+
kv: { reports: z.object({ title: z.string() }) },
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For a plugin folder named `reports`, the host generates:
|
|
379
|
+
|
|
380
|
+
- `reports.report.list({ limit?, status?, cursor? })`: bounded summaries and a next cursor.
|
|
381
|
+
- `reports.report.get({ id })`: one summary, input, result, error and the latest 20 operations (steps and sleeps).
|
|
382
|
+
- `reports.report.start({ input, key? })`: persist and queue a run; returns its summary immediately.
|
|
383
|
+
- `reports.report.cancel({ id })`: persist cancellation intent and abort active work.
|
|
384
|
+
- `reports.report.cancelAll({ status? })`: cancel every unfinished run in the project, or only those in one status; returns `{ cancelled }`.
|
|
385
|
+
- `reports.report.retry({ id })`: retry a safely failed query step.
|
|
386
|
+
- `reports.report.restart({ id })`: start a run that needs attention over from the beginning, on the current code.
|
|
387
|
+
|
|
388
|
+
These use the existing plugin methods, `ctx.call`, cards and subscriptions.
|
|
389
|
+
The namespace is required. `confirm` on the workflow confirms its start;
|
|
390
|
+
it does not preapprove later host actions. An unattended workflow refuses
|
|
391
|
+
confirmation-gated plugin methods. The calling project and caller
|
|
392
|
+
are preserved. A start `key` deduplicates within that project, plugin and
|
|
393
|
+
workflow while its run is retained; reusing it with different input fails.
|
|
394
|
+
|
|
395
|
+
Await each step and use a unique, stable name. Keep effects inside step callbacks:
|
|
396
|
+
the handler runs from the beginning after recovery, and completed callbacks
|
|
397
|
+
return their recorded JSON instead of running again. Steps are sequential;
|
|
398
|
+
nesting, overlap and reusing a name fail the run. Return JSON, including an
|
|
399
|
+
explicit `null` for a step with no result. Large payloads belong in buckets;
|
|
400
|
+
a journal record is limited to 1 MiB, a run journal to 64 MiB. `get` omits
|
|
401
|
+
input or final results above 16 KiB and sets their truncation flags.
|
|
402
|
+
|
|
403
|
+
Use `await ctx.sleep("5m")` between steps for a durable wait. No ID is needed:
|
|
404
|
+
the runner identifies each sleep by its position among the workflow's operations.
|
|
405
|
+
It saves an absolute wake time, releases the executor, and resumes without
|
|
406
|
+
repeating completed steps. Loops and consecutive sleeps each get a separate
|
|
407
|
+
position. The first recorded duration wins on replay, even when the expression
|
|
408
|
+
that supplied it later changes. Durations follow the SDK convention (strings
|
|
409
|
+
like `"5m"` or milliseconds), must be positive, and cannot exceed 365 days.
|
|
410
|
+
|
|
411
|
+
Always await sleep at the workflow level, outside step callbacks; do not catch
|
|
412
|
+
its internal waiting signal or detach background workflow operations. Branch
|
|
413
|
+
using recorded step results so replay follows the same operation order.
|
|
414
|
+
The original run timeout and `maxOffline` still apply during sleep. A sleep
|
|
415
|
+
past the run deadline expires at that deadline; an unavailable environment
|
|
416
|
+
resumes only when available and within those limits. `list`/`get` show
|
|
417
|
+
`waiting` with `nextAttemptAt` as the wake time, and detail rows identify sleeps
|
|
418
|
+
with `kind: "sleep"`. Terminal runs have no pending wake time.
|
|
419
|
+
|
|
420
|
+
`attempts` counts the first invocation and defaults to 1. Only steps declared
|
|
421
|
+
`kind: "query"` may automatically retry; they cannot write SDK data or call
|
|
422
|
+
mutations. Within query steps, use `ctx.mcp` for read-aware host access;
|
|
423
|
+
generic `ctx.host`, notifications and agent starts are refused. Query backoff defaults to full jitter with initial `2s`, factor 2
|
|
424
|
+
and maximum `1m`. Import `NonRetryable` and throw it for a permanent failure.
|
|
425
|
+
Step `timeout` defaults to `5m`. The callback receives `signal`, `attempt`,
|
|
426
|
+
`attempts`, `runId`, `stepId` and an `idempotencyKey` stable across its attempts.
|
|
427
|
+
Pass its signal to fetch or subprocess APIs. A timeout aborts cooperatively;
|
|
428
|
+
a callback must settle before another attempt can begin.
|
|
429
|
+
|
|
430
|
+
Mutation steps get one attempt. An interrupted or timed-out mutation becomes
|
|
431
|
+
`unknown`: its external effect may have happened, and it is never automatically
|
|
432
|
+
repeated. Reconcile that effect before starting new work. Cancellation cannot
|
|
433
|
+
roll back remote effects. Manual retry only extends the budget of a failed
|
|
434
|
+
query, keeps its completed prefix and original lifetime, and rejects cancelled,
|
|
435
|
+
expired, unknown, permanent-failure and invalid-result runs.
|
|
436
|
+
|
|
437
|
+
### When you edit a workflow
|
|
438
|
+
|
|
439
|
+
A run always executes the plugin's current code, loaded from its folder as a
|
|
440
|
+
method call is; nothing is frozen at start. When a run resumes after an edit,
|
|
441
|
+
its finished steps still return their recorded results, so:
|
|
442
|
+
|
|
443
|
+
- An edit that keeps the recorded steps in the same order, such as a fix
|
|
444
|
+
inside a step or a new step at the end, carries on with the new code.
|
|
445
|
+
- An edit the recorded steps can't follow, such as a renamed, removed or
|
|
446
|
+
reordered step, stops the run in `attention`: not finished, and not run
|
|
447
|
+
again on its own. Its `error` says which step it expected. `restart` starts
|
|
448
|
+
it over on the current code, running finished steps again, with the same id,
|
|
449
|
+
key and input and a fresh `timeout`. `cancel` or
|
|
450
|
+
`cancelAll({ status: "attention" })` drops it.
|
|
451
|
+
|
|
452
|
+
Nested workflow starts from inside a run are not supported.
|
|
453
|
+
|
|
454
|
+
The host resumes work without an open phone or page. `timeout` is a wall-clock
|
|
455
|
+
run lifetime, including waits. `maxOffline` expires work when the environment
|
|
456
|
+
or workflow service has been unavailable for that long. A persistent network
|
|
457
|
+
outage is also tracked when the environment has no nonlocal network address;
|
|
458
|
+
this passive check does not probe external services or prove internet access.
|
|
459
|
+
Individual API failures use the step's retry budget. The host records expiry on its next wake.
|
|
460
|
+
Shutdown suspends work without recording user cancellation. A finished run's
|
|
461
|
+
journal stays until its `retention` passes. Admission refuses new runs above
|
|
462
|
+
its storage budget rather than evicting active history. Missing or corrupt
|
|
463
|
+
journals block execution. The default environment-wide budget is 512 MiB.
|
|
464
|
+
Each unfinished, blocked or attention run reserves 130 MiB for its journal and
|
|
465
|
+
one snapshot of its state, including while sleeping. This admits at most three
|
|
466
|
+
unfinished runs, fewer when retained history consumes the remaining space;
|
|
467
|
+
admission reports storage full. Checkpoint replacement temporarily keeps both
|
|
468
|
+
snapshots, so peak disk use can exceed that reservation. The admission budget
|
|
469
|
+
is not a free-disk guarantee.
|
|
470
|
+
|
|
471
|
+
For integration tests, import `testWorkflowRuntime` from
|
|
472
|
+
`@repogo/plugins/testing` and pass the fixture's plugins directory. It uses real
|
|
473
|
+
temporary storage and a subprocess per run, as the host does. Call the same lifecycle methods,
|
|
474
|
+
use `await app.wait("reports.report", id)` to await a terminal/blocked run, and
|
|
475
|
+
`await app.close()` afterward. The in-memory `testRuntime({ plugin })` helper
|
|
476
|
+
does not execute durable workflows.
|
|
477
|
+
|
|
478
|
+
|
|
280
479
|
## Crons
|
|
281
480
|
|
|
282
481
|
What runs with no one looking is listed in one file the user owns,
|
|
@@ -345,7 +544,8 @@ Every method, handler and cron gets `ctx`:
|
|
|
345
544
|
| `plugins()` | The plugins in the same folder, with their methods |
|
|
346
545
|
| `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` |
|
|
347
546
|
| `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 |
|
|
348
|
-
| `notify({ title, body, page?, collapse? })` | Alerts every paired phone; a tap opens `page`, and alerts sharing `collapse` replace each other |
|
|
547
|
+
| `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 |
|
|
548
|
+
| `activity({ id, title, body?, page?, symbol?, tint?, label?, compactLabel?, progress?, timer?, status? })` | Shows or updates a Live Activity on every paired phone; see below |
|
|
349
549
|
| `agent(prompt, { agent, project?, title?, timeout? })` | Starts a chat with `prompt` and waits for the agent's whole answer: `{ chatId, text }`; see below |
|
|
350
550
|
| `emit(event, payload)` | Sends `<plugin>.<event>` to handlers |
|
|
351
551
|
| `signal` | Aborts when a stream's card goes |
|
|
@@ -374,6 +574,46 @@ calling chat when an agent called the plugin. It waits up to `timeout`
|
|
|
374
574
|
running. The same two host methods are there by hand: `ctx.host("chats.start", …)`
|
|
375
575
|
and `ctx.host("chats.wait", …)`, for a chat you want to link to rather than wait for.
|
|
376
576
|
|
|
577
|
+
### Live Activities
|
|
578
|
+
|
|
579
|
+
`ctx.activity` puts a card on the Lock Screen and in the Dynamic Island.
|
|
580
|
+
Its `title`, `body` and `page` are a notice's, and the plugin sets
|
|
581
|
+
everything else too. Send the whole state each time: the same `id` updates
|
|
582
|
+
it, a field left out takes its default, and `status: "done"` or `"failed"`
|
|
583
|
+
ends it with that state, left up for 15 minutes:
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
deploy: method(z.object({ project: z.string() }), async ({ project }, ctx) => {
|
|
587
|
+
const steps = ["Build", "Test", "Deploy", "Verify"];
|
|
588
|
+
const page = `.repogo/tabs/deploys.ui?project=${project}`;
|
|
589
|
+
const show = (current: number, body: string) =>
|
|
590
|
+
ctx.activity({ id: `deploy:${project}`, title: `Deploying ${project}`, body, page,
|
|
591
|
+
symbol: "shippingbox", progress: { kind: "steps", steps, current } });
|
|
592
|
+
await show(0, "Building");
|
|
593
|
+
// …
|
|
594
|
+
await ctx.activity({ id: `deploy:${project}`, title: `${project} is live`, body: "web.app", page,
|
|
595
|
+
status: "done", symbol: "globe", tint: "blue", label: "Live",
|
|
596
|
+
progress: { kind: "steps", steps, current: steps.length } });
|
|
597
|
+
}),
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
| Field | Unless said |
|
|
601
|
+
| --- | --- |
|
|
602
|
+
| `body` | No line under the title |
|
|
603
|
+
| `page` | A tap opens the app |
|
|
604
|
+
| `symbol` | `circle.dotted`; a check once done, a cross once failed |
|
|
605
|
+
| `tint` | White; green once done, red once failed |
|
|
606
|
+
| `label` | "2 of 4", "40%", "Done" or "Failed"; `""` hides it |
|
|
607
|
+
| `compactLabel` | The `label`, else "2/4" or "40%" |
|
|
608
|
+
| `progress` | None. `{ kind: "steps", steps, current }` draws a segment per step, the ones before `current` complete; `{ kind: "bar", value }` a bar from 0 to 1 |
|
|
609
|
+
| `timer` | `{ kind: "elapsed" }` from the first start. `{ kind: "countdown", endsAt }` counts down to a Unix ms time and ends nothing; `{ kind: "none" }` hides it |
|
|
610
|
+
|
|
611
|
+
- The phones get the latest state at most every 5 seconds; a start and an end go at once.
|
|
612
|
+
- A state that comes to more than Apple's 4 KB as a push is refused.
|
|
613
|
+
- A plugin shows at most two at once; one left without an update for an hour stops counting.
|
|
614
|
+
- A query can't call it, as it can't write.
|
|
615
|
+
- The text reaches Apple and RepoGo's relay as is, as a `notify` alert's does: keep secrets out of it.
|
|
616
|
+
|
|
377
617
|
### MCP servers
|
|
378
618
|
|
|
379
619
|
`ctx.mcp` reaches the MCP servers the user connected in RepoGo, by the key
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Database } from "./database";
|
|
2
|
+
/** How a `cached` method keeps its answers: fresh for this long, shared by every project or not. */
|
|
3
|
+
export type CacheOptions = {
|
|
4
|
+
fresh: number;
|
|
5
|
+
shared: boolean;
|
|
6
|
+
timeout: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class AnswerCache {
|
|
9
|
+
private database;
|
|
10
|
+
private namespace;
|
|
11
|
+
private announce;
|
|
12
|
+
/** One fetch per key at a time; a card and a background check share it. */
|
|
13
|
+
private inflight;
|
|
14
|
+
constructor(database: Database, namespace: string, announce: (tags: string[]) => void);
|
|
15
|
+
setup(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* The saved answer at once, checked again in the background when it's
|
|
18
|
+
* older than `fresh`. With none saved, the caller waits for the fetch.
|
|
19
|
+
*/
|
|
20
|
+
read(key: string, options: CacheOptions, fetch: () => unknown): Promise<unknown>;
|
|
21
|
+
/** Settles once no fetch is out. */
|
|
22
|
+
idle(): Promise<void>;
|
|
23
|
+
private refresh;
|
|
24
|
+
private save;
|
|
25
|
+
}
|
|
26
|
+
/** A key for one answer: the method, the project unless it's shared, and the input with its keys sorted. */
|
|
27
|
+
export declare function cacheKey(method: string, root: string | null, input: unknown): string;
|
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 { MethodKind } from "./define";
|
|
5
|
+
import type { HTTPAccess, MethodKind } 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 = {
|
|
@@ -10,9 +10,66 @@ export type Notice = {
|
|
|
10
10
|
body: string;
|
|
11
11
|
/** A page target, `.repogo/tabs/vercel.md?project=a`; a tap opens it. */
|
|
12
12
|
page?: string;
|
|
13
|
+
/** An `https` or `itms-services` link a tap opens instead of the page, such as a build's install. */
|
|
14
|
+
url?: string;
|
|
13
15
|
/** Alerts sharing a collapse id replace one another; defaults to the plugin and title. */
|
|
14
16
|
collapse?: string;
|
|
15
17
|
};
|
|
18
|
+
/** A Live Activity's color: the cards' named colors, less `secondary`. */
|
|
19
|
+
export type ActivityTint = "primary" | "accent" | "success" | "warning" | "danger" | "blue" | "green" | "orange" | "red" | "purple" | "pink" | "teal" | "yellow" | "gray";
|
|
20
|
+
/** A segment per step, or one bar. */
|
|
21
|
+
export type ActivityProgress = {
|
|
22
|
+
kind: "steps";
|
|
23
|
+
/** 1 to 8 names of at most 24 characters. */
|
|
24
|
+
steps: string[];
|
|
25
|
+
/** The active step, 0 to `steps.length`; the ones before it are complete. */
|
|
26
|
+
current: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "bar";
|
|
29
|
+
/** 0 to 1. */
|
|
30
|
+
value: number;
|
|
31
|
+
};
|
|
32
|
+
/** The card's clock, which the phone ticks with no pushes. */
|
|
33
|
+
export type ActivityTimer = {
|
|
34
|
+
kind: "elapsed";
|
|
35
|
+
/** Unix ms; the activity's first start unless said. */
|
|
36
|
+
startedAt?: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "countdown";
|
|
39
|
+
/** Unix ms; reaching it ends nothing. */
|
|
40
|
+
endsAt: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "none";
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A Live Activity on the user's phones, sent whole each time: the same `id`
|
|
46
|
+
* updates it, a field left out takes its default, and status `done` or
|
|
47
|
+
* `failed` ends it with this state.
|
|
48
|
+
*/
|
|
49
|
+
export type Activity = {
|
|
50
|
+
/** The activity's id within the plugin, at most 64 bytes. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** At most 60 characters, as a notice's title. */
|
|
53
|
+
title: string;
|
|
54
|
+
/** At most 120 characters, as a notice's body. */
|
|
55
|
+
body?: string;
|
|
56
|
+
/** A page target of the calling project, `.repogo/tabs/deploys.ui?project=web`; a tap opens it. */
|
|
57
|
+
page?: string;
|
|
58
|
+
/** An SF Symbol name, `shippingbox`; `circle.dotted`, or a check or a cross once ended, unless said. */
|
|
59
|
+
symbol?: string;
|
|
60
|
+
/** Colors the symbol, the progress and the label; white, or green or red once ended, unless said. */
|
|
61
|
+
tint?: ActivityTint;
|
|
62
|
+
/** The card's value, at most 24 characters; "2 of 4", "40%" or the status unless said, and "" hides it. */
|
|
63
|
+
label?: string;
|
|
64
|
+
/** The Dynamic Island's value, at most 10 characters; the label, or "2/4", unless said. */
|
|
65
|
+
compactLabel?: string;
|
|
66
|
+
/** No progress unless said. */
|
|
67
|
+
progress?: ActivityProgress;
|
|
68
|
+
/** Elapsed since the first start unless said. */
|
|
69
|
+
timer?: ActivityTimer;
|
|
70
|
+
/** `running` unless said; `done` and `failed` end it. */
|
|
71
|
+
status?: "running" | "done" | "failed";
|
|
72
|
+
};
|
|
16
73
|
/** One method as `ctx.plugins()` and the agent's `plugins` tool list it. */
|
|
17
74
|
export type ManifestMethod = {
|
|
18
75
|
/** `<plugin>.<method>`, the name `ctx.call` takes. */
|
|
@@ -21,6 +78,8 @@ export type ManifestMethod = {
|
|
|
21
78
|
description: string | null;
|
|
22
79
|
/** What a card asks before running it; null runs on tap. */
|
|
23
80
|
confirm: string | null;
|
|
81
|
+
/** Who may call it at the environment's own address; null, no one. */
|
|
82
|
+
http: HTTPAccess | null;
|
|
24
83
|
/** JSON Schema of the input, and of the output when the method declares one. */
|
|
25
84
|
input: unknown;
|
|
26
85
|
output: unknown;
|
|
@@ -77,7 +136,7 @@ export type Manifest = {
|
|
|
77
136
|
}[];
|
|
78
137
|
};
|
|
79
138
|
/** Who started a call: a tap or a card's query, an agent's `plugin_call`, a cron, or plugin code with none of those behind it. */
|
|
80
|
-
export type Caller = "card" | "agent" | "cron" | "code";
|
|
139
|
+
export type Caller = "card" | "agent" | "cron" | "code" | "http";
|
|
81
140
|
/** How `ctx.agent` runs a chat: which agent, in which project, and how long to wait for its answer. */
|
|
82
141
|
export type AgentOptions = {
|
|
83
142
|
/** `claude` or `codex`, as `agents.list` names them. */
|
|
@@ -126,6 +185,12 @@ export type Context = {
|
|
|
126
185
|
signal: AbortSignal;
|
|
127
186
|
/** Sends an alert to every phone paired with this environment. */
|
|
128
187
|
notify: (notice: Notice) => void;
|
|
188
|
+
/**
|
|
189
|
+
* Shows or updates a Live Activity on every paired phone:
|
|
190
|
+
* `await ctx.activity({ id: "deploy", title: "Deploying", progress: { kind: "steps", steps: ["Build", "Ship"], current: 0 } })`.
|
|
191
|
+
* A plugin shows at most two at once.
|
|
192
|
+
*/
|
|
193
|
+
activity: (activity: Activity) => Promise<void>;
|
|
129
194
|
/**
|
|
130
195
|
* Starts a chat with `prompt` and waits for the agent's whole answer:
|
|
131
196
|
* `await ctx.agent("Why is the build failing?", { agent: "claude" })`.
|
package/dist/define.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { CacheOptions } from "./cache";
|
|
2
3
|
import type { Context } from "./context";
|
|
4
|
+
import { type Duration } from "./duration";
|
|
5
|
+
import type { Workflow } from "./workflow/definition";
|
|
3
6
|
import type { Retain } from "./streams";
|
|
4
7
|
export type Collections = Record<string, z.ZodObject>;
|
|
5
8
|
/** A stream's lines' schema, or that and how long it keeps them (7 days or 50 MB a key unless it says). */
|
|
@@ -25,8 +28,17 @@ export type Method<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodT
|
|
|
25
28
|
* runs, such as "Delete every item?". Methods without one run on tap.
|
|
26
29
|
*/
|
|
27
30
|
confirm?: string;
|
|
31
|
+
/** Who may call it at the environment's own address; unset, no one. */
|
|
32
|
+
http?: HTTPAccess;
|
|
33
|
+
/** Set by `cached`: the query answers from what it saved, and checks again when that's old. */
|
|
34
|
+
cache?: CacheOptions;
|
|
28
35
|
run: (input: z.infer<I>, ctx: Context) => unknown | Promise<unknown>;
|
|
29
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* `key`: the host checks a key the caller sends. `none`: anyone reaches the
|
|
39
|
+
* method, which checks the caller itself, such as a provider's signature.
|
|
40
|
+
*/
|
|
41
|
+
export type HTTPAccess = "key" | "none";
|
|
30
42
|
export type Handler = (payload: any, ctx: Context) => unknown | Promise<unknown>;
|
|
31
43
|
export type PluginDefinition = {
|
|
32
44
|
/** One line, shown to the agent in the index of plugins. */
|
|
@@ -46,6 +58,7 @@ export type PluginDefinition = {
|
|
|
46
58
|
/** Append-only JSONL per key, in `<namespace>/streams/<name>/`, kept as `retain` says. */
|
|
47
59
|
streams?: Record<string, StreamDefinition>;
|
|
48
60
|
methods?: Record<string, Method<any, any>>;
|
|
61
|
+
workflows?: Record<string, Workflow<any, any>>;
|
|
49
62
|
on?: Record<string, Handler>;
|
|
50
63
|
/**
|
|
51
64
|
* Data named here gets no generated methods (list, create, delete…), so cards
|
|
@@ -59,6 +72,24 @@ export declare function method<I extends z.ZodType, O extends z.ZodType = z.ZodT
|
|
|
59
72
|
output?: O;
|
|
60
73
|
description?: string;
|
|
61
74
|
confirm?: string;
|
|
75
|
+
http?: true | {
|
|
76
|
+
auth: HTTPAccess;
|
|
77
|
+
};
|
|
78
|
+
}): Method<I, O>;
|
|
79
|
+
/**
|
|
80
|
+
* A query that answers at once from what it saved last time, for answers that
|
|
81
|
+
* are slow to get: a CLI's list, another service's API. When the saved answer
|
|
82
|
+
* is older than `fresh`, `run` goes again in the background, and cards showing
|
|
83
|
+
* it redraw only if the answer changed. With nothing saved, the first call
|
|
84
|
+
* waits for `run`. Each project keeps its own answers unless `shared`.
|
|
85
|
+
* `run` is read-only, like a query's; it has `timeout` (a minute unless it says).
|
|
86
|
+
*/
|
|
87
|
+
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
|
+
fresh: Duration;
|
|
89
|
+
shared?: boolean;
|
|
90
|
+
timeout?: Duration;
|
|
91
|
+
output?: O;
|
|
92
|
+
description?: string;
|
|
62
93
|
}): Method<I, O>;
|
|
63
94
|
/**
|
|
64
95
|
* A method that sends items as they happen: a log tail, a build's output. Each
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { definePlugin, method, stream } from "./define";
|
|
2
|
-
export
|
|
3
|
-
export type {
|
|
1
|
+
export { cached, definePlugin, method, stream } from "./define";
|
|
2
|
+
export { upload, type UploadedFile } from "./uploads";
|
|
3
|
+
export type { PluginDefinition, Method, MethodKind, HTTPAccess, StreamDefinition } from "./define";
|
|
4
|
+
export type { Activity, ActivityProgress, ActivityTimer, ActivityTint, AgentOptions, AgentReply, Caller, Context, CronEntry, CronStatus, Manifest, ManifestCron, ManifestMethod, ManifestStorage } from "./context";
|
|
4
5
|
export type { LogLevel, LogRecord } from "./logs";
|
|
5
6
|
export type { BucketFile, FileStatus, PutFile } from "./buckets";
|
|
6
7
|
export type { KvEntry, SetOptions } from "./kv";
|
|
@@ -8,3 +9,8 @@ export type { Retain, StreamEntry, StreamKey } from "./streams";
|
|
|
8
9
|
export type { Duration } from "./duration";
|
|
9
10
|
export { MethodError, toCallError, type CallError } from "./errors";
|
|
10
11
|
export { z } from "zod";
|
|
12
|
+
export { workflow, type Workflow, type WorkflowContext, type WorkflowOptions } from "./workflow/definition";
|
|
13
|
+
export { NonRetryable } from "./workflow/errors";
|
|
14
|
+
export type { StepContext } from "./workflow/definition";
|
|
15
|
+
export type { StepOptions } from "./workflow/policy";
|
|
16
|
+
export type { RunSummary } from "./workflow/contracts";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/index.ts
|
|
14
|
+
var exports_src = {};
|
|
15
|
+
__export(exports_src, {
|
|
16
|
+
z: () => z3,
|
|
17
|
+
workflow: () => workflow,
|
|
18
|
+
upload: () => upload,
|
|
19
|
+
toCallError: () => toCallError,
|
|
20
|
+
stream: () => stream,
|
|
21
|
+
method: () => method,
|
|
22
|
+
definePlugin: () => definePlugin,
|
|
23
|
+
cached: () => cached,
|
|
24
|
+
NonRetryable: () => NonRetryable,
|
|
25
|
+
MethodError: () => MethodError
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// src/duration.ts
|
|
29
|
+
var unitMs = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
30
|
+
function parseDuration(duration) {
|
|
31
|
+
if (typeof duration === "number") {
|
|
32
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
33
|
+
throw new Error(`a duration is a positive number of ms: ${duration}`);
|
|
34
|
+
return duration;
|
|
35
|
+
}
|
|
36
|
+
const match = /^(\d+(?:\.\d+)?)([smhd])$/.exec(duration);
|
|
37
|
+
if (!match)
|
|
38
|
+
throw new Error(`a duration is like "30s", "5m", "1h" or "7d": ${duration}`);
|
|
39
|
+
const ms = Number(match[1]) * unitMs[match[2]];
|
|
40
|
+
if (ms <= 0)
|
|
41
|
+
throw new Error(`a duration is longer than zero: ${duration}`);
|
|
42
|
+
return ms;
|
|
43
|
+
}
|
|
6
44
|
|
|
7
45
|
// src/define.ts
|
|
8
46
|
function definePlugin(plugin) {
|
|
@@ -15,20 +53,98 @@ function method(input, run, options = {}) {
|
|
|
15
53
|
kind: options.kind ?? "mutation",
|
|
16
54
|
output: options.output,
|
|
17
55
|
description: options.description,
|
|
18
|
-
confirm: options.confirm
|
|
56
|
+
confirm: options.confirm,
|
|
57
|
+
http: options.http === true ? "key" : options.http?.auth
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function cached(input, run, options) {
|
|
61
|
+
return {
|
|
62
|
+
input,
|
|
63
|
+
run,
|
|
64
|
+
kind: "query",
|
|
65
|
+
output: options.output,
|
|
66
|
+
description: options.description,
|
|
67
|
+
cache: { fresh: parseDuration(options.fresh), shared: options.shared ?? false, timeout: parseDuration(options.timeout ?? "1m") }
|
|
19
68
|
};
|
|
20
69
|
}
|
|
21
70
|
function stream(input, run, options = {}) {
|
|
22
71
|
return { input, run, kind: "stream", description: options.description };
|
|
23
72
|
}
|
|
73
|
+
// src/uploads.ts
|
|
74
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
75
|
+
import { z } from "zod";
|
|
76
|
+
var callFiles = new AsyncLocalStorage;
|
|
77
|
+
function upload() {
|
|
78
|
+
return z.object({ id: z.string(), name: z.string(), type: z.string(), size: z.number(), path: z.string() }).refine((file) => (callFiles.getStore() ?? []).some((held) => held.id === file.id && held.path === file.path), {
|
|
79
|
+
message: "Not a file uploaded for this call."
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// src/errors.ts
|
|
83
|
+
import { z as z2 } from "zod";
|
|
84
|
+
|
|
85
|
+
class MethodError extends Error {
|
|
86
|
+
code;
|
|
87
|
+
fieldErrors;
|
|
88
|
+
constructor(code, message, fieldErrors) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.fieldErrors = fieldErrors;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function toCallError(error) {
|
|
95
|
+
if (error instanceof MethodError) {
|
|
96
|
+
return { code: error.code, message: error.message, ...error.fieldErrors ? { fieldErrors: error.fieldErrors } : {} };
|
|
97
|
+
}
|
|
98
|
+
if (error instanceof z2.ZodError) {
|
|
99
|
+
const fieldErrors = {};
|
|
100
|
+
for (const issue of error.issues)
|
|
101
|
+
fieldErrors["/" + issue.path.join("/")] ??= issue.message;
|
|
102
|
+
const messages = Object.values(fieldErrors);
|
|
103
|
+
const message = messages.length === 1 ? messages[0] : "Check the highlighted fields.";
|
|
104
|
+
return { code: "VALIDATION", message, fieldErrors };
|
|
105
|
+
}
|
|
106
|
+
return { code: "INTERNAL", message: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
24
108
|
|
|
25
109
|
// src/index.ts
|
|
26
|
-
import { z } from "zod";
|
|
110
|
+
import { z as z3 } from "zod";
|
|
111
|
+
|
|
112
|
+
// src/workflow/scope.ts
|
|
113
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
114
|
+
var attemptScope = new AsyncLocalStorage2;
|
|
115
|
+
var workflowScope = new AsyncLocalStorage2;
|
|
116
|
+
|
|
117
|
+
// src/workflow/definition.ts
|
|
118
|
+
function workflowContext(base, run) {
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
...run,
|
|
122
|
+
get signal() {
|
|
123
|
+
return attemptScope.getStore()?.signal ?? run.signal;
|
|
124
|
+
},
|
|
125
|
+
step: { run: (name, callback, options) => run.step.run(name, (context) => callback({ ...context, runId: run.runId, idempotencyKey: `${run.runId}:${name}` }), options) }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function workflow(input, run, options = {}) {
|
|
129
|
+
return { input, run, ...options };
|
|
130
|
+
}
|
|
131
|
+
// src/workflow/errors.ts
|
|
132
|
+
class NonRetryable extends Error {
|
|
133
|
+
constructor(message) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "NonRetryable";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
27
138
|
export {
|
|
28
|
-
z,
|
|
139
|
+
z3 as z,
|
|
140
|
+
workflow,
|
|
141
|
+
upload,
|
|
29
142
|
toCallError,
|
|
30
143
|
stream,
|
|
31
144
|
method,
|
|
32
145
|
definePlugin,
|
|
146
|
+
cached,
|
|
147
|
+
NonRetryable,
|
|
33
148
|
MethodError
|
|
34
149
|
};
|
|
150
|
+
export { parseDuration, callFiles, MethodError, toCallError, attemptScope, workflowScope, workflowContext, exports_src };
|