Unified · Split
| @@ -0,0 +1 @@ | |||
| 1 | Subproject commit 6f4665dda3af787c50685e440cca21a57816b5a6 | ||
| @@ -0,0 +1 @@ | |||
| 1 | Subproject commit 43c45b2cbe0e64fb7859164a28e29da427c37930 | ||
| @@ -0,0 +1,36 @@ | |||
| 1 | # dependencies (bun install) | ||
| 2 | node_modules | ||
| 3 | |||
| 4 | # output | ||
| 5 | out | ||
| 6 | dist | ||
| 7 | *.tgz | ||
| 8 | |||
| 9 | # code coverage | ||
| 10 | coverage | ||
| 11 | *.lcov | ||
| 12 | |||
| 13 | # logs | ||
| 14 | logs | ||
| 15 | _.log | ||
| 16 | report.[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 | ||
| @@ -0,0 +1 @@ | |||
| 1 | CLAUDE.md | ||
| @@ -0,0 +1,120 @@ | |||
| 1 | |||
| 2 | Default 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 | |||
| 24 | Use `bun test` to run tests. | ||
| 25 | |||
| 26 | ```ts#index.test.ts | ||
| 27 | import { test, expect } from "bun:test"; | ||
| 28 | |||
| 29 | test("hello world", () => { | ||
| 30 | expect(1).toBe(1); | ||
| 31 | }); | ||
| 32 | ``` | ||
| 33 | |||
| 34 | ## Frontend | ||
| 35 | |||
| 36 | Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. | ||
| 37 | |||
| 38 | Server: | ||
| 39 | |||
| 40 | ```ts#index.ts | ||
| 41 | import index from "./index.html" | ||
| 42 | |||
| 43 | Bun.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 | |||
| 71 | HTML 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 | |||
| 82 | With the following `frontend.tsx`: | ||
| 83 | |||
| 84 | ```tsx#frontend.tsx | ||
| 85 | import React from "react"; | ||
| 86 | import { createRoot } from "react-dom/client"; | ||
| 87 | |||
| 88 | // import .css files directly and it works | ||
| 89 | import './index.css'; | ||
| 90 | |||
| 91 | const root = createRoot(document.body); | ||
| 92 | |||
| 93 | export default function Frontend() { | ||
| 94 | return <h1>Hello, world!</h1>; | ||
| 95 | } | ||
| 96 | |||
| 97 | root.render(<Frontend />); | ||
| 98 | ``` | ||
| 99 | |||
| 100 | Then, run index.ts | ||
| 101 | |||
| 102 | ```sh | ||
| 103 | bun --hot ./index.ts | ||
| 104 | ``` | ||
| 105 | |||
| 106 | For 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 | |||
| 113 | 1. Run `effect-solutions list` to see available guides | ||
| 114 | 2. Run `effect-solutions show <topic>...` for relevant patterns (supports multiple topics) | ||
| 115 | 3. Search `.reference/effect/` for real implementations (run `effect-solutions setup` first) | ||
| 116 | |||
| 117 | Topics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli. | ||
| 118 | |||
| 119 | Never guess at Effect patterns - check the guide first. | ||
| 120 | <!-- effect-solutions:end --> | ||
| @@ -0,0 +1,309 @@ | |||
| 1 | # OpenAssistant: High-Level Architecture Plan | ||
| 2 | |||
| 3 | ## 1) Product Thesis | ||
| 4 | |||
| 5 | Build 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 | |||
| 22 | Keep 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 | |||
| 29 | Replace: | ||
| 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 | |||
| 37 | Control 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 | |||
| 45 | Execution 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 | |||
| 52 | Interaction 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 | |||
| 59 | Single model-facing tool: | ||
| 60 | |||
| 61 | - `run_code({ code, intent, sessionId })`. | ||
| 62 | |||
| 63 | Injected 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 | |||
| 69 | Example shape (conceptual): | ||
| 70 | |||
| 71 | ```ts | ||
| 72 | const result = await clients.calendar.updateEvent({ id, patch }) | ||
| 73 | if (result.isError()) { | ||
| 74 | return Result.err(result.error) | ||
| 75 | } | ||
| 76 | return Result.ok(result.value) | ||
| 77 | ``` | ||
| 78 | |||
| 79 | Outcome: | ||
| 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 | |||
| 87 | Permission unit: | ||
| 88 | |||
| 89 | - `integration.resource.action` (example: `vercel.firewall.rule.delete`). | ||
| 90 | |||
| 91 | Client 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 | |||
| 99 | Policy 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 | |||
| 107 | Request path: | ||
| 108 | |||
| 109 | 1. Script calls client method. | ||
| 110 | 2. Runtime asks policy engine. | ||
| 111 | 3. If `ask`, execution pauses with `ApprovalPending` receipt. | ||
| 112 | 4. Approval prompt sent to origin chat + optional ops targets. | ||
| 113 | 5. Approver chooses `allow-once`, `allow-window`, `deny`, or `allow-and-policy-update`. | ||
| 114 | 6. Execution resumes or returns denial as value. | ||
| 115 | |||
| 116 | Key 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 | |||
| 124 | Every side-effectful call emits immutable receipts: | ||
| 125 | |||
| 126 | - `call.requested` | ||
| 127 | - `call.approved|call.denied` | ||
| 128 | - `call.started` | ||
| 129 | - `call.finished|call.failed` | ||
| 130 | |||
| 131 | Assistant response rule: | ||
| 132 | |||
| 133 | - For external actions, summarize from receipts only. | ||
| 134 | - If no receipt exists, assistant must state action was not completed. | ||
| 135 | |||
| 136 | UI requirement: | ||
| 137 | |||
| 138 | - Inline "did it really run?" evidence panel per assistant answer. | ||
| 139 | |||
| 140 | ## 9) Scheduler and Reminders | ||
| 141 | |||
| 142 | Scheduler 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 | |||
| 149 | Contract: | ||
| 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 | |||
| 156 | Goal: | ||
| 157 | |||
| 158 | - Convert API specs into safe typed clients and policy metadata. | ||
| 159 | |||
| 160 | Pipeline: | ||
| 161 | |||
| 162 | 1. Ingest OpenAPI spec. | ||
| 163 | 2. Generate TypeScript client methods. | ||
| 164 | 3. Classify methods by risk heuristics and optional provider-specific rules. | ||
| 165 | 4. Emit package with runtime validators + method metadata. | ||
| 166 | 5. Register integration in gateway catalog. | ||
| 167 | |||
| 168 | Outputs 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 | |||
| 177 | Remote 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 | |||
| 183 | Security 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 | ||
| 192 | apps/ | ||
| 193 | gateway/ | ||
| 194 | ops-ui/ | ||
| 195 | discord-client/ | ||
| 196 | cli/ | ||
| 197 | packages/ | ||
| 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 | |||
| 219 | Core entities: | ||
| 220 | |||
| 221 | - `Session` | ||
| 222 | - `Run` | ||
| 223 | - `Script` | ||
| 224 | - `ToolCall` | ||
| 225 | - `ApprovalRequest` | ||
| 226 | - `Receipt` | ||
| 227 | - `Job` | ||
| 228 | - `CredentialLease` | ||
| 229 | - `PolicySnapshot` | ||
| 230 | |||
| 231 | Strong 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 | |||
| 238 | Include: | ||
| 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 | |||
| 248 | Exclude 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 | |||
| 256 | Phase A: Foundation | ||
| 257 | |||
| 258 | - Monorepo skeleton, shared types, event bus, gateway sessions. | ||
| 259 | - Runner sandbox and `run_code` API. | ||
| 260 | |||
| 261 | Phase B: Trust Layer | ||
| 262 | |||
| 263 | - Policy engine, approvals, receipt model, auditable run timeline. | ||
| 264 | |||
| 265 | Phase C: Integrations | ||
| 266 | |||
| 267 | - Manual clients + openapi codegen pipeline. | ||
| 268 | - Scoped credentials and remote tool host. | ||
| 269 | |||
| 270 | Phase D: Automation | ||
| 271 | |||
| 272 | - Scheduler, durable reminders, monitors, retry semantics. | ||
| 273 | |||
| 274 | Phase E: UX | ||
| 275 | |||
| 276 | - Discord + web ops surface for approvals and evidence links. | ||
| 277 | |||
| 278 | ## 16) Key Risks and Mitigations | ||
| 279 | |||
| 280 | Risk: Model-generated TS can still be unsafe. | ||
| 281 | Mitigation: hard sandbox + no direct network/process APIs + capability-injected clients only. | ||
| 282 | |||
| 283 | Risk: Approval fatigue. | ||
| 284 | Mitigation: risk tiers, approval windows, per-integration trusted scopes. | ||
| 285 | |||
| 286 | Risk: "Says it did it" regressions. | ||
| 287 | Mitigation: receipt-backed response contract + explicit uncertainty language. | ||
| 288 | |||
| 289 | Risk: Credential sprawl. | ||
| 290 | Mitigation: brokered ephemeral tokens and strict method-level scopes. | ||
| 291 | |||
| 292 | ## 17) First Technical Decision Set | ||
| 293 | |||
| 294 | Decide 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 | |||
| 304 | You 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). | ||
| @@ -0,0 +1,324 @@ | |||
| 1 | # OpenAssistant MVP: Primitive-First Plan | ||
| 2 | |||
| 3 | ## Goal | ||
| 4 | |||
| 5 | Ship an MVP that is: | ||
| 6 | |||
| 7 | - Small enough to build fast. | ||
| 8 | - Safe enough to trust. | ||
| 9 | - Extensible enough to add integrations later. | ||
| 10 | |||
| 11 | Core 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 | |||
| 24 | Executes model-generated TypeScript in a sandbox with a single injected surface: | ||
| 25 | |||
| 26 | - `tools.<integration>.<method>(input): Result<ok, err>` | ||
| 27 | |||
| 28 | No direct shell/network/process access from model code. | ||
| 29 | |||
| 30 | ### 2) `Tool Contract Primitive` | ||
| 31 | |||
| 32 | Each 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 | |||
| 39 | This is the capability boundary. | ||
| 40 | |||
| 41 | ### 3) `Policy + Approval Primitive` | ||
| 42 | |||
| 43 | Policy returns one of: | ||
| 44 | |||
| 45 | - `allow` | ||
| 46 | - `ask` | ||
| 47 | - `deny` | ||
| 48 | |||
| 49 | If `ask`, run pauses until explicit decision (`allow-once`, `deny`, optional short-lived window). | ||
| 50 | |||
| 51 | ### 4) `Receipt Primitive` (Proof of Work) | ||
| 52 | |||
| 53 | Append-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 | |||
| 60 | These receipts are the source of truth for what actually happened. | ||
| 61 | |||
| 62 | ### 5) `Relevant Tool Activity Primitive` (Evidence Surface) | ||
| 63 | |||
| 64 | The 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 | |||
| 70 | Summary 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 | |||
| 78 | Relevance 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 | |||
| 85 | This is the anti-lying mechanism: responses are grounded in gateway-produced evidence summaries, not free model claims. | ||
| 86 | |||
| 87 | ### 6) `Response Grounding Primitive` | ||
| 88 | |||
| 89 | Assistant action statements should be based on the relevant tool activity summary. | ||
| 90 | |||
| 91 | If 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 | |||
| 95 | Users (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 | |||
| 101 | This enables "write your own tools" without touching core runtime code. | ||
| 102 | |||
| 103 | ### 8) `Auth Profile Primitive` (Claude Max Support) | ||
| 104 | |||
| 105 | MVP 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 | |||
| 112 | This avoids coupling to Claude CLI internals while still leveraging Claude Max plan auth. | ||
| 113 | |||
| 114 | ## Minimal Repository Shape | ||
| 115 | |||
| 116 | Keep it simple first: | ||
| 117 | |||
| 118 | ```text | ||
| 119 | apps/ | ||
| 120 | gateway/ # chat loop, run orchestration, ws/http API | ||
| 121 | discord-client/ # primary MVP chat + approval interaction surface | ||
| 122 | packages/ | ||
| 123 | core/ # shared types, Result, receipt models, policy types | ||
| 124 | runtime/ # TS runner sandbox + tool bridge | ||
| 125 | sdk/ # defineTool(), integration client wrappers | ||
| 126 | integrations/ | ||
| 127 | example-calendar/ | ||
| 128 | example-posthog/ | ||
| 129 | ``` | ||
| 130 | |||
| 131 | Only 3 packages. Add more only when pain is real. | ||
| 132 | |||
| 133 | Notes: | ||
| 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 | |||
| 148 | Use one append-only receipts table/log first. Build read views later. | ||
| 149 | |||
| 150 | ## How “Reminder Truthfulness” Works Without Built-In Reminder System | ||
| 151 | |||
| 152 | User says: “Remind me tomorrow at 9.” | ||
| 153 | |||
| 154 | MVP behavior: | ||
| 155 | |||
| 156 | 1. Model chooses an integration tool (calendar/todo/automation/etc). | ||
| 157 | 2. Runtime executes that tool call. | ||
| 158 | 3. Receipt(s) are written. | ||
| 159 | 4. Gateway derives relevant tool activity summary. | ||
| 160 | 5. Assistant response + client surface that summary. | ||
| 161 | |||
| 162 | So: | ||
| 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 | |||
| 167 | No assumption about which reminder provider is used. | ||
| 168 | |||
| 169 | ## User-Authored Tool Flow (MVP) | ||
| 170 | |||
| 171 | Assistant workspace (example): | ||
| 172 | |||
| 173 | ```text | ||
| 174 | ~/.config/openassistant/ | ||
| 175 | tools/ | ||
| 176 | posthog/ | ||
| 177 | client.ts | ||
| 178 | tools.ts | ||
| 179 | ``` | ||
| 180 | |||
| 181 | Tool definition shape (conceptual): | ||
| 182 | |||
| 183 | ```ts | ||
| 184 | export 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 | |||
| 192 | export 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 | |||
| 205 | Runtime flow: | ||
| 206 | |||
| 207 | 1. Prompt asks for PostHog threshold monitor. | ||
| 208 | 2. Agent writes/updates PostHog tool module in workspace. | ||
| 209 | 3. Gateway reloads tool registry. | ||
| 210 | 4. Run uses newly available tool methods. | ||
| 211 | 5. Every call emits receipts. | ||
| 212 | 6. Gateway computes relevant tool activity. | ||
| 213 | 7. Assistant + client surface relevant tool activity. | ||
| 214 | |||
| 215 | What is guaranteed: | ||
| 216 | |||
| 217 | - Proof that the tool endpoint was called and what happened. | ||
| 218 | |||
| 219 | What 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 | |||
| 227 | Auth store file (example): | ||
| 228 | |||
| 229 | ```text | ||
| 230 | ~/.config/openassistant/auth-profiles.json | ||
| 231 | ``` | ||
| 232 | |||
| 233 | Credential shape: | ||
| 234 | |||
| 235 | ```ts | ||
| 236 | type 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 | |||
| 242 | For 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 | ||
| 250 | openassistant auth anthropic setup-token | ||
| 251 | ``` | ||
| 252 | |||
| 253 | Flow: | ||
| 254 | |||
| 255 | 1. Prompt user to run `claude setup-token`. | ||
| 256 | 2. Validate token prefix/shape. | ||
| 257 | 3. Save token profile in auth store. | ||
| 258 | 4. Set profile as preferred for Anthropic provider in config/order. | ||
| 259 | |||
| 260 | ### Provider Auth Resolution | ||
| 261 | |||
| 262 | Resolution order: | ||
| 263 | |||
| 264 | 1. Explicit profile override (if run/session pinned). | ||
| 265 | 2. Provider profile order from config/store. | ||
| 266 | 3. Env fallback (`ANTHROPIC_OAUTH_TOKEN`, then `ANTHROPIC_API_KEY`). | ||
| 267 | 4. Error with actionable re-auth instructions. | ||
| 268 | |||
| 269 | ### Runtime Behavior | ||
| 270 | |||
| 271 | At run start: | ||
| 272 | |||
| 273 | - Resolve provider auth once for selected model/provider. | ||
| 274 | - Inject credential into model client adapter. | ||
| 275 | |||
| 276 | On 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 | |||
| 282 | This gives Claude Max usability plus long-term multi-profile resilience. | ||
| 283 | |||
| 284 | ## Extensibility Path | ||
| 285 | |||
| 286 | New integration = add one folder under `integrations/*` implementing: | ||
| 287 | |||
| 288 | - Tool contract metadata | ||
| 289 | - Runtime client wrapper | ||
| 290 | - Schema validation | ||
| 291 | |||
| 292 | No 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 | |||
| 310 | 1. `core`: types + receipt/event schema + policy decision enums. | ||
| 311 | 2. `runtime`: sandbox executor + injected tool bridge. | ||
| 312 | 3. `gateway`: run orchestration + tool registry + persistence + evidence summarization. | ||
| 313 | 4. `discord-client`: chat loop + approve/deny actions + receipt viewing links. | ||
| 314 | 5. `integrations`: add calendar/posthog/github as examples. | ||
| 315 | |||
| 316 | ## Success Criteria | ||
| 317 | |||
| 318 | You 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. | ||
| @@ -0,0 +1,15 @@ | |||
| 1 | # assistant | ||
| 2 | |||
| 3 | To install dependencies: | ||
| 4 | |||
| 5 | ```bash | ||
| 6 | bun install | ||
| 7 | ``` | ||
| 8 | |||
| 9 | To run: | ||
| 10 | |||
| 11 | ```bash | ||
| 12 | bun run index.ts | ||
| 13 | ``` | ||
| 14 | |||
| 15 | This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. | ||
| @@ -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 | } | ||
| @@ -0,0 +1,109 @@ | |||
| 1 | import type { ApprovalDecision, ApprovalRequest } from "@openassistant/core"; | ||
| 2 | import { | ||
| 3 | ActionRowBuilder, | ||
| 4 | ButtonBuilder, | ||
| 5 | ButtonStyle, | ||
| 6 | type ButtonInteraction, | ||
| 7 | type MessageCreateOptions, | ||
| 8 | } from "discord.js"; | ||
| 9 | import { ApprovalRegistry } from "./approval-registry.js"; | ||
| 10 | |||
| 11 | const CUSTOM_ID_PREFIX = "oa.approval"; | ||
| 12 | |||
| 13 | export 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 | |||
| 82 | function 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 | |||
| 95 | function 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 | } | ||
| @@ -0,0 +1,38 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | import { ApprovalRegistry } from "./approval-registry.js"; | ||
| 3 | |||
| 4 | describe("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 | }); | ||
| @@ -0,0 +1,67 @@ | |||
| 1 | import type { ApprovalDecision } from "@openassistant/core"; | ||
| 2 | |||
| 3 | type PendingApproval = { | ||
| 4 | requesterId: string; | ||
| 5 | resolve: (decision: ApprovalDecision) => void; | ||
| 6 | timeout: ReturnType<typeof setTimeout>; | ||
| 7 | }; | ||
| 8 | |||
| 9 | export type ResolveApprovalResult = "resolved" | "not_found" | "unauthorized"; | ||
| 10 | |||
| 11 | export 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 | } | ||
| @@ -0,0 +1,32 @@ | |||
| 1 | export interface CalendarEvent { | ||
| 2 | id: string; | ||
| 3 | title: string; | ||
| 4 | startsAt: string; | ||
| 5 | notes?: string; | ||
| 6 | } | ||
| 7 | |||
| 8 | export interface CalendarUpdateInput { | ||
| 9 | title: string; | ||
| 10 | startsAt: string; | ||
| 11 | notes?: string; | ||
| 12 | } | ||
| 13 | |||
| 14 | export 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 | } | ||
| @@ -0,0 +1,77 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | import { generateCodeFromPrompt } from "./codegen.js"; | ||
| 3 | |||
| 4 | describe("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 | }); | ||
| @@ -0,0 +1,322 @@ | |||
| 1 | import { generateObject } from "ai"; | ||
| 2 | import { createClaudeCode } from "ai-sdk-provider-claude-code"; | ||
| 3 | import { z } from "zod"; | ||
| 4 | |||
| 5 | export interface GeneratedCode { | ||
| 6 | code: string; | ||
| 7 | rationale: string; | ||
| 8 | provider: "claude" | "heuristic"; | ||
| 9 | } | ||
| 10 | |||
| 11 | interface CodegenOptions { | ||
| 12 | now?: Date; | ||
| 13 | provider?: "claude" | "heuristic"; | ||
| 14 | generateWithClaude?: (params: { prompt: string; now: Date }) => Promise<{ code: string; rationale: string }>; | ||
| 15 | } | ||
| 16 | |||
| 17 | const DEFAULT_PROVIDER = resolveDefaultProvider(); | ||
| 18 | const DEFAULT_CLAUDE_MODEL = readEnv("OPENASSISTANT_CLAUDE_MODEL")?.trim() || "sonnet"; | ||
| 19 | const CLAUDE_TIMEOUT_MS = Number(readEnv("OPENASSISTANT_CLAUDE_TIMEOUT_MS") ?? 30_000); | ||
| 20 | const CLAUDE_PATH = readEnv("OPENASSISTANT_CLAUDE_PATH")?.trim(); | ||
| 21 | const CODEGEN_SCHEMA = z.object({ | ||
| 22 | code: z.string().min(1), | ||
| 23 | rationale: z.string().min(1), | ||
| 24 | }); | ||
| 25 | |||
| 26 | const claudeCode = createClaudeCode({ | ||
| 27 | defaultSettings: { | ||
| 28 | permissionMode: "dontAsk", | ||
| 29 | tools: [], | ||
| 30 | maxTurns: 1, | ||
| 31 | ...(CLAUDE_PATH ? { pathToClaudeCodeExecutable: CLAUDE_PATH } : {}), | ||
| 32 | }, | ||
| 33 | }); | ||
| 34 | |||
| 35 | export 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 | |||
| 63 | async 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 | |||
| 85 | function 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 | |||
| 104 | function 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 | |||
| 112 | function 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 | |||
| 149 | function 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 | |||
| 159 | function cleanTitle(input: string): string { | ||
| 160 | return input.replace(/^my\s+/i, "").replace(/^calendar\s+/i, "").trim(); | ||
| 161 | } | ||
| 162 | |||
| 163 | function 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 | |||
| 175 | function describeUnknown(error: unknown): string { | ||
| 176 | if (error instanceof Error) { | ||
| 177 | return error.message; | ||
| 178 | } | ||
| 179 | return String(error); | ||
| 180 | } | ||
| 181 | |||
| 182 | function 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 | |||
| 187 | type CalendarUpdateInput = { | ||
| 188 | title: string; | ||
| 189 | startsAt: string; | ||
| 190 | notes: string; | ||
| 191 | }; | ||
| 192 | |||
| 193 | const 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 | |||
| 203 | function 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 | |||
| 212 | function 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 | |||
| 242 | function 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 | |||
| 250 | function 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 | |||
| 289 | function 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 | } | ||
| @@ -0,0 +1,61 @@ | |||
| 1 | import type { CodeModeRunResult } from "@openassistant/core"; | ||
| 2 | |||
| 3 | export 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 | |||
| 24 | function 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 | |||
| 43 | function safeJson(value: unknown): string { | ||
| 44 | try { | ||
| 45 | return JSON.stringify(value); | ||
| 46 | } catch { | ||
| 47 | return String(value); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | function fenced(lang: string, content: string): string { | ||
| 52 | return `\`\`\`${lang}\n${content}\n\`\`\``; | ||
| 53 | } | ||
| 54 | |||
| 55 | function 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 | } | ||
| @@ -0,0 +1,102 @@ | |||
| 1 | import { createCodeModeRunner } from "@openassistant/core"; | ||
| 2 | import { | ||
| 3 | ChannelType, | ||
| 4 | Client, | ||
| 5 | Events, | ||
| 6 | GatewayIntentBits, | ||
| 7 | type Message, | ||
| 8 | type MessageCreateOptions, | ||
| 9 | Partials, | ||
| 10 | } from "discord.js"; | ||
| 11 | import { Effect } from "effect"; | ||
| 12 | import { DiscordApprovalBridge } from "./approval-bridge.js"; | ||
| 13 | import { InMemoryCalendarStore } from "./calendar-store.js"; | ||
| 14 | import { generateCodeFromPrompt } from "./codegen.js"; | ||
| 15 | import { formatDiscordResponse } from "./format-response.js"; | ||
| 16 | import { createToolTree } from "./tools.js"; | ||
| 17 | |||
| 18 | const token = Bun.env.DISCORD_BOT_TOKEN; | ||
| 19 | if (!token) { | ||
| 20 | throw new Error("Missing DISCORD_BOT_TOKEN"); | ||
| 21 | } | ||
| 22 | |||
| 23 | const approvalTimeoutMs = Number(Bun.env.OPENASSISTANT_APPROVAL_TIMEOUT_MS ?? 300_000); | ||
| 24 | |||
| 25 | const client = new Client({ | ||
| 26 | intents: [ | ||
| 27 | GatewayIntentBits.Guilds, | ||
| 28 | GatewayIntentBits.GuildMessages, | ||
| 29 | GatewayIntentBits.DirectMessages, | ||
| 30 | GatewayIntentBits.MessageContent, | ||
| 31 | ], | ||
| 32 | partials: [Partials.Channel], | ||
| 33 | }); | ||
| 34 | |||
| 35 | const approvalBridge = new DiscordApprovalBridge(approvalTimeoutMs); | ||
| 36 | const calendarStore = new InMemoryCalendarStore(); | ||
| 37 | const tools = createToolTree(calendarStore); | ||
| 38 | |||
| 39 | client.once(Events.ClientReady, (readyClient) => { | ||
| 40 | console.log(`[discord-bot] logged in as ${readyClient.user.tag}`); | ||
| 41 | }); | ||
| 42 | |||
| 43 | client.on(Events.InteractionCreate, async (interaction) => { | ||
| 44 | if (!interaction.isButton()) { | ||
| 45 | return; | ||
| 46 | } | ||
| 47 | await approvalBridge.handleInteraction(interaction); | ||
| 48 | }); | ||
| 49 | |||
| 50 | client.on(Events.MessageCreate, async (message) => { | ||
| 51 | if (shouldIgnore(message)) { | ||
| 52 | return; | ||
| 53 | } | ||
| 54 | await handleMessage(message); | ||
| 55 | }); | ||
| 56 | |||
| 57 | await client.login(token); | ||
| 58 | |||
| 59 | function shouldIgnore(message: Message): boolean { | ||
| 60 | return message.author.bot || message.content.trim().length === 0 || message.channel.type !== ChannelType.DM; | ||
| 61 | } | ||
| 62 | |||
| 63 | async 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 | } | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | import { defineTool, type ToolTree } from "@openassistant/core"; | ||
| 2 | import { Effect } from "effect"; | ||
| 3 | import type { InMemoryCalendarStore } from "./calendar-store.js"; | ||
| 4 | |||
| 5 | export 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 | } | ||
| @@ -0,0 +1,9 @@ | |||
| 1 | { | ||
| 2 | "extends": "../../tsconfig.base.json", | ||
| 3 | "compilerOptions": { | ||
| 4 | "rootDir": "src" | ||
| 5 | }, | ||
| 6 | "include": [ | ||
| 7 | "src/**/*.ts" | ||
| 8 | ] | ||
| 9 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | import { healthcheck } from "@openassistant/core"; | ||
| 2 | import { Effect } from "effect"; | ||
| 3 | |||
| 4 | const program = Effect.flatMap(healthcheck, (status) => | ||
| 5 | Effect.sync(() => { | ||
| 6 | console.log(`[gateway] healthcheck=${status}`); | ||
| 7 | }), | ||
| 8 | ); | ||
| 9 | |||
| 10 | await Effect.runPromise(program); | ||
| @@ -0,0 +1,9 @@ | |||
| 1 | { | ||
| 2 | "extends": "../../tsconfig.base.json", | ||
| 3 | "compilerOptions": { | ||
| 4 | "rootDir": "src" | ||
| 5 | }, | ||
| 6 | "include": [ | ||
| 7 | "src/**/*.ts" | ||
| 8 | ] | ||
| 9 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1 @@ | |||
| 1 | console.log("Hello via Bun!"); | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1,117 @@ | |||
| 1 | import { expect, it } from "@effect/vitest"; | ||
| 2 | import { Effect } from "effect"; | ||
| 3 | import { describe } from "vitest"; | ||
| 4 | import { | ||
| 5 | createCodeModeRunner, | ||
| 6 | defineTool, | ||
| 7 | } from "./runner.js"; | ||
| 8 | |||
| 9 | describe("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 | }); | ||
| @@ -0,0 +1,318 @@ | |||
| 1 | import { Effect } from "effect"; | ||
| 2 | |||
| 3 | export type ToolKind = "read" | "write"; | ||
| 4 | export type ToolApprovalMode = "auto" | "required"; | ||
| 5 | export type ApprovalDecision = "approved" | "denied"; | ||
| 6 | export type ToolReceiptDecision = "auto" | ApprovalDecision; | ||
| 7 | export type ToolReceiptStatus = "succeeded" | "failed" | "denied"; | ||
| 8 | |||
| 9 | export 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 | |||
| 22 | export interface ApprovalRequest { | ||
| 23 | callId: string; | ||
| 24 | toolPath: string; | ||
| 25 | kind: ToolKind; | ||
| 26 | approval: ToolApprovalMode; | ||
| 27 | input: unknown; | ||
| 28 | inputPreview?: string; | ||
| 29 | } | ||
| 30 | |||
| 31 | export 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 | |||
| 40 | export type ToolTree = { | ||
| 41 | readonly [name: string]: ToolTree | ToolDefinition<any, any>; | ||
| 42 | }; | ||
| 43 | |||
| 44 | export interface CodeModeRunnerOptions { | ||
| 45 | tools: ToolTree; | ||
| 46 | requestApproval: (request: ApprovalRequest) => Effect.Effect<ApprovalDecision, unknown>; | ||
| 47 | now?: () => Date; | ||
| 48 | newCallId?: () => string; | ||
| 49 | } | ||
| 50 | |||
| 51 | export interface RunCodeOptions { | ||
| 52 | code: string; | ||
| 53 | } | ||
| 54 | |||
| 55 | export 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 | |||
| 67 | const MAX_PREVIEW_CHARS = 180; | ||
| 68 | const AsyncFunctionCtor = Object.getPrototypeOf( | ||
| 69 | async (_tools: unknown) => undefined, | ||
| 70 | ).constructor as new (argName: string, body: string) => (tools: unknown) => Promise<unknown>; | ||
| 71 | |||
| 72 | export 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 | |||
| 85 | export 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 | |||
| 130 | function 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 | |||
| 152 | function 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 | |||
| 185 | function 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 | |||
| 260 | function 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 | |||
| 269 | function 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 | |||
| 282 | function 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 | |||
| 292 | function 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 | |||
| 303 | function 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 | |||
| 310 | function 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 | } | ||
| @@ -0,0 +1,4 @@ | |||
| 1 | import { Effect } from "effect"; | ||
| 2 | |||
| 3 | export const healthcheck = Effect.succeed("ok"); | ||
| 4 | export * from "./codemode/runner.js"; | ||
| @@ -0,0 +1,9 @@ | |||
| 1 | { | ||
| 2 | "extends": "../../tsconfig.base.json", | ||
| 3 | "compilerOptions": { | ||
| 4 | "rootDir": "src" | ||
| 5 | }, | ||
| 6 | "include": [ | ||
| 7 | "src/**/*.ts" | ||
| 8 | ] | ||
| 9 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | import { defineConfig } from "vitest/config"; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | test: { | ||
| 5 | include: ["packages/**/*.test.ts", "apps/**/*.test.ts"], | ||
| 6 | exclude: ["**/node_modules/**", ".context/**", ".reference/**"], | ||
| 7 | }, | ||
| 8 | }); | ||