@gjermundgaraba/effect-actions 0.19.0 → 0.21.0
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 +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# notes-service
|
|
2
2
|
|
|
3
|
-
A small service as most applications of this library are built: one set of contracts, one
|
|
4
|
-
implementation, served as a JSON API and as MCP tools, with a CLI and a browser client on the
|
|
5
|
-
same binding. Notes belong to the signed-in user; reading needs the `notes:read` scope and
|
|
6
|
-
writing `notes:write`.
|
|
3
|
+
A small service as most applications of this library are built: one set of contracts, one implementation, served as a JSON API and as MCP tools, with a CLI and a browser client on the same binding. Notes belong to the signed-in user; reading needs the `notes:read` scope and writing `notes:write`.
|
|
7
4
|
|
|
8
5
|
## Run it
|
|
9
6
|
|
|
10
|
-
From the repository root, after `vp install` (from an installed package, see
|
|
11
|
-
[Run one](../README.md#run-one)):
|
|
7
|
+
From the repository root, after `vp install` (from an installed package, see [Run one](../README.md#run-one)):
|
|
12
8
|
|
|
13
9
|
```sh
|
|
14
10
|
node --import tsx examples/notes-service/src/server.ts # 127.0.0.1:3000
|
|
@@ -34,30 +30,19 @@ claude mcp add --transport http notes http://127.0.0.1:3000/mcp --header "Author
|
|
|
34
30
|
codex mcp add notes --url http://127.0.0.1:3000/mcp --bearer-token-env-var NOTES_TOKEN
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
`src/stdio.ts` is another way in: a subprocess the host starts, with a store of its own.
|
|
38
|
-
`src/worker.ts` serves the same routes as a `fetch` handler, for a serverless host such as
|
|
39
|
-
Cloudflare Workers ([ActionMcp.md](../../docs/ActionMcp.md#serve-tools-over-http)).
|
|
33
|
+
`src/stdio.ts` is another way in: a subprocess the host starts, with a store of its own. `src/worker.ts` serves the same routes as a `fetch` handler, for a serverless host such as Cloudflare Workers ([ActionMcp.md](../../docs/ActionMcp.md#serve-tools-over-http)).
|
|
40
34
|
|
|
41
|
-
The server reads `HOST` (default `127.0.0.1`), `PORT` (default 3000) and `PUBLIC_URL`, the
|
|
42
|
-
address it tells OAuth clients it is at; the CLI reads `NOTES_URL` and `NOTES_TOKEN`. Change the
|
|
43
|
-
port and set `PORT`, `PUBLIC_URL` and `NOTES_URL` to match. Every command asks for
|
|
44
|
-
`NOTES_TOKEN`, the public `get-status` included: without it a command prints one message on
|
|
45
|
-
stderr saying to set it, and exits 1. `--help` needs none.
|
|
35
|
+
The server reads `HOST` (default `127.0.0.1`), `PORT` (default 3000) and `PUBLIC_URL`, the address it tells OAuth clients it is at; the CLI reads `NOTES_URL` and `NOTES_TOKEN`. Change the port and set `PORT`, `PUBLIC_URL` and `NOTES_URL` to match. Every command asks for `NOTES_TOKEN`, the public `get-status` included: without it a command prints one message on stderr saying to set it, and exits 1. `--help` needs none.
|
|
46
36
|
|
|
47
37
|
## Retry a creation
|
|
48
38
|
|
|
49
|
-
`createNote({ title, body })` creates a new note on every call. To retry safely, add an optional
|
|
50
|
-
`requestId`. Generate it once for a new creation, for example with `crypto.randomUUID()`, and
|
|
51
|
-
keep it with the pending operation outside any retry loop. After a lost response, send the
|
|
52
|
-
same key and content again. For the CLI, pass `--request-id` and keep it when repeating the
|
|
53
|
-
command:
|
|
39
|
+
`createNote({ title, body })` creates a new note on every call. To retry safely, add an optional `requestId`. Generate it once for a new creation, for example with `crypto.randomUUID()`, and keep it with the pending operation outside any retry loop. After a lost response, send the same key and content again. For the CLI, pass `--request-id` and keep it when repeating the command:
|
|
54
40
|
|
|
55
41
|
```sh
|
|
56
42
|
echo "First draft" | node --import tsx examples/notes-service/src/cli.ts create-note --request-id plan-1 --title Plan
|
|
57
43
|
```
|
|
58
44
|
|
|
59
|
-
What a key promises, per caller, for changed content and after the note changes, is what
|
|
60
|
-
[test/notes/idempotency.test.ts](test/notes/idempotency.test.ts) asserts.
|
|
45
|
+
What a key promises, per caller, for changed content and after the note changes, is what [test/notes/idempotency.test.ts](test/notes/idempotency.test.ts) asserts.
|
|
61
46
|
|
|
62
47
|
## Layout
|
|
63
48
|
|
|
@@ -93,27 +78,27 @@ test/
|
|
|
93
78
|
|
|
94
79
|
## Where to look
|
|
95
80
|
|
|
96
|
-
| To see
|
|
97
|
-
|
|
|
98
|
-
| A contract with declared errors and their statuses
|
|
99
|
-
| Input fields described, for `--help` and a tool's schema
|
|
100
|
-
| Handlers getting a service once and the caller per call
|
|
101
|
-
| A write safe to retry with the same caller and request key
|
|
102
|
-
| A public action beside protected ones
|
|
103
|
-
| Authorization from `readOnly`, refusing with a scope
|
|
104
|
-
| A token verifier and OAuth discovery read from config
|
|
105
|
-
| One implementation served over HTTP and MCP
|
|
106
|
-
| Config, a body limit and the host's own routes
|
|
107
|
-
| The same actions over stdio, the host naming the caller
|
|
108
|
-
| A serverless entrypoint, the MCP endpoint `requestScoped`
|
|
109
|
-
| A CLI derived from the binding: an argument, a field on stdin | `src/commands.ts`
|
|
110
|
-
| A command written by hand beside the derived ones
|
|
111
|
-
| A bulk import as one action, its command reading a file
|
|
112
|
-
| The CLI's connection, read from config when a command runs
|
|
113
|
-
| A missing token as one message, not a `ConfigError` report
|
|
114
|
-
| A promise client handling a declared error by its tag
|
|
115
|
-
| Testing handlers and authorization with no transport
|
|
116
|
-
| Testing a revision race, two fibers taking turns
|
|
117
|
-
| Testing routes, tools and refusals in memory
|
|
118
|
-
| Testing commands against the routes in memory
|
|
119
|
-
| Testing `--help` and the connection with config as a Layer
|
|
81
|
+
| To see | Read |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| A contract with declared errors and their statuses | `src/notes/Notes.ts` |
|
|
84
|
+
| Input fields described, for `--help` and a tool's schema | `src/notes/Notes.ts` |
|
|
85
|
+
| Handlers getting a service once and the caller per call | `src/notes/handlers.ts` |
|
|
86
|
+
| A write safe to retry with the same caller and request key | `src/notes/store.ts` |
|
|
87
|
+
| A public action beside protected ones | `src/status/` |
|
|
88
|
+
| Authorization from `readOnly`, refusing with a scope | `src/identity/authorize.ts` |
|
|
89
|
+
| A token verifier and OAuth discovery read from config | `src/identity/verifier.ts` |
|
|
90
|
+
| One implementation served over HTTP and MCP | `src/routes.ts` |
|
|
91
|
+
| Config, a body limit and the host's own routes | `src/server.ts`, `src/routes.ts` |
|
|
92
|
+
| The same actions over stdio, the host naming the caller | `src/stdio.ts` |
|
|
93
|
+
| A serverless entrypoint, the MCP endpoint `requestScoped` | `src/worker.ts`, `src/routes.ts` |
|
|
94
|
+
| A CLI derived from the binding: an argument, a field on stdin | `src/commands.ts` |
|
|
95
|
+
| A command written by hand beside the derived ones | `src/commands.ts` |
|
|
96
|
+
| A bulk import as one action, its command reading a file | `src/notes/`, `src/commands.ts` |
|
|
97
|
+
| The CLI's connection, read from config when a command runs | `src/commands.ts` |
|
|
98
|
+
| A missing token as one message, not a `ConfigError` report | `src/commands.ts` |
|
|
99
|
+
| A promise client handling a declared error by its tag | `src/browser.ts` |
|
|
100
|
+
| Testing handlers and authorization with no transport | `test/notes/in-process.test.ts` |
|
|
101
|
+
| Testing a revision race, two fibers taking turns | `test/notes/notes.test.ts` |
|
|
102
|
+
| Testing routes, tools and refusals in memory | `test/routes.test.ts` |
|
|
103
|
+
| Testing commands against the routes in memory | `test/commands.test.ts` |
|
|
104
|
+
| Testing `--help` and the connection with config as a Layer | `test/commands.test.ts` |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
1
2
|
import { Effect } from "effect";
|
|
2
3
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
-
|
|
4
|
+
|
|
4
5
|
import { Http } from "./Api.js";
|
|
5
6
|
import type { Note } from "./notes/Notes.js";
|
|
6
7
|
|
|
@@ -12,30 +13,30 @@ import type { Note } from "./notes/Notes.js";
|
|
|
12
13
|
export const connect = (token: string, baseUrl?: string) => {
|
|
13
14
|
// Built once, outside any Effect. Every method is typed from its action.
|
|
14
15
|
const api = ActionHttp.fetchClient(Http, {
|
|
15
|
-
transformClient: HttpClient.mapRequest(
|
|
16
|
+
transformClient: HttpClient.mapRequest(
|
|
17
|
+
HttpClientRequest.bearerToken(token)
|
|
18
|
+
),
|
|
16
19
|
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
// Each method returns a promise. A failure rejects it with the decoded error, so in a `catch`
|
|
20
23
|
// `error instanceof Action.Unauthenticated` tells an expired sign-in from anything else.
|
|
21
24
|
return {
|
|
22
|
-
notes: () => Effect.runPromise(api.listNotes()),
|
|
25
|
+
notes: async () => await Effect.runPromise(api.listNotes()),
|
|
23
26
|
|
|
24
27
|
// A declared error can also be handled by its tag before the promise: a missing note is
|
|
25
28
|
// `undefined` here, and every other failure still rejects.
|
|
26
|
-
note: (id: string): Promise<Note | undefined> =>
|
|
27
|
-
api.getNote({ id }).pipe(
|
|
28
|
-
Effect.catchTag("NoteNotFound", () => Effect.
|
|
29
|
-
Effect.runPromise
|
|
29
|
+
note: async (id: string): Promise<Note | undefined> =>
|
|
30
|
+
await api.getNote({ id }).pipe(
|
|
31
|
+
Effect.catchTag("NoteNotFound", () => Effect.undefined),
|
|
32
|
+
Effect.runPromise
|
|
30
33
|
),
|
|
31
34
|
|
|
32
|
-
// A stale revision is refused, never overwritten: the error carries the
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
),
|
|
38
|
-
Effect.runPromise,
|
|
35
|
+
// A stale revision is refused, never overwritten: the error, `NoteChanged`, carries the
|
|
36
|
+
// current one.
|
|
37
|
+
save: async (note: Note, body: string) =>
|
|
38
|
+
await Effect.runPromise(
|
|
39
|
+
api.updateNote({ id: note.id, body, revision: note.revision })
|
|
39
40
|
),
|
|
40
41
|
};
|
|
41
42
|
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NodeHttpClient,
|
|
3
|
+
NodeRuntime,
|
|
4
|
+
NodeServices,
|
|
5
|
+
} from "@effect/platform-node";
|
|
6
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
2
7
|
import { Effect } from "effect";
|
|
3
8
|
import { Command } from "effect/cli";
|
|
4
|
-
|
|
9
|
+
|
|
5
10
|
import { commands } from "./commands.js";
|
|
6
11
|
|
|
7
12
|
// Entrypoint: `node --import tsx examples/notes-service/src/cli.ts list-notes`.
|
|
@@ -9,5 +14,5 @@ import { commands } from "./commands.js";
|
|
|
9
14
|
Command.run(commands, { version: "1.0.0" }).pipe(
|
|
10
15
|
Effect.provide([NodeHttpClient.layerUndici, NodeServices.layer]),
|
|
11
16
|
ActionCli.logToStderr,
|
|
12
|
-
NodeRuntime.runMain
|
|
17
|
+
NodeRuntime.runMain()
|
|
13
18
|
);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
2
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
1
3
|
import { Config, Console, Effect, FileSystem, Schema } from "effect";
|
|
2
4
|
import { Argument, CliError, Command } from "effect/cli";
|
|
3
5
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
-
|
|
5
|
-
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
|
+
|
|
6
7
|
import { Http } from "./Api.js";
|
|
7
8
|
|
|
8
9
|
// What a command fails with when no token is configured: a tagged error of the CLI's own, printed
|
|
9
10
|
// on stderr as `MissingToken: <message>` under an `ERROR` heading, and exit 1.
|
|
10
|
-
class MissingToken extends Schema.TaggedError<
|
|
11
|
+
class MissingToken extends Schema.TaggedError<
|
|
12
|
+
MissingToken,
|
|
13
|
+
{ readonly brand: unique symbol }
|
|
14
|
+
>()("MissingToken", {
|
|
11
15
|
message: Schema.String,
|
|
12
16
|
}) {}
|
|
13
17
|
|
|
@@ -15,7 +19,7 @@ class MissingToken extends Schema.TaggedError<MissingToken>()("MissingToken", {
|
|
|
15
19
|
// from config. Every command here, the derived ones and your own, calls through this one.
|
|
16
20
|
const api = Effect.gen(function* () {
|
|
17
21
|
const baseUrl = yield* Config.String("NOTES_URL").pipe(
|
|
18
|
-
Config.withDefault("http://127.0.0.1:3000")
|
|
22
|
+
Config.withDefault("http://127.0.0.1:3000")
|
|
19
23
|
);
|
|
20
24
|
|
|
21
25
|
// Required, since every command but `get-status` is refused without one, and mapped so that a
|
|
@@ -24,14 +28,17 @@ const api = Effect.gen(function* () {
|
|
|
24
28
|
Effect.mapError(
|
|
25
29
|
() =>
|
|
26
30
|
new MissingToken({
|
|
27
|
-
message:
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
message:
|
|
32
|
+
"NOTES_TOKEN is not set. Set it to your token: export NOTES_TOKEN=<your token>",
|
|
33
|
+
})
|
|
34
|
+
)
|
|
30
35
|
);
|
|
31
36
|
|
|
32
37
|
return yield* ActionHttp.client(Http, {
|
|
33
38
|
baseUrl,
|
|
34
|
-
transformClient: HttpClient.mapRequest(
|
|
39
|
+
transformClient: HttpClient.mapRequest(
|
|
40
|
+
HttpClientRequest.bearerToken(token)
|
|
41
|
+
),
|
|
35
42
|
});
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -49,17 +56,26 @@ const unescape = (body: string) => body.replaceAll(/^\\(?=\\*# )/gmu, "");
|
|
|
49
56
|
// `FileSystem` every Effect CLI command has.
|
|
50
57
|
const exportNotes = Command.make(
|
|
51
58
|
"export",
|
|
52
|
-
{
|
|
59
|
+
{
|
|
60
|
+
file: Argument.String("file").pipe(
|
|
61
|
+
Argument.withDescription("The Markdown file to write.")
|
|
62
|
+
),
|
|
63
|
+
},
|
|
53
64
|
({ file }) =>
|
|
54
65
|
Effect.gen(function* () {
|
|
55
66
|
const client = yield* api;
|
|
56
67
|
const files = yield* FileSystem.FileSystem;
|
|
57
68
|
const { notes } = yield* client.listNotes();
|
|
58
|
-
|
|
69
|
+
|
|
70
|
+
const whole = yield* Effect.forEach(notes, ({ id }) =>
|
|
71
|
+
client.getNote({ id })
|
|
72
|
+
);
|
|
59
73
|
|
|
60
74
|
yield* files.writeFileString(
|
|
61
75
|
file,
|
|
62
|
-
whole
|
|
76
|
+
whole
|
|
77
|
+
.map((note) => `# ${note.title}\n\n${escape(note.body)}`)
|
|
78
|
+
.join("\n\n")
|
|
63
79
|
);
|
|
64
80
|
|
|
65
81
|
yield* Console.log(`Wrote ${whole.length} notes to ${file}`);
|
|
@@ -67,10 +83,12 @@ const exportNotes = Command.make(
|
|
|
67
83
|
// Context of your own for a failure, on stderr before it. The line prints for every
|
|
68
84
|
// failure of the command, a file that cannot be written included, so it says only what is
|
|
69
85
|
// true of them all.
|
|
70
|
-
Effect.tapError(() =>
|
|
86
|
+
Effect.tapError(() =>
|
|
87
|
+
Console.error(`The export to ${file} did not finish.`)
|
|
88
|
+
),
|
|
71
89
|
// So the export fails as a derived command does.
|
|
72
|
-
ActionCli.toUserError(Http)
|
|
73
|
-
)
|
|
90
|
+
ActionCli.toUserError(Http)
|
|
91
|
+
)
|
|
74
92
|
).pipe(Command.withDescription("Write every note to a file, as Markdown."));
|
|
75
93
|
|
|
76
94
|
// The other half of `export`: it reads such a file and creates its notes with one call of the
|
|
@@ -84,19 +102,22 @@ const importNotes = Command.make(
|
|
|
84
102
|
"import",
|
|
85
103
|
{
|
|
86
104
|
text: Argument.FileText("file").pipe(
|
|
87
|
-
Argument.withDescription("A Markdown file as `notes export` writes it.")
|
|
105
|
+
Argument.withDescription("A Markdown file as `notes export` writes it.")
|
|
88
106
|
),
|
|
89
107
|
},
|
|
90
108
|
({ text }) =>
|
|
91
109
|
Effect.gen(function* () {
|
|
92
110
|
const client = yield* api;
|
|
93
111
|
|
|
94
|
-
if (text === "")
|
|
112
|
+
if (text === "") {
|
|
113
|
+
return yield* Console.log("Created 0 notes");
|
|
114
|
+
}
|
|
95
115
|
|
|
96
116
|
if (!text.startsWith("# ")) {
|
|
97
117
|
return yield* new CliError.UserError({
|
|
98
118
|
cause: text.split("\n", 1)[0],
|
|
99
|
-
userMessage:
|
|
119
|
+
userMessage:
|
|
120
|
+
"The file does not start with a `# ` title, as `notes export` writes one.",
|
|
100
121
|
});
|
|
101
122
|
}
|
|
102
123
|
|
|
@@ -109,9 +130,11 @@ const importNotes = Command.make(
|
|
|
109
130
|
const { notes: created } = yield* client.importNotes({ notes });
|
|
110
131
|
|
|
111
132
|
yield* Console.log(`Created ${created.length} notes`);
|
|
112
|
-
}).pipe(ActionCli.toUserError(Http))
|
|
133
|
+
}).pipe(ActionCli.toUserError(Http))
|
|
113
134
|
).pipe(
|
|
114
|
-
Command.withDescription(
|
|
135
|
+
Command.withDescription(
|
|
136
|
+
"Create the notes of a file `export` wrote, all or none, at most 100."
|
|
137
|
+
)
|
|
115
138
|
);
|
|
116
139
|
|
|
117
140
|
// A command per action of the binding, a flag per input field, each calling the server over
|
|
@@ -123,7 +146,10 @@ export const commands = ActionCli.remote(Http, {
|
|
|
123
146
|
commands: {
|
|
124
147
|
// `notes create-note --title Draft < draft.md`: the body is piped in, not a flag.
|
|
125
148
|
createNote: { stdin: "body" },
|
|
126
|
-
getNote: {
|
|
149
|
+
getNote: {
|
|
150
|
+
positional: ["id"],
|
|
151
|
+
render: (note) => `# ${note.title}\n\n${note.body}`,
|
|
152
|
+
},
|
|
127
153
|
// `notes update-note <id> --revision 1 < draft.md`: an argument, a flag and stdin together.
|
|
128
154
|
updateNote: { positional: ["id"], stdin: "body" },
|
|
129
155
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Context, Schema } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
3
2
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
3
|
+
import { Context, Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
export const scopes = { read: "notes:read", write: "notes:write" } as const;
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ export interface User {
|
|
|
16
16
|
* provides it per request, and a local host, such as the stdio server, provides it itself.
|
|
17
17
|
*/
|
|
18
18
|
export class CurrentUser extends Context.Service<CurrentUser, User>()(
|
|
19
|
-
"notes-service/CurrentUser"
|
|
19
|
+
"notes-service/CurrentUser"
|
|
20
20
|
) {}
|
|
21
21
|
|
|
22
22
|
// How a remote caller proves it is CurrentUser: a bearer token, the default scheme. Clients
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
3
4
|
import { CurrentUser, scopes } from "./Identity.js";
|
|
4
5
|
|
|
5
6
|
// The one rule, read from each contract: a read needs the read scope, anything else the write
|
|
@@ -11,6 +12,9 @@ export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
|
|
|
11
12
|
const user = yield* CurrentUser;
|
|
12
13
|
|
|
13
14
|
if (!user.scopes.includes(scope)) {
|
|
14
|
-
return yield* new Action.Forbidden({
|
|
15
|
+
return yield* new Action.Forbidden({
|
|
16
|
+
message: `Requires ${scope}.`,
|
|
17
|
+
scopes: [scope],
|
|
18
|
+
});
|
|
15
19
|
}
|
|
16
20
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
3
4
|
import { authorize } from "./authorize.js";
|
|
4
5
|
import { CurrentUser, WhoAmI } from "./Identity.js";
|
|
5
6
|
|
|
6
7
|
export const identity = Action.implement(
|
|
7
8
|
WhoAmI,
|
|
8
9
|
() => Effect.map(CurrentUser, ({ id, scopes }) => ({ id, scopes })),
|
|
9
|
-
{ authorize }
|
|
10
|
+
{ authorize }
|
|
10
11
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { scopes
|
|
1
|
+
import { scopes } from "./Identity.js";
|
|
2
|
+
import type { User } from "./Identity.js";
|
|
2
3
|
|
|
3
4
|
// DEMO ONLY: the people who may sign in, each under a token that is their name. A real service
|
|
4
5
|
// has no such table: its verifier reads the user from a signed token.
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import { Config, Effect, Redacted } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
3
2
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
3
|
+
import { Config, Effect, Redacted } from "effect";
|
|
4
|
+
|
|
4
5
|
import { Login, scopes } from "./Identity.js";
|
|
5
6
|
import { users } from "./users.js";
|
|
6
7
|
|
|
7
|
-
const isToken = (token: string): token is keyof typeof users =>
|
|
8
|
+
const isToken = (token: string): token is keyof typeof users =>
|
|
9
|
+
Object.hasOwn(users, token);
|
|
8
10
|
|
|
9
11
|
// DEMO ONLY: a token is a user's name. A real verifier checks a signed token with your
|
|
10
12
|
// authorization server's library, its audience included. An unknown token is the built-in
|
|
11
13
|
// `Unauthenticated`: a 401 every client decodes.
|
|
12
|
-
const verify = (token: Redacted.Redacted
|
|
14
|
+
const verify = (token: Redacted.Redacted) => {
|
|
13
15
|
const name = Redacted.value(token);
|
|
14
16
|
|
|
15
17
|
return isToken(name)
|
|
16
18
|
? Effect.succeed(users[name])
|
|
17
|
-
: Effect.fail(
|
|
19
|
+
: Effect.fail(
|
|
20
|
+
new Action.Unauthenticated({ message: "Unknown demo token." })
|
|
21
|
+
);
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
// Server only: `Login`'s provider. As an OAuth protected resource it also publishes discovery,
|
|
@@ -22,7 +26,9 @@ const verify = (token: Redacted.Redacted<string>) => {
|
|
|
22
26
|
// is the server's public URL, known only at startup, so it is read from config.
|
|
23
27
|
export const authenticate = Authentication.layer(Login, verify, {
|
|
24
28
|
protectedResource: Effect.map(
|
|
25
|
-
Config.String("PUBLIC_URL").pipe(
|
|
29
|
+
Config.String("PUBLIC_URL").pipe(
|
|
30
|
+
Config.withDefault("http://localhost:3000")
|
|
31
|
+
),
|
|
26
32
|
(url) =>
|
|
27
33
|
({
|
|
28
34
|
resource: `${url}/mcp`,
|
|
@@ -30,6 +36,6 @@ export const authenticate = Authentication.layer(Login, verify, {
|
|
|
30
36
|
authorizationServers: ["https://auth.example.com"],
|
|
31
37
|
scopesSupported: [scopes.read, scopes.write],
|
|
32
38
|
scopesRequired: [scopes.read],
|
|
33
|
-
}) satisfies Authentication.ProtectedResource
|
|
39
|
+
}) satisfies Authentication.ProtectedResource
|
|
34
40
|
),
|
|
35
41
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
3
4
|
import { CurrentUser } from "../identity/Identity.js";
|
|
4
5
|
|
|
5
6
|
export const Note = Schema.Struct({
|
|
@@ -13,23 +14,24 @@ export type Note = typeof Note.Type;
|
|
|
13
14
|
|
|
14
15
|
// A declared error is part of the contract: every client decodes it as this class, and its
|
|
15
16
|
// status is what HTTP answers with.
|
|
16
|
-
export class NoteNotFound extends Schema.TaggedError<
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
export class NoteNotFound extends Schema.TaggedError<
|
|
18
|
+
NoteNotFound,
|
|
19
|
+
{ readonly brand: unique symbol }
|
|
20
|
+
>()("NoteNotFound", { id: Schema.String }, { httpApiStatus: 404 }) {}
|
|
21
|
+
|
|
22
|
+
export class NoteChanged extends Schema.TaggedError<
|
|
23
|
+
NoteChanged,
|
|
24
|
+
{ readonly brand: unique symbol }
|
|
25
|
+
>()(
|
|
23
26
|
"NoteChanged",
|
|
24
27
|
{ id: Schema.String, revision: Schema.Finite },
|
|
25
|
-
{ httpApiStatus: 409 }
|
|
28
|
+
{ httpApiStatus: 409 }
|
|
26
29
|
) {}
|
|
27
30
|
|
|
28
|
-
export class RequestConflict extends Schema.TaggedError<
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
) {}
|
|
31
|
+
export class RequestConflict extends Schema.TaggedError<
|
|
32
|
+
RequestConflict,
|
|
33
|
+
{ readonly brand: unique symbol }
|
|
34
|
+
>()("RequestConflict", { requestId: Schema.String }, { httpApiStatus: 409 }) {}
|
|
33
35
|
|
|
34
36
|
// A description on an input field is that field's help on every surface: beside its flag or
|
|
35
37
|
// argument in a command's `--help`, and in the input schema a model reads from the MCP tool
|
|
@@ -42,7 +44,9 @@ const Id = Schema.String.annotate({
|
|
|
42
44
|
// generic "Expected a value with a length of at least 1". A model and a CLI user read it too.
|
|
43
45
|
const Title = Schema.String.check(
|
|
44
46
|
Schema.isMinLength(1, { message: "A note needs a title." }),
|
|
45
|
-
Schema.isPattern(/^[^\r\n\u2028\u2029]*$/u, {
|
|
47
|
+
Schema.isPattern(/^[^\r\n\u2028\u2029]*$/u, {
|
|
48
|
+
message: "A title is one line.",
|
|
49
|
+
})
|
|
46
50
|
).annotate({
|
|
47
51
|
description: "A short name for the note: one line, at least one character.",
|
|
48
52
|
});
|
|
@@ -53,7 +57,11 @@ const Content = Schema.Struct({ title: Title, body: Body });
|
|
|
53
57
|
|
|
54
58
|
export const ListNotes = Action.make("listNotes", {
|
|
55
59
|
description: "List your notes, without their bodies.",
|
|
56
|
-
success: {
|
|
60
|
+
success: {
|
|
61
|
+
notes: Schema.Array(
|
|
62
|
+
Schema.Struct({ id: Schema.String, title: Schema.String })
|
|
63
|
+
),
|
|
64
|
+
},
|
|
57
65
|
readOnly: true,
|
|
58
66
|
caller: CurrentUser,
|
|
59
67
|
});
|
|
@@ -90,9 +98,12 @@ export const UpdateNote = Action.make("updateNote", {
|
|
|
90
98
|
"Replace a note's body. Pass the revision you read: the update is refused if the note changed since.",
|
|
91
99
|
input: {
|
|
92
100
|
id: Id,
|
|
93
|
-
body: Body.annotate({
|
|
101
|
+
body: Body.annotate({
|
|
102
|
+
description: "The text that replaces the note's body.",
|
|
103
|
+
}),
|
|
94
104
|
revision: Schema.Finite.annotate({
|
|
95
|
-
description:
|
|
105
|
+
description:
|
|
106
|
+
"The revision you read: 1 for a new note, one more after each update.",
|
|
96
107
|
}),
|
|
97
108
|
},
|
|
98
109
|
success: Note,
|
|
@@ -120,10 +131,16 @@ export const ImportNotes = Action.make("importNotes", {
|
|
|
120
131
|
input: {
|
|
121
132
|
notes: Schema.Array(Content)
|
|
122
133
|
.check(
|
|
123
|
-
Schema.isMinLength(1, {
|
|
124
|
-
|
|
134
|
+
Schema.isMinLength(1, {
|
|
135
|
+
message: "An import holds at least one note.",
|
|
136
|
+
}),
|
|
137
|
+
Schema.isMaxLength(100, {
|
|
138
|
+
message: "An import holds at most 100 notes.",
|
|
139
|
+
})
|
|
125
140
|
)
|
|
126
|
-
.annotate({
|
|
141
|
+
.annotate({
|
|
142
|
+
description: "The notes to create, in order: from 1 to 100.",
|
|
143
|
+
}),
|
|
127
144
|
},
|
|
128
145
|
success: { notes: Schema.Array(Note) },
|
|
129
146
|
readOnly: false,
|
|
@@ -131,4 +148,11 @@ export const ImportNotes = Action.make("importNotes", {
|
|
|
131
148
|
mcp: { destructiveHint: false },
|
|
132
149
|
});
|
|
133
150
|
|
|
134
|
-
export const Notes = [
|
|
151
|
+
export const Notes = [
|
|
152
|
+
ListNotes,
|
|
153
|
+
GetNote,
|
|
154
|
+
CreateNote,
|
|
155
|
+
UpdateNote,
|
|
156
|
+
DeleteNote,
|
|
157
|
+
ImportNotes,
|
|
158
|
+
] as const;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
3
4
|
import { authorize } from "../identity/authorize.js";
|
|
4
5
|
import { CurrentUser } from "../identity/Identity.js";
|
|
5
6
|
import { Notes } from "./Notes.js";
|
|
@@ -22,18 +23,26 @@ export const notes = Action.implement(
|
|
|
22
23
|
|
|
23
24
|
return { notes: owned.map(({ id, title }) => ({ id, title })) };
|
|
24
25
|
}),
|
|
25
|
-
getNote: ({ id }) =>
|
|
26
|
+
getNote: ({ id }) =>
|
|
27
|
+
Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
|
|
26
28
|
createNote: ({ requestId, title, body }) =>
|
|
27
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
29
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
30
|
+
store.create(user.id, title, body, requestId)
|
|
31
|
+
),
|
|
28
32
|
updateNote: ({ id, body, revision }) =>
|
|
29
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
34
|
+
store.update(user.id, id, body, revision)
|
|
35
|
+
),
|
|
36
|
+
deleteNote: ({ id }) =>
|
|
37
|
+
Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
|
|
38
|
+
importNotes: ({ notes: imported }) =>
|
|
32
39
|
Effect.map(
|
|
33
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
34
|
-
|
|
40
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
41
|
+
store.createAll(user.id, imported)
|
|
42
|
+
),
|
|
43
|
+
(created) => ({ notes: created })
|
|
35
44
|
),
|
|
36
45
|
};
|
|
37
46
|
}),
|
|
38
|
-
{ authorize }
|
|
47
|
+
{ authorize }
|
|
39
48
|
);
|