History

init

Rhys Sullivan committed 9 months ago

Commit 29fe127No parent (first commit)33 files · +2632 −0Browse files

Unified · Split

33 files
  1. .context/openclaw +1 −0
  2. .context/opensrc-mcp +1 −0
  3. .gitignore +36 −0
  4. AGENTS.md +1 −0
  5. CLAUDE.md +120 −0
  6. OPENASSISTANT_HIGH_LEVEL_PLAN.md +309 −0
  7. OPENASSISTANT_MVP_PRIMITIVES.md +324 −0
  8. README.md +15 −0
  9. apps/discord-bot/package.json +22 −0
  10. apps/discord-bot/src/approval-bridge.ts +109 −0
  11. apps/discord-bot/src/approval-registry.test.ts +38 −0
  12. apps/discord-bot/src/approval-registry.ts +67 −0
  13. apps/discord-bot/src/calendar-store.ts +32 −0
  14. apps/discord-bot/src/codegen.test.ts +77 −0
  15. apps/discord-bot/src/codegen.ts +322 −0
  16. apps/discord-bot/src/format-response.ts +61 −0
  17. apps/discord-bot/src/index.ts +102 −0
  18. apps/discord-bot/src/tools.ts +22 −0
  19. apps/discord-bot/tsconfig.json +9 −0
  20. apps/gateway/package.json +21 −0
  21. apps/gateway/src/index.ts +10 −0
  22. apps/gateway/tsconfig.json +9 −0
  23. bun.lock +384 −0
  24. index.ts +1 −0
  25. package.json +30 −0
  26. packages/core/package.json +17 −0
  27. packages/core/src/codemode/runner.test.ts +117 −0
  28. packages/core/src/codemode/runner.ts +318 −0
  29. packages/core/src/index.ts +4 −0
  30. packages/core/tsconfig.json +9 −0
  31. tsconfig.base.json +22 −0
  32. tsconfig.json +14 −0
  33. vitest.config.ts +8 −0
.context/openclaw+1−0
@@ -0,0 +1 @@
1Subproject commit 6f4665dda3af787c50685e440cca21a57816b5a6
.context/opensrc-mcp+1−0
@@ -0,0 +1 @@
1Subproject commit 43c45b2cbe0e64fb7859164a28e29da427c37930
.gitignore+36−0
@@ -0,0 +1,36 @@
1# dependencies (bun install)
2node_modules
3
4# output
5out
6dist
7*.tgz
8
9# code coverage
10coverage
11*.lcov
12
13# logs
14logs
15_.log
16report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17
18# dotenv environment variable files
19.env
20.env.production
21.env.development.local
22.env.test.local
23.env.production.local
24.env.local
25
26# caches
27.eslintcache
28.cache
29*.tsbuildinfo
30.reference/
31
32# IntelliJ based IDEs
33.idea
34
35# Finder (MacOS) folder config
36.DS_Store
AGENTS.md+1−0
@@ -0,0 +1 @@
1CLAUDE.md
CLAUDE.md+120−0
@@ -0,0 +1,120 @@
1
2Default to using Bun instead of Node.js.
3
4- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
5- Use `bun test` instead of `jest` or `vitest`
6- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
7- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
8- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
9- Use `bunx <package> <command>` instead of `npx <package> <command>`
10- Bun automatically loads .env, so don't use dotenv.
11
12## APIs
13
14- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16- `Bun.redis` for Redis. Don't use `ioredis`.
17- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18- `WebSocket` is built-in. Don't use `ws`.
19- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20- Bun.$`ls` instead of execa.
21
22## Testing
23
24Use `bun test` to run tests.
25
26```ts#index.test.ts
27import { test, expect } from "bun:test";
28
29test("hello world", () => {
30 expect(1).toBe(1);
31});
32```
33
34## Frontend
35
36Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37
38Server:
39
40```ts#index.ts
41import index from "./index.html"
42
43Bun.serve({
44 routes: {
45 "/": index,
46 "/api/users/:id": {
47 GET: (req) => {
48 return new Response(JSON.stringify({ id: req.params.id }));
49 },
50 },
51 },
52 // optional websocket support
53 websocket: {
54 open: (ws) => {
55 ws.send("Hello, world!");
56 },
57 message: (ws, message) => {
58 ws.send(message);
59 },
60 close: (ws) => {
61 // handle close
62 }
63 },
64 development: {
65 hmr: true,
66 console: true,
67 }
68})
69```
70
71HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
72
73```html#index.html
74<html>
75 <body>
76 <h1>Hello, world!</h1>
77 <script type="module" src="./frontend.tsx"></script>
78 </body>
79</html>
80```
81
82With the following `frontend.tsx`:
83
84```tsx#frontend.tsx
85import React from "react";
86import { createRoot } from "react-dom/client";
87
88// import .css files directly and it works
89import './index.css';
90
91const root = createRoot(document.body);
92
93export default function Frontend() {
94 return <h1>Hello, world!</h1>;
95}
96
97root.render(<Frontend />);
98```
99
100Then, run index.ts
101
102```sh
103bun --hot ./index.ts
104```
105
106For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
107
108<!-- effect-solutions:start -->
109## Effect Best Practices
110
111**IMPORTANT:** Always consult effect-solutions before writing Effect code.
112
1131. Run `effect-solutions list` to see available guides
1142. Run `effect-solutions show <topic>...` for relevant patterns (supports multiple topics)
1153. Search `.reference/effect/` for real implementations (run `effect-solutions setup` first)
116
117Topics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli.
118
119Never guess at Effect patterns - check the guide first.
120<!-- effect-solutions:end -->
OPENASSISTANT_HIGH_LEVEL_PLAN.md+309−0
@@ -0,0 +1,309 @@
1# OpenAssistant: High-Level Architecture Plan
2
3## 1) Product Thesis
4
5Build a personal agent platform with OpenClaw-like flexibility, but with a strict trust model:
6
7- No free-form shell as the default execution surface.
8- Actions run through typed TypeScript clients with explicit capability boundaries.
9- Destructive operations require approval by policy.
10- Every claim from the model is backed by auditable execution evidence.
11
12## 2) Core Principles
13
14- Capability-first execution: the model can only call registered client methods.
15- Policy as code: permissions, approval gates, and risk tiers are explicit config.
16- Evidence over narration: the assistant response is generated from execution receipts.
17- Durable automation: reminders/schedules are owned by scheduler state, not model memory.
18- Portable runtime: Bun + TypeScript monorepo, predictable local and remote deploy paths.
19
20## 3) What To Keep vs Replace
21
22Keep from OpenClaw:
23
24- Gateway + session architecture.
25- Multi-client model (Discord, CLI, web, device nodes).
26- Event stream for system and execution updates.
27- Cron/heartbeat concept for always-on automation.
28
29Replace:
30
31- Bash-first action model.
32- Implicit tool use visibility.
33- Broad account credentials without fine-grained scope boundaries.
34
35## 4) System Shape
36
37Control plane:
38
39- `gateway`: authoritative orchestration service.
40- `policy-engine`: evaluates if a call is auto-allowed, approval-required, or denied.
41- `approval-service`: manages pending approvals, TTL, decisions, audit trail.
42- `scheduler`: durable jobs, reminders, retries, dead-letter.
43- `event-log`: append-only event stream and query API for UI and audits.
44
45Execution plane:
46
47- `code-runner`: executes generated TypeScript in sandboxed runtime.
48- `client-runtime`: injects typed clients into script context.
49- `tool-host-local`: local client execution with local creds.
50- `tool-host-remote`: remote tool execution with scoped ephemeral creds.
51
52Interaction plane:
53
54- `chat-clients`: Discord/web/CLI channels.
55- `ops-ui`: approvals, receipts, schedules, policy edits, run timelines.
56
57## 5) Execution Model (Codemode + Typed Clients)
58
59Single model-facing tool:
60
61- `run_code({ code, intent, sessionId })`.
62
63Injected runtime in script:
64
65- `clients.<integration>.<method>(input)`
66- `Result<T, E>` return style.
67- No `child_process`, no `fetch` directly, no unrestricted FS/network.
68
69Example shape (conceptual):
70
71```ts
72const result = await clients.calendar.updateEvent({ id, patch })
73if (result.isError()) {
74 return Result.err(result.error)
75}
76return Result.ok(result.value)
77```
78
79Outcome:
80
81- OpenClaw-level composability.
82- MCP/tool-call-style control boundaries.
83- Better determinism and auditability than shell command synthesis.
84
85## 6) Capability and Permission Model
86
87Permission unit:
88
89- `integration.resource.action` (example: `vercel.firewall.rule.delete`).
90
91Client method metadata:
92
93- `risk`: `read | write | destructive | privileged`.
94- `autoRun`: boolean default.
95- `requiresApproval`: optional override.
96- `idempotent`: boolean.
97- `allowedScopes`: account/project/environment constraints.
98
99Policy decision:
100
101- `allow` if method scope and risk pass policy.
102- `ask` if action is sensitive or outside auto-run envelope.
103- `deny` if forbidden by capability map.
104
105## 7) Approval Flow
106
107Request path:
108
1091. Script calls client method.
1102. Runtime asks policy engine.
1113. If `ask`, execution pauses with `ApprovalPending` receipt.
1124. Approval prompt sent to origin chat + optional ops targets.
1135. Approver chooses `allow-once`, `allow-window`, `deny`, or `allow-and-policy-update`.
1146. Execution resumes or returns denial as value.
115
116Key UX requirements:
117
118- User always sees which exact method and arguments are pending.
119- Expiration and timeout reason are explicit.
120- Run status updates: `pending`, `running`, `completed`, `denied`, `failed`.
121
122## 8) Receipts and Anti-Lying Guarantees
123
124Every side-effectful call emits immutable receipts:
125
126- `call.requested`
127- `call.approved|call.denied`
128- `call.started`
129- `call.finished|call.failed`
130
131Assistant response rule:
132
133- For external actions, summarize from receipts only.
134- If no receipt exists, assistant must state action was not completed.
135
136UI requirement:
137
138- Inline "did it really run?" evidence panel per assistant answer.
139
140## 9) Scheduler and Reminders
141
142Scheduler owns state, not the LLM:
143
144- Reminders become scheduler jobs with a stable `jobId`.
145- Jobs are persisted in a durable store.
146- Delivery attempts and outcomes are logged as receipts.
147- If delivery target fails, retries/backoff are deterministic.
148
149Contract:
150
151- "Remind me tomorrow at 9am" returns a created job receipt immediately.
152- User can query, edit, pause, or delete by `jobId`.
153
154## 10) OpenAPI to Client Pipeline
155
156Goal:
157
158- Convert API specs into safe typed clients and policy metadata.
159
160Pipeline:
161
1621. Ingest OpenAPI spec.
1632. Generate TypeScript client methods.
1643. Classify methods by risk heuristics and optional provider-specific rules.
1654. Emit package with runtime validators + method metadata.
1665. Register integration in gateway catalog.
167
168Outputs per integration package:
169
170- `client.ts` typed method surface.
171- `schema.ts` input/output validation.
172- `policy.ts` default risk/approval metadata.
173- `README.md` capability list and example usage.
174
175## 11) Remote Tools and Ephemeral Credentials
176
177Remote execution model:
178
179- Gateway requests short-lived scoped token from credential broker.
180- Tool host executes only permitted client methods.
181- Token TTL and scope are embedded in receipt for audit.
182
183Security baseline:
184
185- No long-lived broad API keys in model runtime.
186- Per-tool/per-method scope limits.
187- Revocation and rotation built into credential broker.
188
189## 12) Monorepo Layout (Bun)
190
191```text
192apps/
193 gateway/
194 ops-ui/
195 discord-client/
196 cli/
197packages/
198 sdk/
199 runner/
200 policy-engine/
201 approval-service/
202 scheduler/
203 event-log/
204 receipt-model/
205 integration-registry/
206 integration-openapi-gen/
207 integrations/
208 posthog/
209 github/
210 vercel/
211 gcal/
212 transport-ws/
213 transport-http/
214 shared-types/
215```
216
217## 13) Data Model (High-Level)
218
219Core entities:
220
221- `Session`
222- `Run`
223- `Script`
224- `ToolCall`
225- `ApprovalRequest`
226- `Receipt`
227- `Job`
228- `CredentialLease`
229- `PolicySnapshot`
230
231Strong recommendation:
232
233- Event-sourced append log for execution and approvals.
234- Materialized read views for chat timeline and dashboard.
235
236## 14) MVP Scope (Phase 1)
237
238Include:
239
240- Gateway + WS event stream.
241- Code runner with sandboxed TS execution.
242- Typed integration runtime (manual first: GitHub + Calendar + PostHog).
243- Policy engine with `read/write/destructive`.
244- Approval UI in Discord + lightweight web panel.
245- Scheduler with reminders and hourly monitors.
246- Receipt-backed final answer generation.
247
248Exclude initially:
249
250- General bash execution.
251- Arbitrary plugin execution from untrusted sources.
252- Complex multi-tenant org model.
253
254## 15) Phase Plan
255
256Phase A: Foundation
257
258- Monorepo skeleton, shared types, event bus, gateway sessions.
259- Runner sandbox and `run_code` API.
260
261Phase B: Trust Layer
262
263- Policy engine, approvals, receipt model, auditable run timeline.
264
265Phase C: Integrations
266
267- Manual clients + openapi codegen pipeline.
268- Scoped credentials and remote tool host.
269
270Phase D: Automation
271
272- Scheduler, durable reminders, monitors, retry semantics.
273
274Phase E: UX
275
276- Discord + web ops surface for approvals and evidence links.
277
278## 16) Key Risks and Mitigations
279
280Risk: Model-generated TS can still be unsafe.
281Mitigation: hard sandbox + no direct network/process APIs + capability-injected clients only.
282
283Risk: Approval fatigue.
284Mitigation: risk tiers, approval windows, per-integration trusted scopes.
285
286Risk: "Says it did it" regressions.
287Mitigation: receipt-backed response contract + explicit uncertainty language.
288
289Risk: Credential sprawl.
290Mitigation: brokered ephemeral tokens and strict method-level scopes.
291
292## 17) First Technical Decision Set
293
294Decide now:
295
296- Bun runtime boundary for runner isolation.
297- Storage stack for append log + views.
298- Event transport protocol (WS-first, HTTP fallback).
299- Policy DSL format (JSON policy vs TS policy modules).
300- Credential broker interface and lease format.
301
302## 18) Success Criteria
303
304You should be able to do all of the following with evidence:
305
306- "Monitor PostHog and ping me at 10M" (job created, triggered, delivered).
307- "Check Vercel anomaly email and advise" (reads logged, recommendation traceable).
308- "Change firewall rule" (approval prompted, approved actor recorded, call receipt linked).
309- "Remind me tomorrow at 9" (deterministic scheduler receipt, guaranteed delivery attempts).
OPENASSISTANT_MVP_PRIMITIVES.md+324−0
@@ -0,0 +1,324 @@
1# OpenAssistant MVP: Primitive-First Plan
2
3## Goal
4
5Ship an MVP that is:
6
7- Small enough to build fast.
8- Safe enough to trust.
9- Extensible enough to add integrations later.
10
11Core rule: the system does not guarantee outcomes, it guarantees evidence of attempted/completed calls to integrations.
12
13## Non-Goals (MVP)
14
15- No default reminder/calendar system.
16- No giant plugin marketplace.
17- No many-package architecture upfront.
18- No bash-first execution path.
19
20## MVP Building Blocks (Primitives)
21
22### 1) `Run Primitive`
23
24Executes model-generated TypeScript in a sandbox with a single injected surface:
25
26- `tools.<integration>.<method>(input): Result<ok, err>`
27
28No direct shell/network/process access from model code.
29
30### 2) `Tool Contract Primitive`
31
32Each tool method has metadata:
33
34- `id`: `integration.resource.action`
35- `risk`: `read | write | destructive`
36- `requiresApproval`: boolean or policy-driven
37- `schema`: input/output validation
38
39This is the capability boundary.
40
41### 3) `Policy + Approval Primitive`
42
43Policy returns one of:
44
45- `allow`
46- `ask`
47- `deny`
48
49If `ask`, run pauses until explicit decision (`allow-once`, `deny`, optional short-lived window).
50
51### 4) `Receipt Primitive` (Proof of Work)
52
53Append-only receipts for every tool interaction:
54
55- `tool.call.requested`
56- `tool.call.approved | tool.call.denied` (if applicable)
57- `tool.call.started`
58- `tool.call.succeeded | tool.call.failed`
59
60These receipts are the source of truth for what actually happened.
61
62### 5) `Relevant Tool Activity Primitive` (Evidence Surface)
63
64The gateway derives a system-generated summary of relevant tool activity from receipts.
65
66- This summary is passed to the model for final response generation.
67- This same summary is surfaced to the user/client.
68- The summary is filtered to relevant calls, not a full noisy log.
69
70Summary fields (MVP):
71
72- `toolId`
73- `status` (`succeeded | failed | denied | pending`)
74- `approval` (`required/decision` when applicable)
75- `when` (timestamp)
76- `receiptRef` (id/link for audit trail)
77
78Relevance rules (MVP):
79
80- Include calls from the current run.
81- Always include failed/denied/pending calls.
82- Include side-effectful calls (`write | destructive`).
83- Optionally include key reads used to justify decisions.
84
85This is the anti-lying mechanism: responses are grounded in gateway-produced evidence summaries, not free model claims.
86
87### 6) `Response Grounding Primitive`
88
89Assistant action statements should be based on the relevant tool activity summary.
90
91If no successful call exists for an intended action, response must say it could not verify completion.
92
93### 7) `Tool Workspace Primitive` (User-Authored Tools)
94
95Users (or the agent) can create tools in an assistant workspace directory.
96
97- Tool files are TypeScript modules using a shared `defineTool()` contract.
98- Each tool exports metadata, schema, and handler.
99- Gateway discovers and reloads tools from the workspace on file change or explicit reload.
100
101This enables "write your own tools" without touching core runtime code.
102
103### 8) `Auth Profile Primitive` (Claude Max Support)
104
105MVP supports Anthropic Claude Max the same way OpenClaw does:
106
107- User runs `claude setup-token` outside the app.
108- User pastes token into OpenAssistant auth command.
109- Token is stored as an auth profile of `type: "token"` for provider `anthropic`.
110- Model runs resolve credentials from profile order first, then env fallback.
111
112This avoids coupling to Claude CLI internals while still leveraging Claude Max plan auth.
113
114## Minimal Repository Shape
115
116Keep it simple first:
117
118```text
119apps/
120 gateway/ # chat loop, run orchestration, ws/http API
121 discord-client/ # primary MVP chat + approval interaction surface
122packages/
123 core/ # shared types, Result, receipt models, policy types
124 runtime/ # TS runner sandbox + tool bridge
125 sdk/ # defineTool(), integration client wrappers
126integrations/
127 example-calendar/
128 example-posthog/
129```
130
131Only 3 packages. Add more only when pain is real.
132
133Notes:
134
135- `ops` means operator surface (approvals + receipts), not necessarily a web app.
136- MVP should use Discord as the operator surface first to avoid UI overhead.
137- Optional web ops UI can be phase 2.
138
139## Data Model (MVP)
140
141- `Run { id, sessionId, status, startedAt, endedAt }`
142- `ToolCall { id, runId, toolId, inputHash, risk }`
143- `ApprovalRequest { id, toolCallId, status, decidedBy, decidedAt }`
144- `Receipt { id, runId, toolCallId?, type, ts, payload }`
145- `RelevantToolActivity { runId, items[] }` (derived view from receipts)
146- `AuthProfile { id, provider, type, secretRef, expiresAt?, lastUsedAt?, cooldownUntil? }`
147
148Use one append-only receipts table/log first. Build read views later.
149
150## How “Reminder Truthfulness” Works Without Built-In Reminder System
151
152User says: “Remind me tomorrow at 9.”
153
154MVP behavior:
155
1561. Model chooses an integration tool (calendar/todo/automation/etc).
1572. Runtime executes that tool call.
1583. Receipt(s) are written.
1594. Gateway derives relevant tool activity summary.
1605. Assistant response + client surface that summary.
161
162So:
163
164- If scheduling tool call is `succeeded`, response can say it was scheduled and show that tool call.
165- If scheduling call is `failed/denied/pending`, response should say that and surface the relevant call.
166
167No assumption about which reminder provider is used.
168
169## User-Authored Tool Flow (MVP)
170
171Assistant workspace (example):
172
173```text
174~/.config/openassistant/
175 tools/
176 posthog/
177 client.ts
178 tools.ts
179```
180
181Tool definition shape (conceptual):
182
183```ts
184export const checkVisitors = defineTool({
185 id: "posthog.analytics.getVisitors",
186 risk: "read",
187 input: z.object({ projectId: z.string(), website: z.string() }),
188 policy: { default: "allow" },
189 handler: async (input, ctx) => { /* call posthog */ },
190})
191
192export const createThresholdMonitor = defineTool({
193 id: "posthog.monitor.createThresholdJob",
194 risk: "write",
195 input: z.object({
196 website: z.string(),
197 threshold: z.number(),
198 cron: z.string(), // e.g. daily 9am
199 }),
200 policy: { default: "ask" },
201 handler: async (input, ctx) => { /* call scheduler or remote endpoint */ },
202})
203```
204
205Runtime flow:
206
2071. Prompt asks for PostHog threshold monitor.
2082. Agent writes/updates PostHog tool module in workspace.
2093. Gateway reloads tool registry.
2104. Run uses newly available tool methods.
2115. Every call emits receipts.
2126. Gateway computes relevant tool activity.
2137. Assistant + client surface relevant tool activity.
214
215What is guaranteed:
216
217- Proof that the tool endpoint was called and what happened.
218
219What is not guaranteed by core:
220
221- Which reminder/scheduler provider is used internally by that tool.
222
223## Claude Max Compatibility (OpenClaw Pattern)
224
225### Storage
226
227Auth store file (example):
228
229```text
230~/.config/openassistant/auth-profiles.json
231```
232
233Credential shape:
234
235```ts
236type AuthCredential =
237 | { type: "api_key"; provider: string; key: string }
238 | { type: "token"; provider: string; token: string; expires?: number }
239 | { type: "oauth"; provider: string; access: string; refresh: string; expires: number }
240```
241
242For Claude Max setup-token:
243
244- write profile like `anthropic:manual` with `type: "token"`.
245- mark config profile mode as `token` (oauth-compatible for provider auth logic).
246
247### CLI Surface (MVP)
248
249```bash
250openassistant auth anthropic setup-token
251```
252
253Flow:
254
2551. Prompt user to run `claude setup-token`.
2562. Validate token prefix/shape.
2573. Save token profile in auth store.
2584. Set profile as preferred for Anthropic provider in config/order.
259
260### Provider Auth Resolution
261
262Resolution order:
263
2641. Explicit profile override (if run/session pinned).
2652. Provider profile order from config/store.
2663. Env fallback (`ANTHROPIC_OAUTH_TOKEN`, then `ANTHROPIC_API_KEY`).
2674. Error with actionable re-auth instructions.
268
269### Runtime Behavior
270
271At run start:
272
273- Resolve provider auth once for selected model/provider.
274- Inject credential into model client adapter.
275
276On provider failures (rate-limit/auth/billing):
277
278- mark profile failure/cooldown.
279- rotate to next profile when available.
280- emit receipt/event that profile rotation occurred.
281
282This gives Claude Max usability plus long-term multi-profile resilience.
283
284## Extensibility Path
285
286New integration = add one folder under `integrations/*` implementing:
287
288- Tool contract metadata
289- Runtime client wrapper
290- Schema validation
291
292No core refactor needed if primitives are stable.
293
294## MVP Scope Checklist
295
296- Sandbox run primitive working.
297- Tool contract registry working.
298- Workspace tool discovery/reload working.
299- Policy/approval gate working.
300- Receipt log working and queryable.
301- Relevant tool activity summary derivation working.
302- Response grounding from relevant tool activity working.
303- Anthropic setup-token auth flow working (`claude setup-token` paste path).
304- Provider auth resolver supports profile order + env fallback.
305- Discord client handles chat + approvals + receipt links.
306- 2-3 integrations to prove composability.
307
308## Suggested Order (Build Sequence)
309
3101. `core`: types + receipt/event schema + policy decision enums.
3112. `runtime`: sandbox executor + injected tool bridge.
3123. `gateway`: run orchestration + tool registry + persistence + evidence summarization.
3134. `discord-client`: chat loop + approve/deny actions + receipt viewing links.
3145. `integrations`: add calendar/posthog/github as examples.
315
316## Success Criteria
317
318You can reliably demonstrate:
319
320- A destructive action pauses for approval and logs full receipt trail.
321- A non-destructive read auto-runs and logs receipt trail.
322- Assistant response includes relevant tool activity for claims it makes.
323- Assistant cannot claim tool success when relevant activity has no success state.
324- A reminder-style request is only “done” when an integration success receipt exists.
README.md+15−0
@@ -0,0 +1,15 @@
1# assistant
2
3To install dependencies:
4
5```bash
6bun install
7```
8
9To run:
10
11```bash
12bun run index.ts
13```
14
15This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
apps/discord-bot/package.json+22−0
@@ -0,0 +1,22 @@
1{
2 "name": "@openassistant/discord-bot",
3 "private": true,
4 "type": "module",
5 "scripts": {
6 "with-env": "bun run --env-file=../../.env --",
7 "with-env:prod": "bun run --env-file=../../.env.production --",
8 "dev": "bun with-env bun run src/index.ts",
9 "start": "bun with-env bun run src/index.ts",
10 "start:prod": "bun with-env:prod bun run src/index.ts",
11 "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json",
12 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""
13 },
14 "dependencies": {
15 "@openassistant/core": "workspace:*",
16 "ai": "^6.0.73",
17 "ai-sdk-provider-claude-code": "^3.3.6",
18 "discord.js": "^14.17.3",
19 "effect": "latest",
20 "zod": "^4.3.6"
21 }
22}
apps/discord-bot/src/approval-bridge.ts+109−0
@@ -0,0 +1,109 @@
1import type { ApprovalDecision, ApprovalRequest } from "@openassistant/core";
2import {
3 ActionRowBuilder,
4 ButtonBuilder,
5 ButtonStyle,
6 type ButtonInteraction,
7 type MessageCreateOptions,
8} from "discord.js";
9import { ApprovalRegistry } from "./approval-registry.js";
10
11const CUSTOM_ID_PREFIX = "oa.approval";
12
13export class DiscordApprovalBridge {
14 private readonly registry: ApprovalRegistry;
15
16 constructor(timeoutMs: number = 5 * 60_000) {
17 this.registry = new ApprovalRegistry(timeoutMs);
18 }
19
20 async requestApproval(params: {
21 request: ApprovalRequest;
22 channel: { send: (message: MessageCreateOptions) => Promise<unknown> };
23 requesterId: string;
24 }): Promise<ApprovalDecision> {
25 const { request, channel, requesterId } = params;
26 const pending = this.registry.open(request.callId, requesterId);
27
28 try {
29 const message: MessageCreateOptions = {
30 content: [
31 `Approval needed for \`${request.toolPath}\``,
32 `Call ID: \`${request.callId}\``,
33 request.inputPreview ? `Input: \`${request.inputPreview}\`` : null,
34 "Only the requesting user can approve or deny.",
35 ]
36 .filter(Boolean)
37 .join("\n"),
38 components: [approvalButtons(request.callId)],
39 };
40
41 await channel.send(message);
42 } catch {
43 this.registry.cancel(request.callId, "denied");
44 }
45
46 return pending;
47 }
48
49 async handleInteraction(interaction: ButtonInteraction): Promise<boolean> {
50 const parsed = parseCustomId(interaction.customId);
51 if (!parsed) {
52 return false;
53 }
54
55 const decision: ApprovalDecision = parsed.action === "approve" ? "approved" : "denied";
56 const status = this.registry.resolve(parsed.callId, interaction.user.id, decision);
57
58 if (status === "unauthorized") {
59 await interaction.reply({
60 content: "Only the requesting user can resolve this approval.",
61 ephemeral: true,
62 });
63 return true;
64 }
65
66 if (status === "not_found") {
67 await interaction.reply({
68 content: "This approval is no longer pending.",
69 ephemeral: true,
70 });
71 return true;
72 }
73
74 await interaction.update({
75 content: `Approval ${decision} by <@${interaction.user.id}> for \`${parsed.callId}\`.`,
76 components: [],
77 });
78 return true;
79 }
80}
81
82function approvalButtons(callId: string): ActionRowBuilder<ButtonBuilder> {
83 return new ActionRowBuilder<ButtonBuilder>().addComponents(
84 new ButtonBuilder()
85 .setCustomId(`${CUSTOM_ID_PREFIX}:${callId}:approve`)
86 .setLabel("Approve")
87 .setStyle(ButtonStyle.Success),
88 new ButtonBuilder()
89 .setCustomId(`${CUSTOM_ID_PREFIX}:${callId}:deny`)
90 .setLabel("Deny")
91 .setStyle(ButtonStyle.Danger),
92 );
93}
94
95function parseCustomId(customId: string): { callId: string; action: "approve" | "deny" } | null {
96 const parts = customId.split(":");
97 if (parts.length !== 3 || parts[0] !== CUSTOM_ID_PREFIX) {
98 return null;
99 }
100 const callId = parts[1];
101 if (!callId) {
102 return null;
103 }
104 const action = parts[2];
105 if (action !== "approve" && action !== "deny") {
106 return null;
107 }
108 return { callId, action };
109}
apps/discord-bot/src/approval-registry.test.ts+38−0
@@ -0,0 +1,38 @@
1import { describe, expect, it } from "vitest";
2import { ApprovalRegistry } from "./approval-registry.js";
3
4describe("ApprovalRegistry", () => {
5 it("resolves for requester", async () => {
6 const registry = new ApprovalRegistry(1_000);
7 const pending = registry.open("call_1", "user_a");
8
9 const result = registry.resolve("call_1", "user_a", "approved");
10 const decision = await pending;
11
12 expect(result).toBe("resolved");
13 expect(decision).toBe("approved");
14 expect(registry.size()).toBe(0);
15 });
16
17 it("rejects unauthorized actor", async () => {
18 const registry = new ApprovalRegistry(1_000);
19 const pending = registry.open("call_2", "user_a");
20
21 const unauthorized = registry.resolve("call_2", "user_b", "approved");
22 expect(unauthorized).toBe("unauthorized");
23 expect(registry.size()).toBe(1);
24
25 registry.cancel("call_2");
26 const decision = await pending;
27 expect(decision).toBe("denied");
28 });
29
30 it("times out to denied", async () => {
31 const registry = new ApprovalRegistry(10);
32 const pending = registry.open("call_3", "user_a");
33
34 const decision = await pending;
35 expect(decision).toBe("denied");
36 expect(registry.size()).toBe(0);
37 });
38});
apps/discord-bot/src/approval-registry.ts+67−0
@@ -0,0 +1,67 @@
1import type { ApprovalDecision } from "@openassistant/core";
2
3type PendingApproval = {
4 requesterId: string;
5 resolve: (decision: ApprovalDecision) => void;
6 timeout: ReturnType<typeof setTimeout>;
7};
8
9export type ResolveApprovalResult = "resolved" | "not_found" | "unauthorized";
10
11export class ApprovalRegistry {
12 private readonly pending = new Map<string, PendingApproval>();
13
14 constructor(private readonly timeoutMs: number = 5 * 60_000) {}
15
16 open(callId: string, requesterId: string): Promise<ApprovalDecision> {
17 if (this.pending.has(callId)) {
18 throw new Error(`Approval already pending: ${callId}`);
19 }
20
21 return new Promise<ApprovalDecision>((resolve) => {
22 const timeout = setTimeout(() => {
23 this.finish(callId, "denied");
24 }, this.timeoutMs);
25
26 this.pending.set(callId, {
27 requesterId,
28 resolve,
29 timeout,
30 });
31 });
32 }
33
34 resolve(callId: string, actorId: string, decision: ApprovalDecision): ResolveApprovalResult {
35 const entry = this.pending.get(callId);
36 if (!entry) {
37 return "not_found";
38 }
39 if (entry.requesterId !== actorId) {
40 return "unauthorized";
41 }
42
43 this.finish(callId, decision);
44 return "resolved";
45 }
46
47 cancel(callId: string, decision: ApprovalDecision = "denied"): void {
48 if (!this.pending.has(callId)) {
49 return;
50 }
51 this.finish(callId, decision);
52 }
53
54 size(): number {
55 return this.pending.size;
56 }
57
58 private finish(callId: string, decision: ApprovalDecision): void {
59 const entry = this.pending.get(callId);
60 if (!entry) {
61 return;
62 }
63 this.pending.delete(callId);
64 clearTimeout(entry.timeout);
65 entry.resolve(decision);
66 }
67}
apps/discord-bot/src/calendar-store.ts+32−0
@@ -0,0 +1,32 @@
1export interface CalendarEvent {
2 id: string;
3 title: string;
4 startsAt: string;
5 notes?: string;
6}
7
8export interface CalendarUpdateInput {
9 title: string;
10 startsAt: string;
11 notes?: string;
12}
13
14export class InMemoryCalendarStore {
15 private readonly events = new Map<string, CalendarEvent>();
16
17 update(input: CalendarUpdateInput): CalendarEvent {
18 const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
19 const event: CalendarEvent = {
20 id,
21 title: input.title,
22 startsAt: input.startsAt,
23 ...(input.notes ? { notes: input.notes } : {}),
24 };
25 this.events.set(id, event);
26 return event;
27 }
28
29 list(): CalendarEvent[] {
30 return [...this.events.values()];
31 }
32}
apps/discord-bot/src/codegen.test.ts+77−0
@@ -0,0 +1,77 @@
1import { describe, expect, it } from "vitest";
2import { generateCodeFromPrompt } from "./codegen.js";
3
4describe("generateCodeFromPrompt", () => {
5 it("maps calendar prompts with heuristic provider", async () => {
6 const now = new Date("2026-02-06T10:00:00.000Z");
7 const generated = await generateCodeFromPrompt(
8 "update my calendar to put dinner with ella at 5 pm",
9 { now, provider: "heuristic" },
10 );
11
12 expect(generated.provider).toBe("heuristic");
13 expect(generated.code).toContain("tools.calendar.update");
14 expect(generated.code.toLowerCase()).toContain("dinner with ella");
15 expect(generated.rationale).toContain("calendar.update");
16 });
17
18 it("supports direct code mode via code prefix", async () => {
19 const generated = await generateCodeFromPrompt("code: return 42;", {
20 provider: "heuristic",
21 });
22 expect(generated.code).toBe("return 42;");
23 expect(generated.rationale).toContain("direct code");
24 });
25
26 it("parses structured output from claude provider", async () => {
27 const generated = await generateCodeFromPrompt("make an event", {
28 provider: "claude",
29 generateWithClaude: async () => ({
30 code: "const event = await tools.calendar.update({ title: 'Dinner', startsAt: '2026-02-06T17:00:00.000Z' }); return { event };",
31 rationale: "Mapped user prompt to calendar.update",
32 }),
33 });
34
35 expect(generated.provider).toBe("claude");
36 expect(generated.code).toContain("tools.calendar.update");
37 expect(generated.rationale).toContain("Mapped");
38 });
39
40 it("handles multi-line calendar lists with relative days", async () => {
41 const now = new Date("2026-02-06T10:00:00.000Z"); // Friday
42 const prompt = [
43 "Please add the following to my calendar:",
44 "",
45 "Dinner 5pm tomorrow",
46 "",
47 "Lunch 2 pm sunday",
48 "",
49 "Breakfast 3 am Monday",
50 ].join("\n");
51
52 const generated = await generateCodeFromPrompt(prompt, {
53 now,
54 provider: "heuristic",
55 });
56
57 expect(generated.code.match(/tools\.calendar\.update\(/g)?.length).toBe(3);
58 expect(generated.code).toContain('"title":"Dinner"');
59 expect(generated.code).toContain('"title":"Lunch"');
60 expect(generated.code).toContain('"title":"Breakfast"');
61 expect(generated.code).toContain('return { message: "Calendar updated", events };');
62
63 const startsAt = [...generated.code.matchAll(/"startsAt":"([^"]+)"/g)]
64 .map((match) => match[1])
65 .filter((value): value is string => typeof value === "string");
66 expect(startsAt).toHaveLength(3);
67 if (startsAt.length !== 3) {
68 throw new Error("expected 3 startsAt values");
69 }
70 const dinnerAt = new Date(startsAt[0]!);
71 const lunchAt = new Date(startsAt[1]!);
72 const breakfastAt = new Date(startsAt[2]!);
73 expect(dinnerAt.getDay()).toBe(6); // Saturday
74 expect(lunchAt.getDay()).toBe(0); // Sunday
75 expect(breakfastAt.getDay()).toBe(1); // Monday
76 });
77});
apps/discord-bot/src/codegen.ts+322−0
@@ -0,0 +1,322 @@
1import { generateObject } from "ai";
2import { createClaudeCode } from "ai-sdk-provider-claude-code";
3import { z } from "zod";
4
5export interface GeneratedCode {
6 code: string;
7 rationale: string;
8 provider: "claude" | "heuristic";
9}
10
11interface CodegenOptions {
12 now?: Date;
13 provider?: "claude" | "heuristic";
14 generateWithClaude?: (params: { prompt: string; now: Date }) => Promise<{ code: string; rationale: string }>;
15}
16
17const DEFAULT_PROVIDER = resolveDefaultProvider();
18const DEFAULT_CLAUDE_MODEL = readEnv("OPENASSISTANT_CLAUDE_MODEL")?.trim() || "sonnet";
19const CLAUDE_TIMEOUT_MS = Number(readEnv("OPENASSISTANT_CLAUDE_TIMEOUT_MS") ?? 30_000);
20const CLAUDE_PATH = readEnv("OPENASSISTANT_CLAUDE_PATH")?.trim();
21const CODEGEN_SCHEMA = z.object({
22 code: z.string().min(1),
23 rationale: z.string().min(1),
24});
25
26const claudeCode = createClaudeCode({
27 defaultSettings: {
28 permissionMode: "dontAsk",
29 tools: [],
30 maxTurns: 1,
31 ...(CLAUDE_PATH ? { pathToClaudeCodeExecutable: CLAUDE_PATH } : {}),
32 },
33});
34
35export async function generateCodeFromPrompt(
36 prompt: string,
37 options: CodegenOptions = {},
38): Promise<GeneratedCode> {
39 const now = options.now ?? new Date();
40 const provider = options.provider ?? DEFAULT_PROVIDER;
41
42 if (provider === "claude") {
43 try {
44 const generated = await (options.generateWithClaude ?? generateWithClaude)({
45 prompt,
46 now,
47 });
48 return { ...generated, provider: "claude" };
49 } catch (error) {
50 const fallback = generateHeuristic(prompt, now);
51 return {
52 ...fallback,
53 provider: "heuristic",
54 rationale: `${fallback.rationale} Claude fallback: ${describeUnknown(error)}`,
55 };
56 }
57 }
58
59 const generated = generateHeuristic(prompt, now);
60 return { ...generated, provider: "heuristic" };
61}
62
63async function generateWithClaude(params: {
64 prompt: string;
65 now: Date;
66}): Promise<Omit<GeneratedCode, "provider">> {
67 const { prompt, now } = params;
68 const abortController = new AbortController();
69 const timeout = setTimeout(() => abortController.abort(), CLAUDE_TIMEOUT_MS);
70 try {
71 const result = await generateObject({
72 model: claudeCode(DEFAULT_CLAUDE_MODEL),
73 schema: CODEGEN_SCHEMA,
74 prompt: buildPlannerPrompt(prompt, now),
75 temperature: 0,
76 abortSignal: abortController.signal,
77 });
78
79 return result.object;
80 } finally {
81 clearTimeout(timeout);
82 }
83}
84
85function buildPlannerPrompt(userPrompt: string, now: Date): string {
86 return [
87 "You are a code generator for a Bun TypeScript codemode runner.",
88 "Return a function body only (no markdown, no fences, no explanations in code).",
89 "The runtime invokes: new AsyncFunction('tools', code).",
90 "Available tools:",
91 "- await tools.calendar.update({ title: string, startsAt: string, notes?: string })",
92 "- await tools.calendar.list()",
93 "Rules:",
94 "- Use await for tool calls.",
95 "- If request implies creating/updating a calendar event, call tools.calendar.update.",
96 "- startsAt must be an ISO-8601 string.",
97 "- If time/date missing, use current timestamp.",
98 "- Always return a final object (e.g. { message, event }).",
99 `Current timestamp: ${now.toISOString()}`,
100 `User request: ${userPrompt}`,
101 ].join("\n");
102}
103
104function resolveDefaultProvider(): "claude" | "heuristic" {
105 const configured = readEnv("OPENASSISTANT_CODEGEN_PROVIDER")?.trim().toLowerCase();
106 if (configured === "heuristic") {
107 return "heuristic";
108 }
109 return "claude";
110}
111
112function generateHeuristic(prompt: string, now: Date): Omit<GeneratedCode, "provider"> {
113 const trimmed = prompt.trim();
114 if (!trimmed) {
115 return {
116 code: "return { message: 'Empty prompt.' };",
117 rationale: "Empty prompt fallback.",
118 };
119 }
120
121 if (trimmed.toLowerCase().startsWith("code:")) {
122 return {
123 code: trimmed.slice(5).trim(),
124 rationale: "User provided direct code.",
125 };
126 }
127
128 const lower = trimmed.toLowerCase();
129 if (lower.includes("calendar") || lower.includes("event") || lower.includes("schedule")) {
130 const updates = extractCalendarUpdates(trimmed, now);
131 if (updates.length > 0) {
132 const code = buildCalendarCode(updates);
133 return {
134 code,
135 rationale:
136 updates.length === 1
137 ? "Mapped prompt to calendar.update tool call."
138 : `Mapped prompt to ${updates.length} calendar.update tool calls.`,
139 };
140 }
141 }
142
143 return {
144 code: `return { message: "No tool mapping yet for prompt", prompt: ${JSON.stringify(trimmed)} };`,
145 rationale: "Fallback response when no tool intent is detected.",
146 };
147}
148
149function inferTitle(prompt: string): string {
150 const match = prompt.match(
151 /(?:put|add|create)\s+(?:an?\s+)?(?:calendar\s+)?(?:event\s+)?(?:to\s+)?(.+?)(?:\s+at\s+|$)/i,
152 );
153 if (match?.[1]) {
154 return cleanTitle(match[1]);
155 }
156 return "Untitled event";
157}
158
159function cleanTitle(input: string): string {
160 return input.replace(/^my\s+/i, "").replace(/^calendar\s+/i, "").trim();
161}
162
163function inferStartTime(prompt: string, now: Date): string {
164 const time = prompt.match(/(?:\bat\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i);
165 if (!time) {
166 return now.toISOString();
167 }
168
169 const dayToken = prompt.match(
170 /\b(today|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
171 )?.[1];
172 return resolveStartsAt(now, Number(time[1]), time[2] ? Number(time[2]) : 0, time[3], dayToken);
173}
174
175function describeUnknown(error: unknown): string {
176 if (error instanceof Error) {
177 return error.message;
178 }
179 return String(error);
180}
181
182function readEnv(key: string): string | undefined {
183 const bun = (globalThis as { Bun?: { env?: Record<string, string | undefined> } }).Bun;
184 return bun?.env?.[key] ?? process.env[key];
185}
186
187type CalendarUpdateInput = {
188 title: string;
189 startsAt: string;
190 notes: string;
191};
192
193const WEEKDAY_INDEX: Record<string, number> = {
194 sunday: 0,
195 monday: 1,
196 tuesday: 2,
197 wednesday: 3,
198 thursday: 4,
199 friday: 5,
200 saturday: 6,
201};
202
203function buildCalendarCode(updates: CalendarUpdateInput[]): string {
204 const lines: string[] = ["const events = [];"];
205 for (const update of updates) {
206 lines.push(`events.push(await tools.calendar.update(${JSON.stringify(update)}));`);
207 }
208 lines.push('return { message: "Calendar updated", events };');
209 return lines.join(" ");
210}
211
212function extractCalendarUpdates(prompt: string, now: Date): CalendarUpdateInput[] {
213 const lines = prompt
214 .split(/\r?\n/)
215 .map((line) => line.trim())
216 .filter((line) => line.length > 0);
217
218 const updates: CalendarUpdateInput[] = [];
219 for (const line of lines) {
220 if (isInstructionLine(line)) {
221 continue;
222 }
223 const parsed = parseCalendarLine(line, now, prompt);
224 if (parsed) {
225 updates.push(parsed);
226 }
227 }
228
229 if (updates.length > 0) {
230 return updates;
231 }
232
233 return [
234 {
235 title: inferTitle(prompt),
236 startsAt: inferStartTime(prompt, now),
237 notes: `Requested via Discord prompt: ${prompt}`,
238 },
239 ];
240}
241
242function isInstructionLine(line: string): boolean {
243 const value = line.toLowerCase();
244 if (value.includes("calendar") && (value.includes("add") || value.includes("following"))) {
245 return true;
246 }
247 return value.startsWith("please ");
248}
249
250function parseCalendarLine(
251 rawLine: string,
252 now: Date,
253 fullPrompt: string,
254): CalendarUpdateInput | null {
255 const line = rawLine.replace(/^[-*]\s*/, "").replace(/^\d+[.)]\s*/, "").trim();
256 const match = line.match(
257 /^(?<title>.+?)\s+(?:at\s+)?(?<hour>\d{1,2})(?::(?<minute>\d{2}))?\s*(?<meridiem>am|pm)\b(?:\s+(?<day>today|tomorrow|monday|tuesday|wednesday|thursday|friday|saturday|sunday))?$/i,
258 );
259
260 if (!match?.groups) {
261 return null;
262 }
263
264 const titleRaw = match.groups.title;
265 const hourRaw = match.groups.hour;
266 const meridiemRaw = match.groups.meridiem;
267 if (!titleRaw || !hourRaw || !meridiemRaw) {
268 return null;
269 }
270
271 const title = cleanTitle(titleRaw);
272 if (!title) {
273 return null;
274 }
275
276 const hour = Number(hourRaw);
277 const minute = match.groups.minute ? Number(match.groups.minute) : 0;
278 const meridiem = meridiemRaw;
279 const dayToken = match.groups.day;
280 const startsAt = resolveStartsAt(now, hour, minute, meridiem, dayToken);
281
282 return {
283 title,
284 startsAt,
285 notes: `Requested via Discord prompt: ${fullPrompt}`,
286 };
287}
288
289function resolveStartsAt(
290 now: Date,
291 hour12: number,
292 minute: number,
293 meridiemRaw: string | undefined,
294 dayTokenRaw: string | undefined,
295): string {
296 const meridiem = meridiemRaw?.toLowerCase();
297 const dayToken = dayTokenRaw?.toLowerCase();
298 let hour = hour12;
299
300 if (meridiem === "pm" && hour < 12) {
301 hour += 12;
302 } else if (meridiem === "am" && hour === 12) {
303 hour = 0;
304 }
305
306 const date = new Date(now);
307 if (dayToken === "tomorrow") {
308 date.setDate(date.getDate() + 1);
309 } else if (dayToken) {
310 const target = WEEKDAY_INDEX[dayToken];
311 if (target === undefined) {
312 date.setHours(hour, minute, 0, 0);
313 return date.toISOString();
314 }
315 const current = date.getDay();
316 const delta = (target - current + 7) % 7;
317 date.setDate(date.getDate() + delta);
318 }
319
320 date.setHours(hour, minute, 0, 0);
321 return date.toISOString();
322}
apps/discord-bot/src/format-response.ts+61−0
@@ -0,0 +1,61 @@
1import type { CodeModeRunResult } from "@openassistant/core";
2
3export function formatDiscordResponse(params: {
4 prompt: string;
5 generatedCode: string;
6 rationale: string;
7 provider: "claude" | "heuristic";
8 result: CodeModeRunResult;
9}): string {
10 const { prompt, generatedCode, rationale, provider, result } = params;
11 const sections = [
12 `Prompt: ${prompt}`,
13 `Planner (${provider}): ${rationale}`,
14 "Code:",
15 fenced("ts", generatedCode),
16 result.ok ? `Result: ${safeJson(result.value)}` : `Error: ${result.error}`,
17 "Receipts:",
18 formatReceipts(result.receipts),
19 ];
20
21 return truncateDiscord(sections.join("\n\n"));
22}
23
24function formatReceipts(receipts: CodeModeRunResult["receipts"]): string {
25 if (receipts.length === 0) {
26 return "- none";
27 }
28 return receipts
29 .map((receipt) => {
30 const parts = [
31 `- \`${receipt.toolPath}\``,
32 `decision=${receipt.decision}`,
33 `status=${receipt.status}`,
34 ];
35 if (receipt.inputPreview) {
36 parts.push(`input="${receipt.inputPreview}"`);
37 }
38 return parts.join(" ");
39 })
40 .join("\n");
41}
42
43function safeJson(value: unknown): string {
44 try {
45 return JSON.stringify(value);
46 } catch {
47 return String(value);
48 }
49}
50
51function fenced(lang: string, content: string): string {
52 return `\`\`\`${lang}\n${content}\n\`\`\``;
53}
54
55function truncateDiscord(value: string): string {
56 const limit = 1_900;
57 if (value.length <= limit) {
58 return value;
59 }
60 return `${value.slice(0, limit)}\n...`;
61}
apps/discord-bot/src/index.ts+102−0
@@ -0,0 +1,102 @@
1import { createCodeModeRunner } from "@openassistant/core";
2import {
3 ChannelType,
4 Client,
5 Events,
6 GatewayIntentBits,
7 type Message,
8 type MessageCreateOptions,
9 Partials,
10} from "discord.js";
11import { Effect } from "effect";
12import { DiscordApprovalBridge } from "./approval-bridge.js";
13import { InMemoryCalendarStore } from "./calendar-store.js";
14import { generateCodeFromPrompt } from "./codegen.js";
15import { formatDiscordResponse } from "./format-response.js";
16import { createToolTree } from "./tools.js";
17
18const token = Bun.env.DISCORD_BOT_TOKEN;
19if (!token) {
20 throw new Error("Missing DISCORD_BOT_TOKEN");
21}
22
23const approvalTimeoutMs = Number(Bun.env.OPENASSISTANT_APPROVAL_TIMEOUT_MS ?? 300_000);
24
25const client = new Client({
26 intents: [
27 GatewayIntentBits.Guilds,
28 GatewayIntentBits.GuildMessages,
29 GatewayIntentBits.DirectMessages,
30 GatewayIntentBits.MessageContent,
31 ],
32 partials: [Partials.Channel],
33});
34
35const approvalBridge = new DiscordApprovalBridge(approvalTimeoutMs);
36const calendarStore = new InMemoryCalendarStore();
37const tools = createToolTree(calendarStore);
38
39client.once(Events.ClientReady, (readyClient) => {
40 console.log(`[discord-bot] logged in as ${readyClient.user.tag}`);
41});
42
43client.on(Events.InteractionCreate, async (interaction) => {
44 if (!interaction.isButton()) {
45 return;
46 }
47 await approvalBridge.handleInteraction(interaction);
48});
49
50client.on(Events.MessageCreate, async (message) => {
51 if (shouldIgnore(message)) {
52 return;
53 }
54 await handleMessage(message);
55});
56
57await client.login(token);
58
59function shouldIgnore(message: Message): boolean {
60 return message.author.bot || message.content.trim().length === 0 || message.channel.type !== ChannelType.DM;
61}
62
63async function handleMessage(message: Message): Promise<void> {
64 const ack = await message.reply("Working on it...");
65 const approvalChannel = message.channel as unknown;
66 if (
67 !approvalChannel ||
68 typeof approvalChannel !== "object" ||
69 !("send" in approvalChannel) ||
70 typeof (approvalChannel as { send?: unknown }).send !== "function"
71 ) {
72 await ack.edit("This channel does not support approval prompts.");
73 return;
74 }
75
76 const generated = await generateCodeFromPrompt(message.content);
77
78 const runner = createCodeModeRunner({
79 tools,
80 requestApproval: (request) =>
81 Effect.tryPromise({
82 try: () =>
83 approvalBridge.requestApproval({
84 request,
85 channel: approvalChannel as { send: (message: MessageCreateOptions) => Promise<unknown> },
86 requesterId: message.author.id,
87 }),
88 catch: (error) => error,
89 }),
90 });
91
92 const result = await Effect.runPromise(runner.run({ code: generated.code }));
93 const response = formatDiscordResponse({
94 prompt: message.content,
95 generatedCode: generated.code,
96 rationale: generated.rationale,
97 provider: generated.provider,
98 result,
99 });
100
101 await ack.edit(response);
102}
apps/discord-bot/src/tools.ts+22−0
@@ -0,0 +1,22 @@
1import { defineTool, type ToolTree } from "@openassistant/core";
2import { Effect } from "effect";
3import type { InMemoryCalendarStore } from "./calendar-store.js";
4
5export function createToolTree(calendarStore: InMemoryCalendarStore): ToolTree {
6 return {
7 calendar: {
8 update: defineTool({
9 kind: "write",
10 approval: "required",
11 run: (input: { title: string; startsAt: string; notes?: string }) =>
12 Effect.sync(() => calendarStore.update(input)),
13 previewInput: (input) => `${input.title} @ ${input.startsAt}`,
14 }),
15 list: defineTool({
16 kind: "read",
17 approval: "auto",
18 run: () => Effect.sync(() => calendarStore.list()),
19 }),
20 },
21 };
22}
apps/discord-bot/tsconfig.json+9−0
@@ -0,0 +1,9 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "rootDir": "src"
5 },
6 "include": [
7 "src/**/*.ts"
8 ]
9}
apps/gateway/package.json+21−0
@@ -0,0 +1,21 @@
1{
2 "name": "@openassistant/gateway",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "with-env": "bun run --env-file=../../.env --",
10 "with-env:prod": "bun run --env-file=../../.env.production --",
11 "dev": "bun with-env bun run src/index.ts",
12 "start": "bun with-env bun run src/index.ts",
13 "start:prod": "bun with-env:prod bun run src/index.ts",
14 "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json",
15 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""
16 },
17 "dependencies": {
18 "@openassistant/core": "workspace:*",
19 "effect": "latest"
20 }
21}
apps/gateway/src/index.ts+10−0
@@ -0,0 +1,10 @@
1import { healthcheck } from "@openassistant/core";
2import { Effect } from "effect";
3
4const program = Effect.flatMap(healthcheck, (status) =>
5 Effect.sync(() => {
6 console.log(`[gateway] healthcheck=${status}`);
7 }),
8);
9
10await Effect.runPromise(program);
apps/gateway/tsconfig.json+9−0
@@ -0,0 +1,9 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "rootDir": "src"
5 },
6 "include": [
7 "src/**/*.ts"
8 ]
9}
bun.lock+384−0
@@ -0,0 +1,384 @@
1{
2 "lockfileVersion": 1,
3 "configVersion": 1,
4 "workspaces": {
5 "": {
6 "name": "assistant",
7 "devDependencies": {
8 "@effect/language-service": "latest",
9 "@effect/vitest": "^0.27.0",
10 "@types/bun": "latest",
11 "@typescript/native-preview": "7.0.0-dev.20260205.1",
12 "typescript": "latest",
13 "vitest": "^3.2.0",
14 },
15 },
16 "apps/discord-bot": {
17 "name": "@openassistant/discord-bot",
18 "dependencies": {
19 "@openassistant/core": "workspace:*",
20 "ai": "^6.0.73",
21 "ai-sdk-provider-claude-code": "^3.3.6",
22 "discord.js": "^14.17.3",
23 "effect": "latest",
24 "zod": "^4.3.6",
25 },
26 },
27 "apps/gateway": {
28 "name": "@openassistant/gateway",
29 "dependencies": {
30 "@openassistant/core": "workspace:*",
31 "effect": "latest",
32 },
33 },
34 "packages/core": {
35 "name": "@openassistant/core",
36 "dependencies": {
37 "effect": "latest",
38 },
39 },
40 },
41 "packages": {
42 "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.36", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2r1Q6azvqMYxQ1hqfWZmWg4+8MajoldD/ty65XdhCaCoBfvDu7trcvxXDfTSU+3/wZ1JIDky46SWYFOHnTbsBw=="],
43
44 "@ai-sdk/provider": ["@ai-sdk/provider@3.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw=="],
45
46 "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.7", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og=="],
47
48 "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.32", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-8AtsSx/M9jxd0ihS08eqa7VireTEuwQy0i1+6ZJX93LECT6Svlf47dPJiAm7JB+BhVMmwTfQeS6x1akIcCfvbQ=="],
49
50 "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="],
51
52 "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
53
54 "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
55
56 "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
57
58 "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
59
60 "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
61
62 "@effect/language-service": ["@effect/language-service@0.73.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-/SGoq50VDm/XwQI6d0cReYcLjwfdqwZv7uEJp92+ssx5vLCsm+QvHf4Ul6l6PYVzorsgAp/b6fhAwI2VSkqcJQ=="],
63
64 "@effect/vitest": ["@effect/vitest@0.27.0", "", { "peerDependencies": { "effect": "^3.19.0", "vitest": "^3.2.0" } }, "sha512-8bM7n9xlMUYw9GqPIVgXFwFm2jf27m/R7psI64PGpwU5+26iwyxp9eAXEsfT5S6lqztYfpQQ1Ubp5o6HfNYzJQ=="],
65
66 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
67
68 "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
69
70 "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
71
72 "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
73
74 "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
75
76 "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
77
78 "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
79
80 "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
81
82 "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
83
84 "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
85
86 "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
87
88 "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
89
90 "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
91
92 "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
93
94 "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
95
96 "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
97
98 "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
99
100 "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
101
102 "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
103
104 "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
105
106 "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
107
108 "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
109
110 "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
111
112 "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
113
114 "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
115
116 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
117
118 "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
119
120 "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
121
122 "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
123
124 "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
125
126 "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
127
128 "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
129
130 "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
131
132 "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
133
134 "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
135
136 "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
137
138 "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
139
140 "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
141
142 "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
143
144 "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
145
146 "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
147
148 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
149
150 "@openassistant/core": ["@openassistant/core@workspace:packages/core"],
151
152 "@openassistant/discord-bot": ["@openassistant/discord-bot@workspace:apps/discord-bot"],
153
154 "@openassistant/gateway": ["@openassistant/gateway@workspace:apps/gateway"],
155
156 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
157
158 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
159
160 "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
161
162 "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
163
164 "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
165
166 "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
167
168 "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
169
170 "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
171
172 "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
173
174 "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
175
176 "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
177
178 "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
179
180 "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
181
182 "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
183
184 "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
185
186 "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
187
188 "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
189
190 "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
191
192 "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
193
194 "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
195
196 "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
197
198 "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
199
200 "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
201
202 "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
203
204 "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
205
206 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
207
208 "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
209
210 "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
211
212 "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
213
214 "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
215
216 "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
217
218 "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
219
220 "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
221
222 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
223
224 "@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="],
225
226 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
227
228 "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260205.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260205.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260205.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260205.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-eSgzYCbdCXP/E0XL53yIMZNLoY3z1xMOgGyjstVLgUCMLv1yNrFvkhKhHFjM84OTY/LxqRb6ACtvjFO/oSZzvQ=="],
229
230 "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260205.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ULATKP9a26qh8vcmP4qPz8UugGKIwhQPKi3NhvlbTPwhl3fMd3GJd9/B9LJSHw7lIuELQGZxhSlDq9l0FMb/FQ=="],
231
232 "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260205.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-moaKDZHK2dbgcHCnxcwhH8kYRgY69wzPcH5hCNaSrmpbC+Garr78oLtyXot2EDotRDT9foeYsWKdmD6Hx/ypxg=="],
233
234 "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3qfjUQlYCkwQmbpIeXMw75bLXkCI3Uo88Ug1n9p4j6KFaek5TjnHOTmlO6V3pkyH9pEXQEVXTn0pXzQytxqEqw=="],
235
236 "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wfp2bPmrTLb+dpp2bHDjMqMKGjQ9dp5KSw0jV4LSlbgcVvRSEWqs2ByVVj61Z4qiHgwlVyoPTewdan2CWnoBgQ=="],
237
238 "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260205.1", "", { "os": "linux", "cpu": "x64" }, "sha512-p59oY35gvvmdy/iZYxdbFAUXusb7joX2i1Nwl15i4TOn52NcIcW3wb9U/uBrIXKev5VEdlH6BS6VA6dM57zD6w=="],
239
240 "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260205.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-+NQTlmvtZEXwIlw8j+tvAAn1gLDqyWJEjnA5vmT9MoJuEBrxvuS8azn/q26MOp/w8bWfxe3haVyB+L4VurCF6w=="],
241
242 "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260205.1", "", { "os": "win32", "cpu": "x64" }, "sha512-kRa4kaiORAWQx9sHylewUhKsNxz3dRBy6AM/U02UebJRlt6c+JnSjIxAFP+iNQaRpoYNs8UdKKGPrHc7Q0oYow=="],
243
244 "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
245
246 "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
247
248 "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
249
250 "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
251
252 "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
253
254 "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
255
256 "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
257
258 "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
259
260 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
261
262 "ai": ["ai@6.0.73", "", { "dependencies": { "@ai-sdk/gateway": "3.0.36", "@ai-sdk/provider": "3.0.7", "@ai-sdk/provider-utils": "4.0.13", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-p2/ICXIjAM4+bIFHEkAB+l58zq+aTmxAkotsb6doNt/CEms72zt6gxv2ky1fQDwU4ecMOcmMh78VJUSEKECzlg=="],
263
264 "ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.3.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.2.23" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lDJJrQqe2A6tAH96ZLsZW39blkBh3jF1A4rs8B6CJyjNhLNeq46KmxkKDPYDoZw4xbILvnAknjXQNhMSZLnEYg=="],
265
266 "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
267
268 "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
269
270 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
271
272 "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
273
274 "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
275
276 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
277
278 "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
279
280 "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="],
281
282 "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
283
284 "effect": ["effect@3.19.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ=="],
285
286 "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
287
288 "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
289
290 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
291
292 "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
293
294 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
295
296 "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
297
298 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
299
300 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
301
302 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
303
304 "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
305
306 "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
307
308 "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
309
310 "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
311
312 "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
313
314 "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="],
315
316 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
317
318 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
319
320 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
321
322 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
323
324 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
325
326 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
327
328 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
329
330 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
331
332 "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
333
334 "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
335
336 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
337
338 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
339
340 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
341
342 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
343
344 "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
345
346 "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
347
348 "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
349
350 "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
351
352 "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
353
354 "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
355
356 "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
357
358 "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
359
360 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
361
362 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
363
364 "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
365
366 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
367
368 "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
369
370 "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
371
372 "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
373
374 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
375
376 "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
377
378 "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
379
380 "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
381
382 "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
383 }
384}
index.ts+1−0
@@ -0,0 +1 @@
1console.log("Hello via Bun!");
package.json+30−0
@@ -0,0 +1,30 @@
1{
2 "name": "openassistant",
3 "private": true,
4 "type": "module",
5 "workspaces": [
6 "apps/*",
7 "packages/*"
8 ],
9 "scripts": {
10 "with-env": "bun run --env-file=.env --",
11 "with-env:prod": "bun run --env-file=.env.production --",
12 "prepare": "effect-language-service patch",
13 "typecheck": "bun run --filter '@openassistant/core' typecheck && bun run --filter '@openassistant/gateway' typecheck && bun run --filter '@openassistant/discord-bot' typecheck",
14 "typecheck:tsc": "tsc --noEmit --incremental false -p packages/core/tsconfig.json && tsc --noEmit --incremental false -p apps/gateway/tsconfig.json && tsc --noEmit --incremental false -p apps/discord-bot/tsconfig.json",
15 "build": "bun run typecheck",
16 "test": "vitest run",
17 "test:watch": "vitest",
18 "clean": "bun run --filter '@openassistant/core' clean && bun run --filter '@openassistant/gateway' clean && bun run --filter '@openassistant/discord-bot' clean",
19 "dev:gateway": "bun run --cwd apps/gateway dev",
20 "dev:discord": "bun run --cwd apps/discord-bot dev"
21 },
22 "devDependencies": {
23 "@effect/language-service": "latest",
24 "@effect/vitest": "^0.27.0",
25 "@types/bun": "latest",
26 "@typescript/native-preview": "7.0.0-dev.20260205.1",
27 "typescript": "latest",
28 "vitest": "^3.2.0"
29 }
30}
packages/core/package.json+17−0
@@ -0,0 +1,17 @@
1{
2 "name": "@openassistant/core",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "with-env": "bun run --env-file=../../.env --",
10 "with-env:prod": "bun run --env-file=../../.env.production --",
11 "typecheck": "tsgo --noEmit --incremental false -p tsconfig.json",
12 "clean": "bun -e \"import { rmSync } from 'node:fs'; for (const p of ['dist','node_modules','.turbo','coverage','.tsbuildinfo']) rmSync(p,{recursive:true,force:true});\""
13 },
14 "dependencies": {
15 "effect": "latest"
16 }
17}
packages/core/src/codemode/runner.test.ts+117−0
@@ -0,0 +1,117 @@
1import { expect, it } from "@effect/vitest";
2import { Effect } from "effect";
3import { describe } from "vitest";
4import {
5 createCodeModeRunner,
6 defineTool,
7} from "./runner.js";
8
9describe("codemode runner", () => {
10 it.effect("runs read tools without approval", () =>
11 Effect.gen(function* () {
12 const approvalCalls: Array<{ toolPath: string }> = [];
13 const runner = createCodeModeRunner({
14 tools: {
15 calendar: {
16 read: defineTool({
17 kind: "read",
18 approval: "auto",
19 run: (input: { id: string }) => Effect.succeed({ id: input.id }),
20 }),
21 },
22 },
23 requestApproval: (request) =>
24 Effect.sync(() => {
25 approvalCalls.push({ toolPath: request.toolPath });
26 return "approved" as const;
27 }),
28 });
29
30 const result = yield* runner.run({
31 code: "const event = await tools.calendar.read({ id: 'evt_1' }); return event.id;",
32 });
33
34 expect(result.ok).toBe(true);
35 if (result.ok) {
36 expect(result.value).toBe("evt_1");
37 }
38 expect(approvalCalls).toHaveLength(0);
39 expect(result.receipts).toHaveLength(1);
40 expect(result.receipts[0]?.decision).toBe("auto");
41 expect(result.receipts[0]?.status).toBe("succeeded");
42 expect(result.receipts[0]?.toolPath).toBe("calendar.read");
43 }),
44 );
45
46 it.effect("blocks writes when denied and records receipt", () =>
47 Effect.gen(function* () {
48 let ranMutation = false;
49 const runner = createCodeModeRunner({
50 tools: {
51 calendar: {
52 update: defineTool({
53 kind: "write",
54 approval: "required",
55 run: (_input: { id: string }) =>
56 Effect.sync(() => {
57 ranMutation = true;
58 return { ok: true };
59 }),
60 }),
61 },
62 },
63 requestApproval: () => Effect.succeed("denied" as const),
64 });
65
66 const result = yield* runner.run({
67 code: "await tools.calendar.update({ id: 'evt_2' }); return 'done';",
68 });
69
70 expect(result.ok).toBe(false);
71 if (!result.ok) {
72 expect(result.error).toContain("denied");
73 }
74 expect(ranMutation).toBe(false);
75 expect(result.receipts).toHaveLength(1);
76 expect(result.receipts[0]?.decision).toBe("denied");
77 expect(result.receipts[0]?.status).toBe("denied");
78 expect(result.receipts[0]?.toolPath).toBe("calendar.update");
79 }),
80 );
81
82 it.effect("runs writes when approved and records success receipt", () =>
83 Effect.gen(function* () {
84 let ranMutation = false;
85 const runner = createCodeModeRunner({
86 tools: {
87 calendar: {
88 update: defineTool({
89 kind: "write",
90 approval: "required",
91 run: (input: { id: string; title: string }) =>
92 Effect.sync(() => {
93 ranMutation = true;
94 return { id: input.id, title: input.title };
95 }),
96 }),
97 },
98 },
99 requestApproval: () => Effect.succeed("approved" as const),
100 });
101
102 const result = yield* runner.run({
103 code: "const out = await tools.calendar.update({ id: 'evt_3', title: 'Dinner' }); return out.title;",
104 });
105
106 expect(result.ok).toBe(true);
107 if (result.ok) {
108 expect(result.value).toBe("Dinner");
109 }
110 expect(ranMutation).toBe(true);
111 expect(result.receipts).toHaveLength(1);
112 expect(result.receipts[0]?.decision).toBe("approved");
113 expect(result.receipts[0]?.status).toBe("succeeded");
114 expect(result.receipts[0]?.toolPath).toBe("calendar.update");
115 }),
116 );
117});
packages/core/src/codemode/runner.ts+318−0
@@ -0,0 +1,318 @@
1import { Effect } from "effect";
2
3export type ToolKind = "read" | "write";
4export type ToolApprovalMode = "auto" | "required";
5export type ApprovalDecision = "approved" | "denied";
6export type ToolReceiptDecision = "auto" | ApprovalDecision;
7export type ToolReceiptStatus = "succeeded" | "failed" | "denied";
8
9export interface ToolCallReceipt {
10 callId: string;
11 toolPath: string;
12 kind: ToolKind;
13 approval: ToolApprovalMode;
14 decision: ToolReceiptDecision;
15 status: ToolReceiptStatus;
16 timestamp: string;
17 inputPreview?: string;
18 outputPreview?: string;
19 error?: string;
20}
21
22export interface ApprovalRequest {
23 callId: string;
24 toolPath: string;
25 kind: ToolKind;
26 approval: ToolApprovalMode;
27 input: unknown;
28 inputPreview?: string;
29}
30
31export interface ToolDefinition<TInput = unknown, TOutput = unknown> {
32 readonly _tag: "ToolDefinition";
33 readonly kind: ToolKind;
34 readonly approval: ToolApprovalMode;
35 readonly run: (input: TInput) => Effect.Effect<TOutput, unknown>;
36 readonly previewInput?: (input: TInput) => string | undefined;
37 readonly previewOutput?: (output: TOutput) => string | undefined;
38}
39
40export type ToolTree = {
41 readonly [name: string]: ToolTree | ToolDefinition<any, any>;
42};
43
44export interface CodeModeRunnerOptions {
45 tools: ToolTree;
46 requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>;
47 now?: () => Date;
48 newCallId?: () => string;
49}
50
51export interface RunCodeOptions {
52 code: string;
53}
54
55export type CodeModeRunResult =
56 | {
57 ok: true;
58 value: unknown;
59 receipts: ToolCallReceipt[];
60 }
61 | {
62 ok: false;
63 error: string;
64 receipts: ToolCallReceipt[];
65 };
66
67const MAX_PREVIEW_CHARS = 180;
68const AsyncFunctionCtor = Object.getPrototypeOf(
69 async (_tools: unknown) => undefined,
70).constructor as new (argName: string, body: string) => (tools: unknown) => Promise<unknown>;
71
72export function defineTool<TInput, TOutput>(definition: {
73 kind: ToolKind;
74 approval: ToolApprovalMode;
75 run: (input: TInput) => Effect.Effect<TOutput, unknown>;
76 previewInput?: (input: TInput) => string | undefined;
77 previewOutput?: (output: TOutput) => string | undefined;
78}): ToolDefinition<TInput, TOutput> {
79 return {
80 _tag: "ToolDefinition",
81 ...definition,
82 };
83}
84
85export function createCodeModeRunner(options: CodeModeRunnerOptions): {
86 run: (params: RunCodeOptions) => Effect.Effect<CodeModeRunResult, never>;
87} {
88 const now = options.now ?? (() => new Date());
89 const newCallId = options.newCallId ?? defaultCallId;
90
91 return {
92 run: (params) => {
93 const receipts: ToolCallReceipt[] = [];
94 const tools = materializeToolTree({
95 tools: options.tools,
96 requestApproval: options.requestApproval,
97 newCallId,
98 now,
99 receipts,
100 });
101
102 return Effect.tryPromise({
103 try: async () => {
104 const execute = new AsyncFunctionCtor("tools", `"use strict";\n${params.code}`);
105 const value = await execute(tools);
106 return {
107 ok: true as const,
108 value,
109 receipts: [...receipts],
110 };
111 },
112 catch: (cause) => ({
113 ok: false as const,
114 error: describeUnknown(cause),
115 receipts: [...receipts],
116 }),
117 }).pipe(
118 Effect.catchAll((result) =>
119 Effect.succeed({
120 ok: false as const,
121 error: result.error,
122 receipts: result.receipts,
123 }),
124 ),
125 );
126 },
127 };
128}
129
130function materializeToolTree(params: {
131 tools: ToolTree;
132 requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>;
133 newCallId: () => string;
134 now: () => Date;
135 receipts: ToolCallReceipt[];
136}): Record<string, unknown> {
137 const out: Record<string, unknown> = {};
138 for (const [name, node] of Object.entries(params.tools)) {
139 const toolPath = name;
140 out[name] = materializeToolNode({
141 node,
142 toolPath,
143 requestApproval: params.requestApproval,
144 newCallId: params.newCallId,
145 now: params.now,
146 receipts: params.receipts,
147 });
148 }
149 return out;
150}
151
152function materializeToolNode(params: {
153 node: ToolTree | ToolDefinition<any, any>;
154 toolPath: string;
155 requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>;
156 newCallId: () => string;
157 now: () => Date;
158 receipts: ToolCallReceipt[];
159}): unknown {
160 if (isToolDefinition(params.node)) {
161 return createToolInvoker({
162 definition: params.node,
163 toolPath: params.toolPath,
164 requestApproval: params.requestApproval,
165 newCallId: params.newCallId,
166 now: params.now,
167 receipts: params.receipts,
168 });
169 }
170
171 const out: Record<string, unknown> = {};
172 for (const [name, child] of Object.entries(params.node)) {
173 out[name] = materializeToolNode({
174 node: child,
175 toolPath: `${params.toolPath}.${name}`,
176 requestApproval: params.requestApproval,
177 newCallId: params.newCallId,
178 now: params.now,
179 receipts: params.receipts,
180 });
181 }
182 return out;
183}
184
185function createToolInvoker(params: {
186 definition: ToolDefinition<any, any>;
187 toolPath: string;
188 requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>;
189 newCallId: () => string;
190 now: () => Date;
191 receipts: ToolCallReceipt[];
192}): (input: unknown) => Promise<unknown> {
193 return async (input: unknown) => {
194 const callId = params.newCallId();
195 const timestamp = params.now().toISOString();
196 const inputPreview = previewValue(input, params.definition.previewInput);
197
198 let decision: ToolReceiptDecision = "auto";
199 if (params.definition.approval === "required") {
200 const approval = await Effect.runPromise(
201 params.requestApproval({
202 callId,
203 toolPath: params.toolPath,
204 kind: params.definition.kind,
205 approval: params.definition.approval,
206 input,
207 ...(inputPreview !== undefined ? { inputPreview } : {}),
208 }),
209 );
210 decision = approval;
211 if (approval === "denied") {
212 params.receipts.push({
213 callId,
214 toolPath: params.toolPath,
215 kind: params.definition.kind,
216 approval: params.definition.approval,
217 decision,
218 status: "denied",
219 timestamp,
220 ...(inputPreview !== undefined ? { inputPreview } : {}),
221 });
222 throw new Error(`Tool call denied: ${params.toolPath}`);
223 }
224 }
225
226 const execution = await Effect.runPromiseExit(params.definition.run(input));
227 if (execution._tag === "Success") {
228 const value = execution.value;
229 const outputPreview = previewValue(value, params.definition.previewOutput);
230 params.receipts.push({
231 callId,
232 toolPath: params.toolPath,
233 kind: params.definition.kind,
234 approval: params.definition.approval,
235 decision,
236 status: "succeeded",
237 timestamp,
238 ...(inputPreview !== undefined ? { inputPreview } : {}),
239 ...(outputPreview !== undefined ? { outputPreview } : {}),
240 });
241 return value;
242 }
243
244 const error = describeUnknown(execution.cause);
245 params.receipts.push({
246 callId,
247 toolPath: params.toolPath,
248 kind: params.definition.kind,
249 approval: params.definition.approval,
250 decision,
251 status: "failed",
252 timestamp,
253 ...(inputPreview !== undefined ? { inputPreview } : {}),
254 error,
255 });
256 throw new Error(error);
257 };
258}
259
260function isToolDefinition(value: unknown): value is ToolDefinition<any, any> {
261 return (
262 typeof value === "object" &&
263 value !== null &&
264 "_tag" in value &&
265 (value as { _tag?: unknown })._tag === "ToolDefinition"
266 );
267}
268
269function previewValue<T>(
270 value: T,
271 customPreview: ((value: T) => string | undefined) | undefined,
272): string | undefined {
273 if (customPreview) {
274 return truncatePreview(customPreview(value));
275 }
276 if (value === undefined) {
277 return undefined;
278 }
279 return truncatePreview(safeStringify(value));
280}
281
282function truncatePreview(value: string | undefined): string | undefined {
283 if (!value) {
284 return undefined;
285 }
286 if (value.length <= MAX_PREVIEW_CHARS) {
287 return value;
288 }
289 return `${value.slice(0, MAX_PREVIEW_CHARS)}...`;
290}
291
292function safeStringify(value: unknown): string {
293 if (typeof value === "string") {
294 return value;
295 }
296 try {
297 return JSON.stringify(value);
298 } catch {
299 return String(value);
300 }
301}
302
303function defaultCallId(): string {
304 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
305 return crypto.randomUUID();
306 }
307 return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
308}
309
310function describeUnknown(cause: unknown): string {
311 if (cause instanceof Error) {
312 return cause.message;
313 }
314 if (typeof cause === "string") {
315 return cause;
316 }
317 return safeStringify(cause);
318}
packages/core/src/index.ts+4−0
@@ -0,0 +1,4 @@
1import { Effect } from "effect";
2
3export const healthcheck = Effect.succeed("ok");
4export * from "./codemode/runner.js";
packages/core/tsconfig.json+9−0
@@ -0,0 +1,9 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "rootDir": "src"
5 },
6 "include": [
7 "src/**/*.ts"
8 ]
9}
tsconfig.base.json+22−0
@@ -0,0 +1,22 @@
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "NodeNext",
5 "moduleResolution": "NodeNext",
6 "moduleDetection": "force",
7 "verbatimModuleSyntax": true,
8 "rewriteRelativeImportExtensions": true,
9 "strict": true,
10 "exactOptionalPropertyTypes": true,
11 "noUncheckedIndexedAccess": true,
12 "noImplicitOverride": true,
13 "noUnusedLocals": true,
14 "noUnusedParameters": true,
15 "skipLibCheck": true,
16 "plugins": [
17 {
18 "name": "@effect/language-service"
19 }
20 ]
21 }
22}
tsconfig.json+14−0
@@ -0,0 +1,14 @@
1{
2 "files": [],
3 "references": [
4 {
5 "path": "./packages/core"
6 },
7 {
8 "path": "./apps/gateway"
9 },
10 {
11 "path": "./apps/discord-bot"
12 }
13 ]
14}
vitest.config.ts+8−0
@@ -0,0 +1,8 @@
1import { defineConfig } from "vitest/config";
2
3export default defineConfig({
4 test: {
5 include: ["packages/**/*.test.ts", "apps/**/*.test.ts"],
6 exclude: ["**/node_modules/**", ".context/**", ".reference/**"],
7 },
8});