@hachej/boring-core 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,83 +1,320 @@
1
- # @boring/core
1
+ # @hachej/boring-core
2
2
 
3
- Database, auth, and app factory for boring-ui apps.
3
+ <div align="center">
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![npm](https://img.shields.io/npm/v/@hachej/boring-core.svg)](https://www.npmjs.com/package/@hachej/boring-core)
7
+
8
+ </div>
9
+
10
+ The foundation package for boring-ui apps: Postgres/Drizzle database schema, email/password auth (better-auth), config loader, Fastify HTTP app factory, and React frontend shell. Every child app imports core first.
4
11
 
5
12
  ```bash
6
- pnpm add @boring/core
13
+ pnpm add @hachej/boring-core
7
14
  ```
8
15
 
9
16
  ---
10
17
 
11
- ## What it provides
18
+ ## TL;DR
12
19
 
13
- - **Database** Drizzle ORM schema for users, workspaces, sessions, invites
14
- - **Auth** — better-auth with workspace support, invite flows, email verification
15
- - **App factory** — Fastify app with auth routes, middleware, and CORS wired in
16
- - **Frontend shell** — `<BoringApp>` React provider with auth pages and workspace switcher
20
+ **The Problem**: Building a multi-user agent-powered app means re-implementing auth, sessions, workspaces, invites, email flows, and an app shell every single time. These are the same across every deployment.
17
21
 
18
- ---
22
+ **The Solution**: `@hachej/boring-core` provides a complete app skeleton — Postgres DB, better-auth with email verification + password reset + magic links, workspace membership with roles, email transport (Resend/SMTP/console), and a `<BoringApp>` React shell with auth pages. You bring the domain logic.
23
+
24
+ ### Why Use @hachej/boring-core?
19
25
 
20
- ## Quickstart
26
+ | Feature | What It Does |
27
+ |---------|--------------|
28
+ | **Full auth suite** | Email/password + email verification + password reset + magic links (better-auth) |
29
+ | **Workspace management** | Create, update, delete workspaces; member roles (owner/editor/viewer); invites |
30
+ | **Fastify app factory** | Pre-wired with helmet, CORS, rate limiting, secret redaction, graceful shutdown |
31
+ | **Drizzle + Postgres** | Ready-to-run schema for users, workspaces, members, invites, settings |
32
+ | **Email transport** | Resend (default), SMTP, or console — pluggable via URL scheme |
33
+ | **<BoringApp> shell** | Client-rendered React shell with auth gate, theme toggle, workspace switcher |
34
+ | **Config loader** | TOML + env vars merged, Zod-validated, redacted for frontend |
21
35
 
22
- Server:
36
+ ---
37
+
38
+ ## Quick Example
23
39
 
24
40
  ```ts
25
- import { createCoreApp, loadConfig } from "@boring/core/server"
41
+ // Server 4 lines to a full app
42
+ import { createCoreApp, loadConfig } from "@hachej/boring-core/server"
26
43
 
27
44
  const config = await loadConfig()
28
- const app = await createCoreApp(config)
29
- await app.listen({ port: config.port })
45
+ const app = await createCoreApp(config) // Fastify + DB + auth + routes
46
+
47
+ await app.listen({ port: 3000 })
30
48
  ```
31
49
 
32
- Frontend:
50
+ ```tsx
51
+ // Frontend — mount auth gate + workspace routing
52
+ <BoringApp>
53
+ <Route path="/workspace/:id" element={<WorkspaceRoute />} />
54
+ <Route path="/settings" element={<Settings />} />
55
+ </BoringApp>
56
+ ```
33
57
 
34
58
  ```tsx
35
- import { BoringApp } from "@boring/core/front"
36
- import { WorkspaceProvider, IdeLayout } from "@boring/workspace"
37
-
38
- export function App() {
39
- return (
40
- <BoringApp>
41
- <Route path="/" element={<WorkspaceProvider><IdeLayout /></WorkspaceProvider>} />
42
- </BoringApp>
43
- )
44
- }
59
+ // In your components typed auth + workspace access
60
+ const user = useUser()
61
+ const workspace = useCurrentWorkspace()
62
+ const role = useWorkspaceRole() // 'owner' | 'editor' | 'viewer'
45
63
  ```
46
64
 
47
65
  ---
48
66
 
49
- ## Config
67
+ ## Design Philosophy
68
+
69
+ 1. **Core owns persistence and identity** — DB tables, auth, sessions, workspaces, invites. Everything else injects stores via interfaces.
70
+ 2. **One config source** — `boring.app.toml` + environment variables merged, Zod-validated at boot. No scattered config.
71
+ 3. **Email flows are real, not stubs** — password reset, email verification, magic links, workspace invites — all shipped with React Email templates.
72
+ 4. **Swap seams, not rewrites** — `AuthProvider`, `UserStore`, `WorkspaceStore` are interfaces. The default impl is Postgres; swap via `createCoreApp({ authProvider })`.
73
+ 5. **Fail closed on auth** — config fetch failure throws a `ConfigFetchError` with retries. Users see "Cannot reach server" not a blank page.
74
+
75
+ ---
50
76
 
51
- Minimum `.env` to get started:
77
+ ## Installation
52
78
 
53
79
  ```bash
54
- DATABASE_URL=postgres://postgres:postgres@localhost:5432/boring
55
- BETTER_AUTH_SECRET=<any 64-char hex string>
56
- ANTHROPIC_API_KEY=sk-ant-...
80
+ # pnpm
81
+ pnpm add @hachej/boring-core @hachej/boring-workspace
82
+
83
+ # npm
84
+ npm install @hachej/boring-core @hachej/boring-workspace
85
+
86
+ # from source
87
+ git clone https://github.com/hachej/boring-ui.git
88
+ cd boring-ui && pnpm install
89
+ pnpm --filter @hachej/boring-core build
57
90
  ```
58
91
 
59
- Run migrations:
92
+ ### Dependencies
93
+
94
+ Postgres is required for production. For dev, set `CORE_STORES=local` and core runs in-memory (state resets on restart).
95
+
96
+ ---
97
+
98
+ ## Quick Start
99
+
100
+ ### 1. Set Environment
60
101
 
61
102
  ```bash
62
- pnpm --filter @boring/core drizzle:migrate
103
+ # .env
104
+ DATABASE_URL=postgres://user:pass@localhost:5432/myapp
105
+ BETTER_AUTH_SECRET=<32-byte random hex>
106
+ BETTER_AUTH_URL=http://localhost:3000
107
+ WORKSPACE_SETTINGS_ENCRYPTION_KEY=<32-byte hex>
108
+ MAIL_FROM=noreply@myapp.dev
109
+ MAIL_TRANSPORT_URL=resend://re_xxxxxxxxxxxxxxxx
63
110
  ```
64
111
 
65
- ---
112
+ ### 2. Create Config File
113
+
114
+ ```toml
115
+ # boring.app.toml
116
+ [app]
117
+ id = "my-app"
118
+
119
+ [frontend.branding]
120
+ name = "My App"
121
+ logo = "/logo.svg"
66
122
 
67
- ## Package surfaces
123
+ [features]
124
+ invites_enabled = true
125
+ invite_ttl_days = 7
126
+ ```
127
+
128
+ ### 3. Run Migrations
129
+
130
+ ```bash
131
+ pnpm drizzle-kit generate --config node_modules/@hachej/boring-core/drizzle.config.ts
132
+ pnpm drizzle-kit migrate --config node_modules/@hachej/boring-core/drizzle.config.ts
133
+ ```
134
+
135
+ ### 4. Server Entry
68
136
 
69
137
  ```ts
70
- import { ... } from "@boring/core/server" // Fastify app factory, config
71
- import { ... } from "@boring/core/front" // React shell, auth pages
72
- import { ... } from "@boring/core/db" // Drizzle schema and client
138
+ import { createCoreApp, loadConfig } from "@hachej/boring-core/server"
139
+
140
+ const config = await loadConfig()
141
+ const app = await createCoreApp(config)
142
+
143
+ // add child-app routes
144
+ app.get("/api/v1/my-thing", async () => ({ ok: true }))
145
+
146
+ await app.listen({ port: config.port })
147
+ ```
148
+
149
+ ### 5. Frontend Entry
150
+
151
+ ```tsx
152
+ import { createRoot } from "react-dom/client"
153
+ import { BoringApp } from "@hachej/boring-core/front"
154
+ import { Route } from "react-router-dom"
155
+ import "@hachej/boring-core/theme.css"
156
+
157
+ createRoot(document.getElementById("root")!).render(
158
+ <BoringApp>
159
+ <Route path="/" element={<Dashboard />} />
160
+ </BoringApp>,
161
+ )
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Package Surfaces
167
+
168
+ | Import | Environment | What You Get |
169
+ |--------|-------------|--------------|
170
+ | `@hachej/boring-core/server` | Node | `createCoreApp`, `loadConfig`, auth, stores, routes |
171
+ | `@hachej/boring-core/server/db` | Node | Drizzle schema, migrations, store interfaces |
172
+ | `@hachej/boring-core/front` | Browser | `<BoringApp>`, hooks, auth pages, components |
173
+ | `@hachej/boring-core/shared` | Any | `User`, `Workspace`, `HttpError`, `ErrorCode` types |
174
+ | `@hachej/boring-core/theme.css` | Browser | CSS theme tokens for the frontend shell |
175
+ | `@hachej/boring-core/app/front` | Browser | App composition helpers (`WorkspaceAgentFront`, etc.) |
176
+ | `@hachej/boring-core/app/server` | Node | App composition helpers (`createWorkspaceAgentApp`) |
177
+
178
+ ---
179
+
180
+ ## Configuration
181
+
182
+ ### Environment Variables
183
+
184
+ | Variable | Required | Description |
185
+ |----------|----------|-------------|
186
+ | `DATABASE_URL` | Yes (prod) | Postgres connection string |
187
+ | `BETTER_AUTH_SECRET` | Yes | 32-byte hex — signs session cookies |
188
+ | `BETTER_AUTH_URL` | Yes | Public URL for OAuth callbacks |
189
+ | `WORKSPACE_SETTINGS_ENCRYPTION_KEY` | Yes (prod) | 32-byte hex — encrypts workspace settings |
190
+ | `MAIL_FROM` | Yes (prod) | Sender address for auth emails |
191
+ | `MAIL_TRANSPORT_URL` | Yes (prod) | `resend://key`, `smtp://host`, or `console://` |
192
+ | `CORE_STORES` | No | `postgres` (default) or `local` (in-memory dev) |
193
+ | `CORS_ORIGINS` | Yes (prod) | Comma-separated allowlist |
194
+ | `SEND_WELCOME_EMAIL` | No | Default `true` — suppress with `false` |
195
+ | `SESSION_TTL_SECONDS` | No | Default 2,592,000 (30 days) |
196
+
197
+ ---
198
+
199
+ ## Architecture
200
+
201
+ ```
202
+ ┌──────────────────────┐
203
+ │ Browser Client │
204
+ │ /auth/* + /me + │
205
+ │ /workspaces/* │
206
+ └──────────┬───────────┘
207
+ │ HTTP (typed, cookie auth)
208
+ ┌──────────▼───────────┐
209
+ │ Fastify App │
210
+ │ ├── authHook (req.user)
211
+ │ ├── helmet + CORS │
212
+ │ ├── rate limits │
213
+ │ ├── secret redaction│
214
+ │ └── graceful shutdown
215
+ └──────────┬───────────┘
216
+
217
+ ┌──────────▼───────────┐
218
+ │ better-auth │
219
+ │ (sessions, email, │
220
+ │ password reset) │
221
+ └──────────┬───────────┘
222
+
223
+ ┌──────────▼───────────┐
224
+ │ Drizzle + Postgres │
225
+ │ users, sessions, │
226
+ │ workspaces, members, │
227
+ │ invites, settings │
228
+ └──────────────────────┘
73
229
  ```
74
230
 
231
+ ### Error Handling Contract
232
+
233
+ All errors flow through a single `setErrorHandler`:
234
+
235
+ | Condition | Status | Code |
236
+ |-----------|--------|------|
237
+ | No/expired session | 401 | `unauthorized` |
238
+ | Insufficient role | 403 | `forbidden` / `not_member` |
239
+ | Zod validation fail | 400 | `validation_failed` |
240
+ | Rate limited | 429 | `rate_limited` + `Retry-After` |
241
+ | DB ping fails | 503 | `db_unavailable` |
242
+ | Everything else | 500 | `internal_error` |
243
+
244
+ Every response includes `{ error, code, message, requestId }`. Client-side `apiFetch` parses this into `HttpError` instances.
245
+
246
+ ---
247
+
248
+ ## How @hachej/boring-core Compares
249
+
250
+ | Feature | @hachej/boring-core | Supabase + custom | Firebase | Roll your own |
251
+ |---------|---------------------|-------------------|----------|---------------|
252
+ | Auth flows | ✅ email + reset + magic link | ✅ OAuth only | ✅ OAuth/phone | Weeks to build |
253
+ | Workspaces + invites | ✅ owner/editor/viewer roles | ❌ Custom tables | ❌ Custom rules | ~1 week |
254
+ | Email templates | ✅ 5 React Email templates | ❌ You write them | ❌ SendGrid setup | ~3 days |
255
+ | App shell | ✅ `<BoringApp>` + hooks | ❌ DIY | ❌ DIY | ~1 week |
256
+ | Rate limiting | ✅ pre-wired routes | ❌ Edge functions | ⚠️ Cloud rules | ~2 days |
257
+ | Config validation | ✅ TOML + env + Zod | ❌ dotenv only | ⚠️ Remote config | Custom |
258
+
259
+ **When to use @hachej/boring-core:**
260
+ - Building a multi-user app around an AI agent
261
+ - You need auth + workspaces + invites in days, not weeks
262
+ - You're deploying to Fly.io, Render, Railway, or any Postgres-capable host
263
+
264
+ **When it might not fit:**
265
+ - You need server-side rendering (client-rendered only)
266
+ - You want SQLite (Postgres-only with Drizzle)
267
+ - You need Google/Apple/Discord OAuth (planned for v1.x)
268
+ - You need billing/Stripe integration (future `@boring/cloud` package)
269
+
270
+ ---
271
+
272
+ ## Troubleshooting
273
+
274
+ | Error | Cause | Fix |
275
+ |-------|-------|-----|
276
+ | `ConfigValidationError` at boot | Missing required env var | Check `.env` has all required vars |
277
+ | `config_fetch_failed` in browser | API server not reachable | Verify `BETTER_AUTH_URL` matches |
278
+ | `mail_disabled` warning at boot | `MAIL_FROM` not set | Set `MAIL_TRANSPORT_URL=console://` for dev |
279
+ | `unauthorized` on `/api/v1/me` | No session cookie | Check `BETTER_AUTH_URL` and `CORS_ORIGINS` |
280
+ | `db_unavailable` on `/health` | Postgres can't connect | Verify `DATABASE_URL` and network access |
281
+
282
+ ---
283
+
284
+ ## Limitations
285
+
286
+ - **Postgres only** — No SQLite/libsql support in v1.
287
+ - **Client-rendered only** — `<BoringApp>` mounts client-side. No SSR.
288
+ - **GitHub OAuth deferred** — Planned for v1.x, bundled with agent's GitHub App install.
289
+ - **No billing** — Stripe integration planned for `@boring/cloud` package.
290
+ - **In-memory stores are dev-only** — `CORE_STORES=local` resets on restart. Not for production.
291
+ - **Partial swap seams** — `AuthProvider` is swappable, but the React auth surfaces (`useSession`, sign-in pages) are better-auth-shaped.
292
+
293
+ ---
294
+
295
+ ## FAQ
296
+
297
+ **Q: Can I use this without Postgres?**
298
+ A: In dev, yes — set `CORE_STORES=local`. State is in-memory and resets on restart. For production, Postgres is required.
299
+
300
+ **Q: How do I add Google/Discord OAuth?**
301
+ A: better-auth supports these out of the box. Add the provider config to `createAuth()` in the core source. Official v1.x support planned.
302
+
303
+ **Q: Can I swap better-auth for Clerk/Neon?**
304
+ A: The `AuthProvider` interface is designed as a swap seam. You'll need to re-implement the React auth surfaces (`SignInPage`, `useSession`, etc.) and preserve the `users.id` continuity invariant.
305
+
306
+ **Q: How do email templates work?**
307
+ A: Five React Email components (`VerifyEmail`, `ResetPassword`, `MagicLink`, `WorkspaceInvite`, `Welcome`) rendered via `@react-email/render`. CSS is inlined. Swap them by providing your own mail transport.
308
+
309
+ **Q: What's the difference between `@hachej/boring-core/server` and `@hachej/boring-core/server/db`?**
310
+ A: `server` includes the full Fastify app, routes, auth, and stores. `server/db` is the Drizzle schema + connection + store interfaces only — useful for migration tooling and type-only imports.
311
+
312
+ ---
313
+
314
+ *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
315
+
75
316
  ---
76
317
 
77
- ## Part of [boring-ui](https://github.com/hachej/boring-ui)
318
+ ## License
78
319
 
79
- | Package | Role |
80
- |---|---|
81
- | `@boring/core` | DB, auth, app factory |
82
- | `@boring/workspace` | Plugin system, layouts |
83
- | `@boring/agent` | Agent runtime + tools |
320
+ MIT
@@ -23,6 +23,7 @@ import { access, mkdir, readFile, stat } from "fs/promises";
23
23
  import { createReadStream } from "fs";
24
24
  import path from "path";
25
25
  import {
26
+ compactPiPackages,
26
27
  registerAgentRoutes
27
28
  } from "@hachej/boring-agent/server";
28
29
  import {
@@ -63,6 +64,24 @@ var FRONTEND_AUTH_PAGES = /* @__PURE__ */ new Set([
63
64
  var FRONTEND_AUTH_PAGES_SPA_ONLY = /* @__PURE__ */ new Set([
64
65
  "/auth/verify-email"
65
66
  ]);
67
+ function dedupeStrings(values) {
68
+ return Array.from(new Set(values));
69
+ }
70
+ function mergeResourceLoaderOptions(base, override) {
71
+ if (!base && !override) return void 0;
72
+ return {
73
+ ...base,
74
+ ...override,
75
+ additionalSkillPaths: dedupeStrings([
76
+ ...base?.additionalSkillPaths ?? [],
77
+ ...override?.additionalSkillPaths ?? []
78
+ ]),
79
+ piPackages: compactPiPackages([
80
+ ...base?.piPackages ?? [],
81
+ ...override?.piPackages ?? []
82
+ ])
83
+ };
84
+ }
66
85
  function contentType(filePath) {
67
86
  const ext = path.extname(filePath).toLowerCase();
68
87
  return MIME_TYPES[ext] ?? "application/octet-stream";
@@ -131,6 +150,11 @@ function httpError(message, statusCode) {
131
150
  error.statusCode = statusCode;
132
151
  return error;
133
152
  }
153
+ function firstString(value) {
154
+ if (typeof value === "string") return value;
155
+ if (!Array.isArray(value)) return void 0;
156
+ return value.find((item) => typeof item === "string");
157
+ }
134
158
  function validateWorkspaceIdSegment(value) {
135
159
  const workspaceId = value.trim();
136
160
  if (!workspaceId) throw httpError("workspace id is required", 400);
@@ -139,12 +163,14 @@ function validateWorkspaceIdSegment(value) {
139
163
  }
140
164
  return workspaceId;
141
165
  }
142
- async function resolveAuthorizedWorkspaceId(request, workspaceStore) {
143
- const headerValue = request.headers?.["x-boring-workspace-id"];
166
+ function resolveWorkspaceIdFromRequest(request) {
167
+ const headers = request.headers ?? {};
168
+ const headerValue = headers["x-boring-workspace-id"] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === "x-boring-workspace-id")?.[1];
144
169
  const query = request.query;
145
- const queryValue = query?.workspaceId;
146
- const workspaceId = typeof headerValue === "string" ? headerValue : typeof queryValue === "string" ? queryValue : "";
147
- const normalizedWorkspaceId = validateWorkspaceIdSegment(workspaceId);
170
+ return validateWorkspaceIdSegment(firstString(headerValue) ?? firstString(query?.workspaceId) ?? "");
171
+ }
172
+ async function resolveAuthorizedWorkspaceId(request, workspaceStore) {
173
+ const normalizedWorkspaceId = resolveWorkspaceIdFromRequest(request);
148
174
  const user = request.user;
149
175
  if (!user?.id) throw httpError("authentication required", 401);
150
176
  let member = false;
@@ -331,26 +357,28 @@ async function createCoreWorkspaceAgentServer(options = {}) {
331
357
  const provisionedWorkspaceRoots = /* @__PURE__ */ new Map();
332
358
  const ensureWorkspaceProvisioned = (root) => {
333
359
  if (pluginCollection.provisioningContributions.length === 0) return Promise.resolve();
334
- const existing = provisionedWorkspaceRoots.get(root);
360
+ const resolvedRoot = path.resolve(root);
361
+ const existing = provisionedWorkspaceRoots.get(resolvedRoot);
335
362
  if (existing) return existing;
336
363
  const pending = provisionWorkspaceAgentServer({
337
- workspaceRoot: root,
364
+ workspaceRoot: resolvedRoot,
338
365
  provisioningContributions: pluginCollection.provisioningContributions,
339
366
  force: options.forceProvisioning
340
367
  }).catch((error) => {
341
- provisionedWorkspaceRoots.delete(root);
368
+ provisionedWorkspaceRoots.delete(resolvedRoot);
342
369
  throw error;
343
370
  });
344
- provisionedWorkspaceRoots.set(root, pending);
371
+ provisionedWorkspaceRoots.set(resolvedRoot, pending);
345
372
  return pending;
346
373
  };
347
374
  await ensureWorkspaceProvisioned(workspaceRoot);
348
375
  const bridges = /* @__PURE__ */ new Map();
349
376
  const getUiBridge = (workspaceId) => {
350
- let bridge = bridges.get(workspaceId);
377
+ const safeWorkspaceId = validateWorkspaceIdSegment(workspaceId);
378
+ let bridge = bridges.get(safeWorkspaceId);
351
379
  if (!bridge) {
352
380
  bridge = createInMemoryBridge();
353
- bridges.set(workspaceId, bridge);
381
+ bridges.set(safeWorkspaceId, bridge);
354
382
  }
355
383
  return bridge;
356
384
  };
@@ -360,6 +388,30 @@ async function createCoreWorkspaceAgentServer(options = {}) {
360
388
  await ensureWorkspaceProvisioned(root);
361
389
  return root;
362
390
  };
391
+ const resourceLoaderOptionsByRoot = /* @__PURE__ */ new Map();
392
+ const getPluginResourceLoaderOptions = (root) => {
393
+ const resolvedRoot = path.resolve(root);
394
+ if (resourceLoaderOptionsByRoot.has(resolvedRoot)) {
395
+ return resourceLoaderOptionsByRoot.get(resolvedRoot);
396
+ }
397
+ const scopedPluginCollection = collectWorkspaceAgentServerPlugins({
398
+ workspaceRoot: resolvedRoot,
399
+ systemPromptAppend: options.systemPromptAppend,
400
+ resourceLoaderOptions: options.resourceLoaderOptions,
401
+ plugins: options.plugins,
402
+ excludeDefaults: options.excludeDefaults
403
+ });
404
+ resourceLoaderOptionsByRoot.set(
405
+ resolvedRoot,
406
+ scopedPluginCollection.agentOptions.resourceLoaderOptions
407
+ );
408
+ return scopedPluginCollection.agentOptions.resourceLoaderOptions;
409
+ };
410
+ const resolveResourceLoaderOptions = async (ctx) => {
411
+ const pluginOptions = getPluginResourceLoaderOptions(ctx.workspaceRoot);
412
+ const callerOptions = options.getResourceLoaderOptions ? await options.getResourceLoaderOptions(ctx) : void 0;
413
+ return mergeResourceLoaderOptions(pluginOptions, callerOptions);
414
+ };
363
415
  await app.register(registerAgentRoutes, {
364
416
  workspaceRoot,
365
417
  sessionId: options.sessionId,
@@ -373,6 +425,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
373
425
  ],
374
426
  systemPromptAppend: pluginCollection.agentOptions.systemPromptAppend,
375
427
  resourceLoaderOptions: pluginCollection.agentOptions.resourceLoaderOptions,
428
+ getResourceLoaderOptions: resolveResourceLoaderOptions,
429
+ sessionNamespace: options.sessionNamespace,
430
+ getSessionNamespace: options.getSessionNamespace,
376
431
  getExtraTools: async (ctx) => {
377
432
  const callerTools = options.getExtraTools ? await options.getExtraTools(ctx) : [];
378
433
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-core",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
@@ -78,9 +78,9 @@
78
78
  "react-router-dom": "^7.14.2",
79
79
  "smol-toml": "^1.6.1",
80
80
  "zod": "^3.25.76",
81
- "@hachej/boring-agent": "0.1.13",
82
- "@hachej/boring-ui-kit": "0.1.13",
83
- "@hachej/boring-workspace": "0.1.13"
81
+ "@hachej/boring-agent": "0.1.14",
82
+ "@hachej/boring-ui-kit": "0.1.14",
83
+ "@hachej/boring-workspace": "0.1.14"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@testing-library/jest-dom": "^6.9.1",