bazilion 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.understand-anything/.understandignore +25 -0
  2. package/.understand-anything/fingerprints.json +14267 -0
  3. package/.understand-anything/knowledge-graph.json +18128 -0
  4. package/.understand-anything/meta.json +6 -0
  5. package/CLAUDE.md +164 -0
  6. package/LICENSE +21 -0
  7. package/README.md +195 -0
  8. package/apps/cli/package.json +21 -0
  9. package/apps/cli/src/auth-file.ts +18 -0
  10. package/apps/cli/src/client.ts +37 -0
  11. package/apps/cli/src/columnize.ts +32 -0
  12. package/apps/cli/src/commands/agent.ts +574 -0
  13. package/apps/cli/src/commands/auth.ts +110 -0
  14. package/apps/cli/src/commands/backup.ts +135 -0
  15. package/apps/cli/src/commands/completion.ts +155 -0
  16. package/apps/cli/src/commands/config.ts +95 -0
  17. package/apps/cli/src/commands/doctor.ts +131 -0
  18. package/apps/cli/src/commands/group.ts +132 -0
  19. package/apps/cli/src/commands/inbox.ts +82 -0
  20. package/apps/cli/src/commands/login.ts +73 -0
  21. package/apps/cli/src/commands/memory.ts +106 -0
  22. package/apps/cli/src/commands/profile.ts +259 -0
  23. package/apps/cli/src/commands/provider.ts +170 -0
  24. package/apps/cli/src/commands/send.ts +24 -0
  25. package/apps/cli/src/commands/serve.ts +89 -0
  26. package/apps/cli/src/commands/skill.ts +120 -0
  27. package/apps/cli/src/commands/token.ts +148 -0
  28. package/apps/cli/src/commands/trigger.ts +129 -0
  29. package/apps/cli/src/commands/uninstall.ts +156 -0
  30. package/apps/cli/src/index.ts +196 -0
  31. package/apps/cli/src/paths.ts +12 -0
  32. package/apps/cli/test/agent.test.ts +213 -0
  33. package/apps/cli/test/backup.test.ts +95 -0
  34. package/apps/cli/test/chat.test.ts +539 -0
  35. package/apps/cli/test/columnize.test.ts +29 -0
  36. package/apps/cli/test/completion.test.ts +45 -0
  37. package/apps/cli/test/config-page.test.ts +151 -0
  38. package/apps/cli/test/group.test.ts +74 -0
  39. package/apps/cli/test/helpers.ts +70 -0
  40. package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
  41. package/apps/cli/test/inbox.test.ts +147 -0
  42. package/apps/cli/test/memory.test.ts +69 -0
  43. package/apps/cli/test/profile.test.ts +110 -0
  44. package/apps/cli/test/send.test.ts +30 -0
  45. package/apps/cli/test/server-fixture.ts +212 -0
  46. package/apps/cli/test/session-head.test.ts +45 -0
  47. package/apps/cli/test/skill.test.ts +128 -0
  48. package/apps/cli/test/token.test.ts +109 -0
  49. package/apps/cli/test/trigger.test.ts +245 -0
  50. package/apps/cli/tsconfig.json +4 -0
  51. package/apps/daemon/package.json +31 -0
  52. package/apps/daemon/src/app.ts +41 -0
  53. package/apps/daemon/src/core/agent/archive.ts +8 -0
  54. package/apps/daemon/src/core/agent/delete.ts +30 -0
  55. package/apps/daemon/src/core/agent/resolve.ts +31 -0
  56. package/apps/daemon/src/core/agent/spawn.ts +95 -0
  57. package/apps/daemon/src/core/agent/unarchive.ts +11 -0
  58. package/apps/daemon/src/core/availableModels.ts +58 -0
  59. package/apps/daemon/src/core/db/client.ts +113 -0
  60. package/apps/daemon/src/core/db/migrate.ts +41 -0
  61. package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
  62. package/apps/daemon/src/core/group/delete.ts +21 -0
  63. package/apps/daemon/src/core/group/register.ts +68 -0
  64. package/apps/daemon/src/core/index.ts +71 -0
  65. package/apps/daemon/src/core/paths.ts +56 -0
  66. package/apps/daemon/src/core/profile/create.ts +78 -0
  67. package/apps/daemon/src/core/profile/delete.ts +26 -0
  68. package/apps/daemon/src/core/profile/identity.ts +70 -0
  69. package/apps/daemon/src/core/profile/load.ts +45 -0
  70. package/apps/daemon/src/core/profile/seed.ts +74 -0
  71. package/apps/daemon/src/core/profile/templates.ts +60 -0
  72. package/apps/daemon/src/core/profile/update.ts +57 -0
  73. package/apps/daemon/src/core/profile/validate.ts +9 -0
  74. package/apps/daemon/src/core/repos/agents.ts +202 -0
  75. package/apps/daemon/src/core/repos/config.ts +78 -0
  76. package/apps/daemon/src/core/repos/groups.ts +55 -0
  77. package/apps/daemon/src/core/repos/messages.ts +127 -0
  78. package/apps/daemon/src/core/repos/profiles.ts +83 -0
  79. package/apps/daemon/src/core/repos/providerModels.ts +58 -0
  80. package/apps/daemon/src/core/repos/providerState.ts +37 -0
  81. package/apps/daemon/src/core/repos/secrets.ts +145 -0
  82. package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
  83. package/apps/daemon/src/core/repos/triggers.ts +101 -0
  84. package/apps/daemon/src/core/repos/webTokens.ts +87 -0
  85. package/apps/daemon/src/core/secrets.ts +65 -0
  86. package/apps/daemon/src/core/services.ts +264 -0
  87. package/apps/daemon/src/core/skills/discover.ts +28 -0
  88. package/apps/daemon/src/core/skills/import.ts +136 -0
  89. package/apps/daemon/src/core/skills/parse.ts +52 -0
  90. package/apps/daemon/src/core/skills/resolve.ts +50 -0
  91. package/apps/daemon/src/index.ts +45 -0
  92. package/apps/daemon/src/lib/agent-cancel.ts +48 -0
  93. package/apps/daemon/src/lib/agent-id.ts +13 -0
  94. package/apps/daemon/src/lib/agent-turn.ts +56 -0
  95. package/apps/daemon/src/lib/api-key.ts +56 -0
  96. package/apps/daemon/src/lib/auth.ts +41 -0
  97. package/apps/daemon/src/lib/cron.ts +93 -0
  98. package/apps/daemon/src/lib/ctx.ts +80 -0
  99. package/apps/daemon/src/lib/messaging-host.ts +34 -0
  100. package/apps/daemon/src/lib/middleware-auth.ts +52 -0
  101. package/apps/daemon/src/lib/scheduler.ts +294 -0
  102. package/apps/daemon/src/routes/agents.ts +772 -0
  103. package/apps/daemon/src/routes/auth-login.ts +193 -0
  104. package/apps/daemon/src/routes/config.ts +267 -0
  105. package/apps/daemon/src/routes/groups.ts +133 -0
  106. package/apps/daemon/src/routes/messages.ts +29 -0
  107. package/apps/daemon/src/routes/misc.ts +239 -0
  108. package/apps/daemon/src/routes/profiles.ts +197 -0
  109. package/apps/daemon/src/routes/skills.ts +123 -0
  110. package/apps/daemon/src/routes/triggers.ts +29 -0
  111. package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
  112. package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
  113. package/apps/daemon/src/runtime/index.ts +77 -0
  114. package/apps/daemon/src/runtime/memory/files.ts +103 -0
  115. package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
  116. package/apps/daemon/src/runtime/memory/types.ts +16 -0
  117. package/apps/daemon/src/runtime/pi/events.ts +173 -0
  118. package/apps/daemon/src/runtime/pi/session.ts +536 -0
  119. package/apps/daemon/src/runtime/pi/tools.ts +85 -0
  120. package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
  121. package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
  122. package/apps/daemon/src/runtime/providers/registry.ts +374 -0
  123. package/apps/daemon/src/runtime/providers/retry.ts +176 -0
  124. package/apps/daemon/src/runtime/providers/types.ts +33 -0
  125. package/apps/daemon/src/runtime/session/prompt.ts +83 -0
  126. package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
  127. package/apps/daemon/src/runtime/tools/home.ts +114 -0
  128. package/apps/daemon/src/runtime/tools/memory.ts +81 -0
  129. package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
  130. package/apps/daemon/src/runtime/tools/registry.ts +29 -0
  131. package/apps/daemon/src/runtime/tools/types.ts +13 -0
  132. package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
  133. package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
  134. package/apps/daemon/src/runtime/tools/web.ts +273 -0
  135. package/apps/daemon/src/runtime/worker/entry.ts +221 -0
  136. package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
  137. package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
  138. package/apps/daemon/test/core/agents.test.ts +319 -0
  139. package/apps/daemon/test/core/available-models.test.ts +63 -0
  140. package/apps/daemon/test/core/config.test.ts +99 -0
  141. package/apps/daemon/test/core/groups.test.ts +63 -0
  142. package/apps/daemon/test/core/helpers.ts +50 -0
  143. package/apps/daemon/test/core/identity.test.ts +85 -0
  144. package/apps/daemon/test/core/migrations.test.ts +83 -0
  145. package/apps/daemon/test/core/profiles.test.ts +182 -0
  146. package/apps/daemon/test/core/provider-models.test.ts +57 -0
  147. package/apps/daemon/test/core/provider-state.test.ts +36 -0
  148. package/apps/daemon/test/core/skill-meta.test.ts +45 -0
  149. package/apps/daemon/test/core/skills.test.ts +271 -0
  150. package/apps/daemon/test/core/triggers.test.ts +182 -0
  151. package/apps/daemon/test/core/web-tokens.test.ts +79 -0
  152. package/apps/daemon/test/cron.test.ts +90 -0
  153. package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
  154. package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
  155. package/apps/daemon/test/runtime/memory.test.ts +78 -0
  156. package/apps/daemon/test/runtime/messaging.test.ts +213 -0
  157. package/apps/daemon/test/runtime/mock-server.ts +65 -0
  158. package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
  159. package/apps/daemon/test/runtime/providers.test.ts +306 -0
  160. package/apps/daemon/test/runtime/retry.test.ts +191 -0
  161. package/apps/daemon/test/runtime/session-head.test.ts +90 -0
  162. package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
  163. package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
  164. package/apps/daemon/tsconfig.json +4 -0
  165. package/apps/mobile/README.md +60 -0
  166. package/apps/mobile/app/_layout.tsx +58 -0
  167. package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
  168. package/apps/mobile/app/agents/[id]/index.tsx +166 -0
  169. package/apps/mobile/app/agents/index.tsx +212 -0
  170. package/apps/mobile/app/index.tsx +21 -0
  171. package/apps/mobile/app/pair.tsx +226 -0
  172. package/apps/mobile/app/settings.tsx +419 -0
  173. package/apps/mobile/app.json +49 -0
  174. package/apps/mobile/assets/adaptive-icon.png +0 -0
  175. package/apps/mobile/assets/favicon.png +0 -0
  176. package/apps/mobile/assets/icon.png +0 -0
  177. package/apps/mobile/assets/splash-icon.png +0 -0
  178. package/apps/mobile/babel.config.js +6 -0
  179. package/apps/mobile/metro.config.js +28 -0
  180. package/apps/mobile/package.json +44 -0
  181. package/apps/mobile/src/auth.ts +66 -0
  182. package/apps/mobile/src/pair-url.ts +48 -0
  183. package/apps/mobile/src/theme-context.tsx +88 -0
  184. package/apps/mobile/src/theme.ts +135 -0
  185. package/apps/mobile/test/pair-url.test.ts +46 -0
  186. package/apps/mobile/tsconfig.json +23 -0
  187. package/apps/web/components.json +25 -0
  188. package/apps/web/package.json +39 -0
  189. package/apps/web/public/baziu.svg +8 -0
  190. package/apps/web/src/components/AgentTabs.tsx +45 -0
  191. package/apps/web/src/components/BaziuLogo.tsx +21 -0
  192. package/apps/web/src/components/ChatPane.tsx +1033 -0
  193. package/apps/web/src/components/ConfigTabs.tsx +29 -0
  194. package/apps/web/src/components/CopyButton.tsx +68 -0
  195. package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
  196. package/apps/web/src/components/FieldRow.tsx +94 -0
  197. package/apps/web/src/components/Footer.tsx +10 -0
  198. package/apps/web/src/components/PawIcon.tsx +15 -0
  199. package/apps/web/src/components/Sidebar.tsx +287 -0
  200. package/apps/web/src/components/SpawnDialog.tsx +129 -0
  201. package/apps/web/src/components/ThemeToggle.tsx +75 -0
  202. package/apps/web/src/components/TopNav.tsx +34 -0
  203. package/apps/web/src/components/ui/button.tsx +67 -0
  204. package/apps/web/src/components/ui/card.tsx +103 -0
  205. package/apps/web/src/components/ui/checkbox.tsx +31 -0
  206. package/apps/web/src/components/ui/dialog.tsx +168 -0
  207. package/apps/web/src/components/ui/input.tsx +19 -0
  208. package/apps/web/src/components/ui/label.tsx +22 -0
  209. package/apps/web/src/components/ui/radio-group.tsx +44 -0
  210. package/apps/web/src/components/ui/select.tsx +192 -0
  211. package/apps/web/src/components/ui/separator.tsx +26 -0
  212. package/apps/web/src/components/ui/table.tsx +116 -0
  213. package/apps/web/src/components/ui/tabs.tsx +88 -0
  214. package/apps/web/src/components/ui/textarea.tsx +18 -0
  215. package/apps/web/src/lib/auth.ts +50 -0
  216. package/apps/web/src/lib/daemon-client.ts +34 -0
  217. package/apps/web/src/lib/md.ts +45 -0
  218. package/apps/web/src/lib/utils.ts +6 -0
  219. package/apps/web/src/lib/wire-constants.ts +27 -0
  220. package/apps/web/src/routeTree.gen.ts +408 -0
  221. package/apps/web/src/router.tsx +20 -0
  222. package/apps/web/src/routes/__root.tsx +123 -0
  223. package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
  224. package/apps/web/src/routes/agents/$id/index.tsx +527 -0
  225. package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
  226. package/apps/web/src/routes/agents/index.tsx +265 -0
  227. package/apps/web/src/routes/api/$.ts +88 -0
  228. package/apps/web/src/routes/config/index.tsx +315 -0
  229. package/apps/web/src/routes/config/services.tsx +49 -0
  230. package/apps/web/src/routes/config/tokens.tsx +192 -0
  231. package/apps/web/src/routes/groups/$id/index.tsx +153 -0
  232. package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
  233. package/apps/web/src/routes/groups/index.tsx +191 -0
  234. package/apps/web/src/routes/index.tsx +133 -0
  235. package/apps/web/src/routes/login.tsx +63 -0
  236. package/apps/web/src/routes/profiles/$id.tsx +549 -0
  237. package/apps/web/src/routes/profiles/index.tsx +458 -0
  238. package/apps/web/src/routes/skills/index.tsx +297 -0
  239. package/apps/web/src/routes/welcome.tsx +61 -0
  240. package/apps/web/src/styles.css +449 -0
  241. package/apps/web/tsconfig.json +25 -0
  242. package/apps/web/vite.config.ts +25 -0
  243. package/biome.json +23 -0
  244. package/docs/agent-engine.md +219 -0
  245. package/docs/architecture.md +627 -0
  246. package/docs/backlog/README.md +42 -0
  247. package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
  248. package/docs/openclaw-reference.md +210 -0
  249. package/package.json +38 -0
  250. package/packages/api-types/package.json +11 -0
  251. package/packages/api-types/src/entities.ts +146 -0
  252. package/packages/api-types/src/events.ts +55 -0
  253. package/packages/api-types/src/index.ts +488 -0
  254. package/packages/api-types/src/memory.ts +15 -0
  255. package/packages/client/package.json +13 -0
  256. package/packages/client/src/index.ts +117 -0
  257. package/pnpm-workspace.yaml +3 -0
  258. package/tsconfig.base.json +23 -0
  259. package/tsconfig.json +11 -0
  260. package/vitest.config.ts +22 -0
@@ -0,0 +1,193 @@
1
+ // /api/auth/openai/* — ChatGPT OAuth provider connection state.
2
+ // /api/providers/test — model smoke-test.
3
+ // /api/login — token-based browser login (sets the bz_token cookie).
4
+
5
+ import { spawn } from 'node:child_process'
6
+ import type { ProviderTestRequest, ProviderTestResponse } from '@bazilion/api-types'
7
+ import { Hono } from 'hono'
8
+ import { setCookie } from 'hono/cookie'
9
+ import { isSetupComplete, mergeSecretsIntoEnv, providerStateRepo } from '../core/index.ts'
10
+ import { isValidToken } from '../lib/auth.ts'
11
+ import { getCtx } from '../lib/ctx.ts'
12
+ import {
13
+ clearOpenAICodexCredentials,
14
+ createProviderRegistry,
15
+ getOpenAICodexStatus,
16
+ loadProviderConfigFromEnv,
17
+ loginOpenAICodex,
18
+ saveOpenAICodexLoginCredentials,
19
+ } from '../runtime/index.ts'
20
+
21
+ export const authRouter = new Hono()
22
+
23
+ // ─── Auth probe ──────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Cheap session validator. Web SSR middleware calls this once per request
27
+ * to translate "is the cookie still good?" + "has setup been finished?"
28
+ * into JSON-status pairs that the Astro middleware turns into redirects.
29
+ *
30
+ * Reaching this handler at all means auth passed (the daemon's own
31
+ * middleware-auth would have 401'd otherwise). The body just exposes the
32
+ * setup-complete bit so the web layer can route accordingly.
33
+ */
34
+ authRouter.get('/auth/me', (c) => {
35
+ const { db } = getCtx()
36
+ return c.json({ authed: true, setupComplete: isSetupComplete(db) })
37
+ })
38
+
39
+ // ─── ChatGPT OAuth ───────────────────────────────────────────────────────
40
+
41
+ authRouter.get('/auth/openai', (c) => {
42
+ const { db, authToken } = getCtx()
43
+ return c.json(getOpenAICodexStatus(db, authToken))
44
+ })
45
+
46
+ authRouter.put('/auth/openai', async (c) => {
47
+ const body = (await c.req.json().catch(() => null)) as {
48
+ refresh?: unknown
49
+ access?: unknown
50
+ expires?: unknown
51
+ } | null
52
+ if (
53
+ !body ||
54
+ typeof body.refresh !== 'string' ||
55
+ typeof body.access !== 'string' ||
56
+ typeof body.expires !== 'number'
57
+ ) {
58
+ return c.json(
59
+ { error: 'body must be { refresh: string, access: string, expires: number }' },
60
+ 400,
61
+ )
62
+ }
63
+ const { db, authToken } = getCtx()
64
+ saveOpenAICodexLoginCredentials(db, authToken, {
65
+ refresh: body.refresh,
66
+ access: body.access,
67
+ expires: body.expires,
68
+ })
69
+ return c.json(getOpenAICodexStatus(db, authToken))
70
+ })
71
+
72
+ authRouter.delete('/auth/openai', (c) => {
73
+ const { db, authToken } = getCtx()
74
+ clearOpenAICodexCredentials(db, authToken)
75
+ return c.json({ connected: false, expiresAt: null, accountId: null })
76
+ })
77
+
78
+ authRouter.post('/auth/openai/login', async (c) => {
79
+ const { db, authToken } = getCtx()
80
+ try {
81
+ const creds = await loginOpenAICodex({
82
+ onAuth: ({ url }) => openBrowser(url),
83
+ onPrompt: () =>
84
+ Promise.reject(
85
+ new Error(
86
+ 'interactive paste not supported in the web flow — cancel and try again, or use `bazilion auth openai login`',
87
+ ),
88
+ ),
89
+ onProgress: () => {
90
+ // single blocking POST keeps the UI simple
91
+ },
92
+ })
93
+ saveOpenAICodexLoginCredentials(db, authToken, creds)
94
+ return c.json(getOpenAICodexStatus(db, authToken))
95
+ } catch (err) {
96
+ return c.json({ error: (err as Error).message }, 400)
97
+ }
98
+ })
99
+
100
+ // ─── Provider model smoke-test ───────────────────────────────────────────
101
+
102
+ authRouter.post('/providers/test', async (c) => {
103
+ const body = (await c.req.json().catch(() => null)) as ProviderTestRequest | null
104
+ if (!body || typeof body.model !== 'string' || !body.model) {
105
+ return c.json({ error: 'model is required' }, 400)
106
+ }
107
+ const message = typeof body.message === 'string' && body.message ? body.message : 'say hi briefly'
108
+ const { db, paths, authToken } = getCtx()
109
+ const env = mergeSecretsIntoEnv(db, authToken)
110
+ const reg = createProviderRegistry(loadProviderConfigFromEnv(env, { db, authToken }), {
111
+ enabledSet: providerStateRepo.listEnabled(db),
112
+ })
113
+ try {
114
+ const { provider, model } = reg.resolve(body.model)
115
+ const res = await provider.chat({
116
+ model,
117
+ messages: [{ role: 'user', content: message }],
118
+ maxTokens: 256,
119
+ })
120
+ const out: ProviderTestResponse = { content: res.content, usage: res.usage }
121
+ return c.json(out)
122
+ } catch (err) {
123
+ return c.json({ error: (err as Error).message }, 400)
124
+ }
125
+ })
126
+
127
+ // ─── Browser login ───────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Token-based login. Two body shapes accepted:
131
+ * - `application/json`: `{ token: "<value>" }` → returns `{ ok: true }` on
132
+ * success, sets the `bz_token` cookie. Used by clients (web pages, mobile)
133
+ * that prefer JSON.
134
+ * - `application/x-www-form-urlencoded`: `token=<value>` → 302 to `/` on
135
+ * success or `/login?error=1` on failure. Used by the legacy `<form>` on
136
+ * the Astro `/login` page; preserved for compatibility through Stage A.
137
+ */
138
+ authRouter.post('/login', async (c) => {
139
+ const ct = c.req.header('content-type') ?? ''
140
+ let token: string | null = null
141
+
142
+ if (ct.startsWith('application/json')) {
143
+ const body = (await c.req.json().catch(() => null)) as { token?: unknown } | null
144
+ if (body && typeof body.token === 'string') token = body.token
145
+ } else {
146
+ const form = await c.req.formData().catch(() => null)
147
+ const v = form?.get('token')
148
+ if (typeof v === 'string') token = v
149
+ }
150
+
151
+ if (!token || !isValidToken(token)) {
152
+ if (ct.startsWith('application/json')) {
153
+ return c.json({ error: 'invalid token' }, 401)
154
+ }
155
+ return c.redirect('/login?error=1', 302)
156
+ }
157
+
158
+ setCookie(c, 'bz_token', token, {
159
+ path: '/',
160
+ httpOnly: true,
161
+ sameSite: 'Lax',
162
+ maxAge: 60 * 60 * 24 * 30,
163
+ })
164
+
165
+ if (ct.startsWith('application/json')) {
166
+ return c.json({ ok: true })
167
+ }
168
+ return c.redirect('/', 302)
169
+ })
170
+
171
+ /**
172
+ * Open `url` in the host's default browser. Only called by the OAuth flow
173
+ * triggered from /config — the user is on the same machine as the daemon in
174
+ * that scenario (loopback-only `bazilion serve`).
175
+ */
176
+ function openBrowser(url: string): void {
177
+ const platform = process.platform
178
+ const [cmd, ...args] =
179
+ platform === 'darwin'
180
+ ? ['open', url]
181
+ : platform === 'win32'
182
+ ? ['cmd', '/c', 'start', '""', url]
183
+ : ['xdg-open', url]
184
+ try {
185
+ const child = spawn(cmd as string, args, { stdio: 'ignore', detached: true })
186
+ child.unref()
187
+ child.on('error', () => {
188
+ // xdg-open missing on minimal installs — swallow so the flow still works
189
+ })
190
+ } catch {
191
+ // spawn failed — user will still see the URL in the progress log
192
+ }
193
+ }
@@ -0,0 +1,267 @@
1
+ // /api/config/* — provider matrix, services, per-provider enabled toggle and
2
+ // curated models, and per-field config/secret writes.
3
+
4
+ import type {
5
+ ProviderConfigEntry,
6
+ ProviderConfigResponse,
7
+ ServiceCard,
8
+ ServiceConfigResponse,
9
+ ServiceFieldState,
10
+ SetProviderModelsRequest,
11
+ } from '@bazilion/api-types'
12
+ import { Hono } from 'hono'
13
+ import {
14
+ ensureSetupSeeded,
15
+ findFieldByEnvVar,
16
+ groupAvailableModels,
17
+ mergeSecretsIntoEnv,
18
+ openConfig,
19
+ openSecrets,
20
+ providerModelRepo,
21
+ providerStateRepo,
22
+ type ServiceDef,
23
+ servicesByCategory,
24
+ } from '../core/index.ts'
25
+ import { getCtx } from '../lib/ctx.ts'
26
+ import {
27
+ listAllProviders,
28
+ listCatalogModels,
29
+ listCatalogModelsSync,
30
+ loadProviderConfigFromEnv,
31
+ } from '../runtime/index.ts'
32
+
33
+ export const configRouter = new Hono()
34
+
35
+ // /api/config/providers
36
+ configRouter.get('/providers', async (c) => {
37
+ const { db, paths, authToken } = getCtx()
38
+ const env = mergeSecretsIntoEnv(db, authToken)
39
+ const registryProviders = listAllProviders(loadProviderConfigFromEnv(env, { db, authToken }))
40
+ const registryByName = new Map(registryProviders.map((p) => [p.name, p]))
41
+
42
+ const configValues = readAll(() => openConfig(db).getAll())
43
+ const secretValues = readAll(() => openSecrets(db, authToken).getAll())
44
+
45
+ const providerServices = servicesByCategory('provider')
46
+ const enabledSet = providerStateRepo.listEnabled(db)
47
+
48
+ const entries = await Promise.all(
49
+ providerServices.map(async (svc): Promise<ProviderConfigEntry> => {
50
+ const meta = registryByName.get(svc.id)
51
+ const enabled = enabledSet.has(svc.id)
52
+ const envHint = meta?.envHint ?? ''
53
+ const ac = new AbortController()
54
+ const t = setTimeout(() => ac.abort(), 5_000)
55
+ try {
56
+ const { catalog, live } = enabled
57
+ ? await listCatalogModels(svc.id, env, ac.signal)
58
+ : { catalog: listCatalogModelsSync(svc.id), live: undefined }
59
+ return {
60
+ id: svc.id,
61
+ displayName: svc.displayName,
62
+ ...(svc.hint ? { hint: svc.hint } : {}),
63
+ enabled,
64
+ envHint,
65
+ fields: resolveFieldStates(svc, configValues, secretValues),
66
+ catalog,
67
+ ...(live ? { live } : {}),
68
+ curated: providerModelRepo.list(db, svc.id),
69
+ }
70
+ } finally {
71
+ clearTimeout(t)
72
+ }
73
+ }),
74
+ )
75
+
76
+ const body: ProviderConfigResponse = { providers: entries }
77
+ return c.json(body)
78
+ })
79
+
80
+ // /api/config/services — non-provider service cards (e.g. SearXNG, Brave).
81
+ configRouter.get('/services', (c) => {
82
+ const { db, authToken } = getCtx()
83
+ const configValues = readAll(() => openConfig(db).getAll())
84
+ const secretValues = readAll(() => openSecrets(db, authToken).getAll())
85
+
86
+ const services: ServiceCard[] = servicesByCategory('service').map((svc) => ({
87
+ id: svc.id,
88
+ displayName: svc.displayName,
89
+ ...(svc.hint ? { hint: svc.hint } : {}),
90
+ fields: resolveFieldStates(svc, configValues, secretValues),
91
+ }))
92
+
93
+ const body: ServiceConfigResponse = { services }
94
+ return c.json(body)
95
+ })
96
+
97
+ // /api/config/providers/:name/enabled — flip the admin switch.
98
+ configRouter.put('/providers/:name/enabled', async (c) => {
99
+ const name = c.req.param('name')
100
+ if (!knownProviderIds().has(name)) return c.json({ error: `unknown provider: ${name}` }, 404)
101
+
102
+ const body = (await c.req.json().catch(() => null)) as { enabled?: unknown } | null
103
+ if (!body || (typeof body.enabled !== 'boolean' && typeof body.enabled !== 'string')) {
104
+ return c.json({ error: 'body must be {"enabled": boolean}' }, 400)
105
+ }
106
+ const enabled =
107
+ typeof body.enabled === 'boolean' ? body.enabled : body.enabled.toLowerCase() === 'true'
108
+
109
+ const { db, paths } = getCtx()
110
+ providerStateRepo.setEnabled(db, name, enabled)
111
+ ensureSetupSeeded(db, paths)
112
+ return c.json({ name, enabled })
113
+ })
114
+
115
+ // /api/config/providers/:name/models — curated model list.
116
+ configRouter.get('/providers/:name/models', (c) => {
117
+ const name = c.req.param('name')
118
+ if (!knownProviderRegistryNames().has(name))
119
+ return c.json({ error: `unknown provider: ${name}` }, 404)
120
+ const { db } = getCtx()
121
+ return c.json({ models: providerModelRepo.list(db, name) })
122
+ })
123
+
124
+ configRouter.put('/providers/:name/models', async (c) => {
125
+ const name = c.req.param('name')
126
+ if (!knownProviderRegistryNames().has(name))
127
+ return c.json({ error: `unknown provider: ${name}` }, 404)
128
+ const body = (await c.req.json().catch(() => null)) as Partial<SetProviderModelsRequest> | null
129
+ if (!body) return c.json({ error: 'invalid JSON body' }, 400)
130
+
131
+ // Accept textarea-style newline-separated input from web forms in addition
132
+ // to the CLI's array shape.
133
+ let models: string[] = []
134
+ if (Array.isArray(body.models)) {
135
+ models = body.models.filter((m): m is string => typeof m === 'string')
136
+ } else if (typeof (body as Record<string, unknown>).models === 'string') {
137
+ models = ((body as Record<string, unknown>).models as string).split(/\r?\n/)
138
+ } else {
139
+ return c.json(
140
+ { error: 'models must be an array of strings or a newline-separated string' },
141
+ 400,
142
+ )
143
+ }
144
+
145
+ const { db, paths } = getCtx()
146
+ providerModelRepo.replace(db, name, models)
147
+ ensureSetupSeeded(db, paths)
148
+ return c.json({ models: providerModelRepo.list(db, name) })
149
+ })
150
+
151
+ // /api/config/fields/:envVar — write-through for any envVar the registry knows.
152
+ configRouter.put('/fields/:envVar', async (c) => {
153
+ const envVar = c.req.param('envVar')
154
+ const found = findFieldByEnvVar(envVar)
155
+ if (!found) return c.json({ error: `unknown envVar: ${envVar}` }, 404)
156
+
157
+ const body = (await c.req.json().catch(() => null)) as { value?: unknown } | null
158
+ if (!body || typeof body.value !== 'string') {
159
+ return c.json({ error: 'body must be {"value": "<string>"}' }, 400)
160
+ }
161
+
162
+ const { db, authToken } = getCtx()
163
+ if (found.field.kind === 'config') {
164
+ const store = openConfig(db)
165
+ if (body.value === '') store.remove(envVar)
166
+ else store.set(envVar, body.value)
167
+ } else {
168
+ const store = openSecrets(db, authToken)
169
+ if (body.value === '') store.remove(envVar)
170
+ else store.set(envVar, body.value)
171
+ }
172
+
173
+ return c.json(readFieldState(db, authToken, envVar, found.field.kind))
174
+ })
175
+
176
+ // /api/config/available-models — provider-grouped curated models. Drives
177
+ // the model dropdowns on the profile + agent spawn pages.
178
+ configRouter.get('/available-models', (c) => {
179
+ const { db } = getCtx()
180
+ return c.json({ groups: groupAvailableModels(db) })
181
+ })
182
+
183
+ configRouter.delete('/fields/:envVar', (c) => {
184
+ const envVar = c.req.param('envVar')
185
+ const found = findFieldByEnvVar(envVar)
186
+ if (!found) return c.json({ error: `unknown envVar: ${envVar}` }, 404)
187
+
188
+ const { db, authToken } = getCtx()
189
+ if (found.field.kind === 'config') {
190
+ openConfig(db).remove(envVar)
191
+ } else {
192
+ openSecrets(db, authToken).remove(envVar)
193
+ }
194
+ return c.body(null, 204)
195
+ })
196
+
197
+ // ─── helpers ─────────────────────────────────────────────────────────────
198
+
199
+ function mask(value: string): string {
200
+ if (value.length === 0) return ''
201
+ return value.length > 8 ? `${value.slice(0, 6)}…` : '***'
202
+ }
203
+
204
+ function resolveFieldStates(
205
+ service: ServiceDef,
206
+ configValues: Record<string, string>,
207
+ secretValues: Record<string, string>,
208
+ ): ServiceFieldState[] {
209
+ return service.fields.map((f) => {
210
+ const val = (f.kind === 'config' ? configValues[f.envVar] : secretValues[f.envVar]) ?? ''
211
+ const state: ServiceFieldState = {
212
+ envVar: f.envVar,
213
+ kind: f.kind,
214
+ label: f.label,
215
+ set: val.length > 0,
216
+ ...(f.placeholder ? { placeholder: f.placeholder } : {}),
217
+ ...(f.description ? { description: f.description } : {}),
218
+ }
219
+ if (f.kind === 'config') {
220
+ state.value = val
221
+ } else if (val.length > 0) {
222
+ state.preview = mask(val)
223
+ }
224
+ return state
225
+ })
226
+ }
227
+
228
+ function readAll(read: () => Record<string, string>): Record<string, string> {
229
+ try {
230
+ return read()
231
+ } catch {
232
+ return {}
233
+ }
234
+ }
235
+
236
+ interface FieldState {
237
+ envVar: string
238
+ kind: 'secret' | 'config'
239
+ set: boolean
240
+ value?: string
241
+ preview?: string
242
+ }
243
+
244
+ function readFieldState(
245
+ db: import('../core/index.ts').BazilionDb,
246
+ authToken: string,
247
+ envVar: string,
248
+ kind: 'secret' | 'config',
249
+ ): FieldState {
250
+ if (kind === 'config') {
251
+ const v = openConfig(db).get(envVar) ?? ''
252
+ return { envVar, kind, set: v.length > 0, value: v }
253
+ }
254
+ const v = openSecrets(db, authToken).get(envVar) ?? ''
255
+ return { envVar, kind, set: v.length > 0, ...(v.length > 0 ? { preview: mask(v) } : {}) }
256
+ }
257
+
258
+ function knownProviderIds(): Set<string> {
259
+ return new Set(servicesByCategory('provider').map((s) => s.id))
260
+ }
261
+
262
+ function knownProviderRegistryNames(): Set<string> {
263
+ const { db, authToken } = getCtx()
264
+ return new Set(
265
+ listAllProviders(loadProviderConfigFromEnv(process.env, { db, authToken })).map((p) => p.name),
266
+ )
267
+ }
@@ -0,0 +1,133 @@
1
+ // /api/groups/* — group registry, per-group USER.md, per-group shared
2
+ // memory. Memory is keyed by the group slug because the qmd index lives at
3
+ // `<group.path>/memory/` and is shared by every agent in the group.
4
+
5
+ import { join } from 'node:path'
6
+ import type { RegisterGroupRequest, SetGroupUserMdRequest } from '@bazilion/api-types'
7
+ import { Hono } from 'hono'
8
+ import { deleteGroup, groupRepo, registerGroup } from '../core/index.ts'
9
+ import { getCtx } from '../lib/ctx.ts'
10
+ import { qmdBackend } from '../runtime/index.ts'
11
+
12
+ // 12 KB cap matches OpenClaw's bootstrapMaxChars default — enough for a rich
13
+ // USER.md, small enough that it can't silently blow out the system prompt.
14
+ const USER_MD_MAX_BYTES = 12_000
15
+
16
+ export const groupsRouter = new Hono()
17
+
18
+ groupsRouter.get('/', (c) => {
19
+ const { db, paths } = getCtx()
20
+ return c.json(groupRepo.list(db, paths))
21
+ })
22
+
23
+ groupsRouter.post('/', async (c) => {
24
+ const body = (await c.req.json().catch(() => null)) as RegisterGroupRequest | null
25
+ if (!body || typeof body.id !== 'string') {
26
+ return c.json({ error: 'id is required' }, 400)
27
+ }
28
+ const { db, paths } = getCtx()
29
+ try {
30
+ const g = registerGroup(db, { id: body.id, name: body.name, link: body.link }, paths)
31
+ return c.json(g, 201)
32
+ } catch (err) {
33
+ return c.json({ error: (err as Error).message }, 400)
34
+ }
35
+ })
36
+
37
+ groupsRouter.get('/:id', (c) => {
38
+ const { db, paths } = getCtx()
39
+ const g = groupRepo.get(db, c.req.param('id'), paths)
40
+ if (!g) return c.json({ error: `group not found: ${c.req.param('id')}` }, 404)
41
+ return c.json(g)
42
+ })
43
+
44
+ groupsRouter.delete('/:id', (c) => {
45
+ const { db, paths } = getCtx()
46
+ try {
47
+ deleteGroup(db, paths, c.req.param('id'))
48
+ return c.body(null, 204)
49
+ } catch (err) {
50
+ return c.json({ error: (err as Error).message }, 400)
51
+ }
52
+ })
53
+
54
+ groupsRouter.put('/:id/user-md', async (c) => {
55
+ const body = (await c.req.json().catch(() => null)) as SetGroupUserMdRequest | null
56
+ if (!body || typeof body.userMd !== 'string') {
57
+ return c.json({ error: 'userMd (string) is required' }, 400)
58
+ }
59
+ if (Buffer.byteLength(body.userMd, 'utf8') > USER_MD_MAX_BYTES) {
60
+ return c.json({ error: `userMd exceeds ${USER_MD_MAX_BYTES}-byte cap` }, 413)
61
+ }
62
+ const { db, paths } = getCtx()
63
+ const g = groupRepo.get(db, c.req.param('id'), paths)
64
+ if (!g) return c.json({ error: `group not found: ${c.req.param('id')}` }, 404)
65
+ groupRepo.setUserMd(db, c.req.param('id'), body.userMd)
66
+ return c.json(groupRepo.get(db, c.req.param('id'), paths))
67
+ })
68
+
69
+ // ─── Memory (per-group, shared across all member agents) ──────────────────
70
+
71
+ async function openMemory(rawId: string) {
72
+ const { db, paths } = getCtx()
73
+ const group = groupRepo.get(db, rawId, paths)
74
+ if (!group) throw new Error(`group not found: ${rawId}`)
75
+ const mem = qmdBackend(join(group.path, 'memory'))
76
+ await mem.init()
77
+ return { mem, group }
78
+ }
79
+
80
+ groupsRouter.get('/:id/memory', async (c) => {
81
+ try {
82
+ const { mem } = await openMemory(c.req.param('id'))
83
+ return c.json(await mem.list())
84
+ } catch (err) {
85
+ return c.json({ error: (err as Error).message }, 404)
86
+ }
87
+ })
88
+
89
+ groupsRouter.get('/:id/memory/search', async (c) => {
90
+ const q = c.req.query('q')
91
+ if (!q) return c.json({ error: 'q is required' }, 400)
92
+ const limit = Number.parseInt(c.req.query('limit') ?? '10', 10)
93
+ try {
94
+ const { mem } = await openMemory(c.req.param('id'))
95
+ return c.json(await mem.search(q, { limit }))
96
+ } catch (err) {
97
+ return c.json({ error: (err as Error).message }, 404)
98
+ }
99
+ })
100
+
101
+ // `:key{.+}` matches multi-segment paths so memory keys with slashes (e.g.
102
+ // `notes/2026-04-25.md`) survive the routing layer. Without the regex Hono
103
+ // would only capture a single segment.
104
+ groupsRouter.get('/:id/memory/:key{.+}', async (c) => {
105
+ try {
106
+ const { mem } = await openMemory(c.req.param('id'))
107
+ return c.json(await mem.read(c.req.param('key')))
108
+ } catch (err) {
109
+ return c.json({ error: (err as Error).message }, 404)
110
+ }
111
+ })
112
+
113
+ groupsRouter.put('/:id/memory/:key{.+}', async (c) => {
114
+ const body = (await c.req.json().catch(() => null)) as { content?: string } | null
115
+ if (!body || typeof body.content !== 'string')
116
+ return c.json({ error: 'content is required' }, 400)
117
+ try {
118
+ const { mem } = await openMemory(c.req.param('id'))
119
+ return c.json(await mem.write(c.req.param('key'), body.content))
120
+ } catch (err) {
121
+ return c.json({ error: (err as Error).message }, 500)
122
+ }
123
+ })
124
+
125
+ groupsRouter.delete('/:id/memory/:key{.+}', async (c) => {
126
+ try {
127
+ const { mem } = await openMemory(c.req.param('id'))
128
+ await mem.remove(c.req.param('key'))
129
+ return c.body(null, 204)
130
+ } catch (err) {
131
+ return c.json({ error: (err as Error).message }, 500)
132
+ }
133
+ })
@@ -0,0 +1,29 @@
1
+ // /api/messages/:id — fetch + mark-read for a single message. (Inbox listing
2
+ // + send is per-agent at /api/agents/:id/messages.)
3
+
4
+ import type { UpdateMessageRequest } from '@bazilion/api-types'
5
+ import { Hono } from 'hono'
6
+ import { messageRepo } from '../core/index.ts'
7
+ import { getCtx } from '../lib/ctx.ts'
8
+
9
+ export const messagesRouter = new Hono()
10
+
11
+ messagesRouter.get('/:id', (c) => {
12
+ const { db } = getCtx()
13
+ const msg = messageRepo.get(db, c.req.param('id'))
14
+ if (!msg) return c.json({ error: `message not found: ${c.req.param('id')}` }, 404)
15
+ return c.json(msg)
16
+ })
17
+
18
+ messagesRouter.patch('/:id', async (c) => {
19
+ const id = c.req.param('id')
20
+ const body = (await c.req.json().catch(() => null)) as UpdateMessageRequest | null
21
+ if (!body || body.read !== true) {
22
+ return c.json({ error: 'body must be {read: true}' }, 400)
23
+ }
24
+ const { db } = getCtx()
25
+ const existing = messageRepo.get(db, id)
26
+ if (!existing) return c.json({ error: `message not found: ${id}` }, 404)
27
+ messageRepo.markRead(db, id)
28
+ return c.json(messageRepo.get(db, id))
29
+ })