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,153 @@
1
+ import { ApiClientError } from '@bazilion/client'
2
+ import type { Agent, Group } from '@bazilion/api-types'
3
+ import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
4
+ import { createServerFn } from '@tanstack/react-start'
5
+ import { useState } from 'react'
6
+ import { daemonClient } from '../../../lib/daemon-client'
7
+
8
+ interface GroupDetail {
9
+ group: Group
10
+ members: Agent[]
11
+ }
12
+
13
+ const fetchGroup = createServerFn({ method: 'POST' })
14
+ .inputValidator((d: { id: string }) => d)
15
+ .handler(async ({ data }): Promise<GroupDetail | null> => {
16
+ const c = daemonClient()
17
+ let group: Group
18
+ try {
19
+ group = await c.get<Group>(`/api/groups/${encodeURIComponent(data.id)}`)
20
+ } catch (err) {
21
+ if (err instanceof ApiClientError && err.status === 404) return null
22
+ throw err
23
+ }
24
+ const all = await c.get<Agent[]>('/api/agents?includeArchived=true')
25
+ const members = all.filter((a) => a.groupId === group.id)
26
+ return { group, members }
27
+ })
28
+
29
+ export const Route = createFileRoute('/groups/$id/')({
30
+ loader: async ({ params }) => {
31
+ const data = await fetchGroup({ data: { id: params.id } })
32
+ if (!data) throw redirect({ to: '/groups' })
33
+ return data
34
+ },
35
+ component: GroupDetailPage,
36
+ })
37
+
38
+ function GroupDetailPage() {
39
+ const { group, members } = Route.useLoaderData()
40
+ const router = useRouter()
41
+ const [userMd, setUserMd] = useState(group.userMd)
42
+ const [busy, setBusy] = useState(false)
43
+ const [err, setErr] = useState<string | null>(null)
44
+ const [savedAt, setSavedAt] = useState<number | null>(null)
45
+
46
+ async function save() {
47
+ setBusy(true)
48
+ setErr(null)
49
+ try {
50
+ const res = await fetch(`/api/groups/${encodeURIComponent(group.id)}/user-md`, {
51
+ method: 'PUT',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ userMd }),
54
+ })
55
+ if (!res.ok) {
56
+ const e = (await res.json().catch(() => ({}))) as { error?: string }
57
+ throw new Error(e.error ?? `${res.status} ${res.statusText}`)
58
+ }
59
+ setSavedAt(Date.now())
60
+ await router.invalidate()
61
+ } catch (e) {
62
+ setErr((e as Error).message)
63
+ } finally {
64
+ setBusy(false)
65
+ }
66
+ }
67
+
68
+ return (
69
+ <main className="mx-auto max-w-3xl px-6 py-8">
70
+ <h1 className="font-serif text-3xl text-foreground">
71
+ {group.name} <span className="text-muted-foreground text-base">({group.id})</span>
72
+ </h1>
73
+ <p className="text-muted-foreground text-sm mb-6 mt-1">
74
+ <code className="font-mono">{group.path}</code> · {members.length}{' '}
75
+ member{members.length === 1 ? '' : 's'} ·{' '}
76
+ <a
77
+ href={`/groups/${encodeURIComponent(group.id)}/memory`}
78
+ className="text-primary underline"
79
+ >
80
+ shared memory →
81
+ </a>
82
+ </p>
83
+
84
+ <section className="rounded-lg border bg-card p-5 mb-6">
85
+ <h3 className="font-serif text-xl mb-1">USER.md</h3>
86
+ <p className="text-muted-foreground text-sm mb-3">
87
+ Read-only context about the human, injected into every member agent's system prompt.
88
+ 12 KB cap. Agents cannot edit this file — only you can.
89
+ </p>
90
+ <textarea
91
+ value={userMd}
92
+ onChange={(e) => setUserMd(e.target.value)}
93
+ rows={16}
94
+ className="w-full rounded-md border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-ring/30"
95
+ placeholder="What the agent should know about you in this group context…"
96
+ />
97
+ <div className="flex items-center gap-3 mt-3">
98
+ <button
99
+ type="button"
100
+ onClick={save}
101
+ disabled={busy || userMd === group.userMd}
102
+ className="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50"
103
+ >
104
+ {busy ? 'saving…' : 'save'}
105
+ </button>
106
+ <span className="text-xs text-muted-foreground">
107
+ {userMd.length} / 12000 chars
108
+ </span>
109
+ {savedAt && <span className="text-xs text-emerald-700">✓ saved</span>}
110
+ {err && <span className="text-xs text-rose-700">{err}</span>}
111
+ </div>
112
+ </section>
113
+
114
+ <section className="rounded-lg border bg-card p-5">
115
+ <h3 className="font-serif text-xl mb-3">members</h3>
116
+ {members.length === 0 ? (
117
+ <p className="text-muted-foreground text-sm">No agents in this group yet.</p>
118
+ ) : (
119
+ <table className="w-full text-sm">
120
+ <thead className="text-left text-muted-foreground border-b">
121
+ <tr>
122
+ <th className="py-2">name</th>
123
+ <th>status</th>
124
+ <th>profile</th>
125
+ <th />
126
+ </tr>
127
+ </thead>
128
+ <tbody>
129
+ {members.map((m) => (
130
+ <tr key={m.id} className="border-b last:border-0">
131
+ <td className="py-2">
132
+ <a href={`/agents/${m.id}`} className="text-primary underline">
133
+ {m.name}
134
+ </a>
135
+ </td>
136
+ <td>{m.status}</td>
137
+ <td>
138
+ <code className="font-mono text-xs">{m.profileId}</code>
139
+ </td>
140
+ <td>
141
+ <code className="font-mono text-xs text-muted-foreground">
142
+ {m.id.slice(0, 8)}…
143
+ </code>
144
+ </td>
145
+ </tr>
146
+ ))}
147
+ </tbody>
148
+ </table>
149
+ )}
150
+ </section>
151
+ </main>
152
+ )
153
+ }
@@ -0,0 +1,321 @@
1
+ // Per-group shared memory: BM25-indexed markdown notes that every member
2
+ // agent reads from and writes to. Browse the list, search, edit/create
3
+ // entries. The qmd index lives at <group.path>/memory/ on disk.
4
+
5
+ import { ApiClientError } from '@bazilion/client'
6
+ import type { Agent, Group, MemoryEntry, MemoryHit } from '@bazilion/api-types'
7
+ import { createFileRoute, redirect } from '@tanstack/react-router'
8
+ import { createServerFn } from '@tanstack/react-start'
9
+ import { useEffect, useRef, useState } from 'react'
10
+ import { daemonClient } from '../../../lib/daemon-client'
11
+
12
+ interface MemoryView {
13
+ group: Group
14
+ memberCount: number
15
+ entries: MemoryEntry[]
16
+ }
17
+
18
+ const fetchMemory = createServerFn({ method: 'POST' })
19
+ .inputValidator((d: { id: string }) => d)
20
+ .handler(async ({ data }): Promise<MemoryView | null> => {
21
+ const c = daemonClient()
22
+ let group: Group
23
+ try {
24
+ group = await c.get<Group>(`/api/groups/${encodeURIComponent(data.id)}`)
25
+ } catch (err) {
26
+ if (err instanceof ApiClientError && err.status === 404) return null
27
+ throw err
28
+ }
29
+ const [entries, agents] = await Promise.all([
30
+ c.get<MemoryEntry[]>(`/api/groups/${encodeURIComponent(group.id)}/memory`),
31
+ c.get<Agent[]>('/api/agents?includeArchived=true'),
32
+ ])
33
+ const memberCount = agents.filter((a) => a.groupId === group.id).length
34
+ return { group, memberCount, entries }
35
+ })
36
+
37
+ export const Route = createFileRoute('/groups/$id/memory')({
38
+ loader: async ({ params }) => {
39
+ const data = await fetchMemory({ data: { id: params.id } })
40
+ if (!data) throw redirect({ to: '/groups' })
41
+ return data
42
+ },
43
+ component: MemoryPage,
44
+ })
45
+
46
+ function encodeKey(key: string): string {
47
+ return key.split('/').map(encodeURIComponent).join('/')
48
+ }
49
+
50
+ interface ListRow {
51
+ key: string
52
+ preview: string
53
+ score?: number
54
+ }
55
+
56
+ type Mode = 'none' | 'edit' | 'new'
57
+
58
+ function MemoryPage() {
59
+ const { group, memberCount, entries: initialEntries } = Route.useLoaderData()
60
+ const groupId = group.id
61
+
62
+ const [rows, setRows] = useState<ListRow[]>(() =>
63
+ initialEntries.map((e) => ({ key: e.key, preview: e.content.slice(0, 80) })),
64
+ )
65
+ const [isSearch, setIsSearch] = useState(false)
66
+ const [query, setQuery] = useState('')
67
+ const [selectedKey, setSelectedKey] = useState<string | null>(null)
68
+ const [mode, setMode] = useState<Mode>('none')
69
+ const [keyInput, setKeyInput] = useState('')
70
+ const [content, setContent] = useState('')
71
+ const [status, setStatus] = useState<{ msg: string; kind: 'info' | 'error' } | null>(null)
72
+ const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
73
+
74
+ async function loadList(q: string) {
75
+ try {
76
+ const url =
77
+ q.trim().length > 0
78
+ ? `/api/groups/${encodeURIComponent(groupId)}/memory/search?q=${encodeURIComponent(q)}&limit=50`
79
+ : `/api/groups/${encodeURIComponent(groupId)}/memory`
80
+ const res = await fetch(url)
81
+ if (!res.ok) {
82
+ const body = (await res.json().catch(() => null)) as { error?: string } | null
83
+ throw new Error(body?.error ?? res.statusText)
84
+ }
85
+ const data = (await res.json()) as MemoryEntry[] | MemoryHit[]
86
+ if (q.trim().length > 0) {
87
+ setRows(
88
+ (data as MemoryHit[]).map((h) => ({
89
+ key: h.key,
90
+ preview: h.snippet,
91
+ score: h.score,
92
+ })),
93
+ )
94
+ setIsSearch(true)
95
+ } else {
96
+ setRows(
97
+ (data as MemoryEntry[]).map((e) => ({
98
+ key: e.key,
99
+ preview: e.content.slice(0, 80),
100
+ })),
101
+ )
102
+ setIsSearch(false)
103
+ }
104
+ } catch (err) {
105
+ setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
106
+ }
107
+ }
108
+
109
+ useEffect(() => {
110
+ if (searchTimer.current) clearTimeout(searchTimer.current)
111
+ searchTimer.current = setTimeout(() => {
112
+ void loadList(query)
113
+ }, 200)
114
+ return () => {
115
+ if (searchTimer.current) clearTimeout(searchTimer.current)
116
+ }
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [query])
119
+
120
+ async function openEntry(key: string) {
121
+ setSelectedKey(key)
122
+ setMode('edit')
123
+ setStatus({ msg: 'loading…', kind: 'info' })
124
+ try {
125
+ const res = await fetch(
126
+ `/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(key)}`,
127
+ )
128
+ if (!res.ok) {
129
+ const body = (await res.json().catch(() => null)) as { error?: string } | null
130
+ throw new Error(body?.error ?? res.statusText)
131
+ }
132
+ const entry = (await res.json()) as MemoryEntry
133
+ setKeyInput(entry.key)
134
+ setContent(entry.content ?? '')
135
+ setStatus({ msg: `loaded ${entry.key}`, kind: 'info' })
136
+ } catch (err) {
137
+ setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
138
+ }
139
+ }
140
+
141
+ function newEntry() {
142
+ setSelectedKey(null)
143
+ setMode('new')
144
+ setKeyInput('')
145
+ setContent('')
146
+ setStatus({ msg: 'new entry — type a key and content, then save', kind: 'info' })
147
+ }
148
+
149
+ async function save() {
150
+ const key = keyInput.trim()
151
+ if (!key) {
152
+ setStatus({ msg: 'key is required', kind: 'error' })
153
+ return
154
+ }
155
+ try {
156
+ const res = await fetch(
157
+ `/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(key)}`,
158
+ {
159
+ method: 'PUT',
160
+ headers: { 'content-type': 'application/json' },
161
+ body: JSON.stringify({ content }),
162
+ },
163
+ )
164
+ if (!res.ok) {
165
+ const body = (await res.json().catch(() => null)) as { error?: string } | null
166
+ throw new Error(body?.error ?? res.statusText)
167
+ }
168
+ setSelectedKey(key)
169
+ setMode('edit')
170
+ setStatus({ msg: 'saved', kind: 'info' })
171
+ await loadList(query)
172
+ } catch (err) {
173
+ setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
174
+ }
175
+ }
176
+
177
+ async function del() {
178
+ if (!selectedKey) return
179
+ if (!confirm(`delete ${selectedKey}?`)) return
180
+ try {
181
+ const res = await fetch(
182
+ `/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(selectedKey)}`,
183
+ { method: 'DELETE' },
184
+ )
185
+ if (!res.ok && res.status !== 204) {
186
+ const body = (await res.json().catch(() => null)) as { error?: string } | null
187
+ throw new Error(body?.error ?? res.statusText)
188
+ }
189
+ setSelectedKey(null)
190
+ setMode('none')
191
+ setKeyInput('')
192
+ setContent('')
193
+ setStatus({ msg: 'deleted', kind: 'info' })
194
+ await loadList(query)
195
+ } catch (err) {
196
+ setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
197
+ }
198
+ }
199
+
200
+ const canSave = mode !== 'none' && keyInput.trim().length > 0
201
+ const canDelete = mode === 'edit' && selectedKey !== null
202
+
203
+ return (
204
+ <main className="mx-auto max-w-5xl px-6 py-8">
205
+ <header className="mb-6">
206
+ <h1 className="font-serif text-3xl text-foreground">
207
+ {group.name}{' '}
208
+ <span className="text-muted-foreground text-base">/ shared memory</span>
209
+ </h1>
210
+ <p className="text-muted-foreground text-sm mt-1">
211
+ BM25-indexed markdown notes shared by every agent in{' '}
212
+ <a
213
+ href={`/groups/${encodeURIComponent(group.id)}`}
214
+ className="font-mono underline"
215
+ >
216
+ {group.id}
217
+ </a>{' '}
218
+ ({memberCount} member{memberCount === 1 ? '' : 's'}). Anything written here is
219
+ visible to every member; per-agent notes belong in the agent's own{' '}
220
+ <code className="font-mono">IDENTITY.md</code> via <code>home_write</code>.
221
+ </p>
222
+ </header>
223
+
224
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]">
225
+ <div>
226
+ <div className="overflow-hidden rounded-[16px] border border-frost bg-snow">
227
+ <div className="border-b border-frost p-2">
228
+ <input
229
+ type="text"
230
+ placeholder="search (BM25)..."
231
+ autoComplete="off"
232
+ value={query}
233
+ onChange={(e) => setQuery(e.target.value)}
234
+ />
235
+ </div>
236
+ <div className="max-h-[520px] overflow-y-auto">
237
+ {rows.length === 0 ? (
238
+ <div className="p-8 text-center text-mocha-light">no entries</div>
239
+ ) : (
240
+ rows.map((r) => {
241
+ const active = r.key === selectedKey
242
+ return (
243
+ <button
244
+ key={r.key}
245
+ type="button"
246
+ onClick={() => openEntry(r.key)}
247
+ className={`unstyled block w-full cursor-pointer border-b border-frost/50 px-3.5 py-2 text-left font-mono text-[0.84em] transition-colors last:border-b-0 hover:bg-sapphire-glow ${
248
+ active ? 'bg-sapphire-glow font-medium text-sapphire-deep' : ''
249
+ }`}
250
+ >
251
+ {isSearch && r.score !== undefined && (
252
+ <span className="float-right text-[0.75em] text-sapphire">
253
+ {r.score.toFixed(2)}
254
+ </span>
255
+ )}
256
+ {r.key}
257
+ {r.preview && (
258
+ <span className="mt-1 block text-[0.9em] font-normal text-mocha-light">
259
+ {r.preview}
260
+ </span>
261
+ )}
262
+ </button>
263
+ )
264
+ })
265
+ )}
266
+ </div>
267
+ </div>
268
+ <button type="button" className="ghost-btn mt-3 w-full" onClick={newEntry}>
269
+ + new entry
270
+ </button>
271
+ </div>
272
+
273
+ <div>
274
+ <div className="mb-3 flex items-center gap-2">
275
+ <input
276
+ type="text"
277
+ placeholder="key (e.g. prefs/hiking.md)"
278
+ value={keyInput}
279
+ onChange={(e) => setKeyInput(e.target.value)}
280
+ disabled={mode !== 'new'}
281
+ className="flex-1 font-mono"
282
+ />
283
+ </div>
284
+ <textarea
285
+ placeholder="select an entry on the left, or create a new one."
286
+ value={content}
287
+ onChange={(e) => setContent(e.target.value)}
288
+ className="min-h-[360px] w-full font-mono text-[0.9em] leading-[1.55]"
289
+ />
290
+ <div className="mt-3 flex gap-2">
291
+ <button
292
+ type="button"
293
+ className="btn-primary"
294
+ onClick={save}
295
+ disabled={!canSave}
296
+ >
297
+ save
298
+ </button>
299
+ <button
300
+ type="button"
301
+ className="ghost-btn"
302
+ onClick={del}
303
+ disabled={!canDelete}
304
+ >
305
+ delete
306
+ </button>
307
+ </div>
308
+ {status && (
309
+ <p
310
+ className={`mt-2 text-[0.9em] ${
311
+ status.kind === 'error' ? 'text-[#9B3D3D]' : 'text-mocha-light'
312
+ }`}
313
+ >
314
+ {status.msg}
315
+ </p>
316
+ )}
317
+ </div>
318
+ </div>
319
+ </main>
320
+ )
321
+ }
@@ -0,0 +1,191 @@
1
+ import type { Agent, Group } from '@bazilion/api-types'
2
+ import { createFileRoute, useRouter } from '@tanstack/react-router'
3
+ import { createServerFn } from '@tanstack/react-start'
4
+ import { useState } from 'react'
5
+ import { daemonClient } from '../../lib/daemon-client'
6
+
7
+ interface GroupsData {
8
+ groups: Group[]
9
+ memberCounts: Record<string, number>
10
+ }
11
+
12
+ const fetchGroupsData = createServerFn({ method: 'GET' }).handler(
13
+ async (): Promise<GroupsData> => {
14
+ const c = daemonClient()
15
+ const [groups, agents] = await Promise.all([
16
+ c.get<Group[]>('/api/groups'),
17
+ c.get<Agent[]>('/api/agents?includeArchived=true'),
18
+ ])
19
+ const memberCounts: Record<string, number> = {}
20
+ for (const a of agents) memberCounts[a.groupId] = (memberCounts[a.groupId] ?? 0) + 1
21
+ return { groups, memberCounts }
22
+ },
23
+ )
24
+
25
+ export const Route = createFileRoute('/groups/')({
26
+ loader: () => fetchGroupsData(),
27
+ component: GroupsPage,
28
+ })
29
+
30
+ function GroupsPage() {
31
+ const { groups, memberCounts } = Route.useLoaderData()
32
+ const router = useRouter()
33
+
34
+ async function remove(id: string) {
35
+ if (!confirm('remove this group registration?')) return
36
+ const res = await fetch(`/api/groups/${id}`, { method: 'DELETE' })
37
+ if (!res.ok && res.status !== 204) {
38
+ alert(res.statusText)
39
+ return
40
+ }
41
+ await router.invalidate()
42
+ }
43
+
44
+ return (
45
+ <div>
46
+ <h1>groups</h1>
47
+ <p className="muted">
48
+ A group is a collaboration context: one filesystem root, one USER.md, one roster. Every
49
+ agent belongs to exactly one group.
50
+ </p>
51
+
52
+ <RegisterGroupForm onRegistered={() => router.invalidate()} />
53
+
54
+ <table>
55
+ <thead>
56
+ <tr>
57
+ <th>id</th>
58
+ <th>name</th>
59
+ <th>path</th>
60
+ <th>members</th>
61
+ <th>USER.md</th>
62
+ <th />
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ {groups.length === 0 && (
67
+ <tr>
68
+ <td colSpan={6} className="muted">
69
+ no groups registered
70
+ </td>
71
+ </tr>
72
+ )}
73
+ {groups.map((g) => {
74
+ const count = memberCounts[g.id] ?? 0
75
+ const userMdBytes = g.userMd.length
76
+ return (
77
+ <tr key={g.id}>
78
+ <td>
79
+ <code>{g.id}</code>
80
+ </td>
81
+ <td>{g.name}</td>
82
+ <td>
83
+ <code>{g.path}</code>
84
+ </td>
85
+ <td>{count}</td>
86
+ <td>
87
+ <a href={`/groups/${g.id}`}>
88
+ {userMdBytes > 0 ? `${userMdBytes} chars` : 'empty'}
89
+ </a>
90
+ </td>
91
+ <td>
92
+ {count === 0 ? (
93
+ <button type="button" className="ghost-btn" onClick={() => remove(g.id)}>
94
+ remove
95
+ </button>
96
+ ) : (
97
+ <span
98
+ className="muted"
99
+ title={`${count} agent(s) belong to this group`}
100
+ >
101
+ in use
102
+ </span>
103
+ )}
104
+ </td>
105
+ </tr>
106
+ )
107
+ })}
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ function RegisterGroupForm({ onRegistered }: { onRegistered: () => void }) {
115
+ const [id, setId] = useState('')
116
+ const [name, setName] = useState('')
117
+ const [link, setLink] = useState('')
118
+ const [err, setErr] = useState<string | null>(null)
119
+ const [submitting, setSubmitting] = useState(false)
120
+
121
+ async function submit(e: React.FormEvent<HTMLFormElement>) {
122
+ e.preventDefault()
123
+ setErr(null)
124
+ if (!id.trim()) {
125
+ setErr('id is required')
126
+ return
127
+ }
128
+ setSubmitting(true)
129
+ try {
130
+ const res = await fetch('/api/groups', {
131
+ method: 'POST',
132
+ headers: { 'content-type': 'application/json' },
133
+ body: JSON.stringify({
134
+ id: id.trim(),
135
+ name: name.trim() || undefined,
136
+ link: link.trim() || undefined,
137
+ }),
138
+ })
139
+ if (!res.ok) {
140
+ const e2 = (await res.json().catch(() => null)) as { error?: string } | null
141
+ throw new Error(e2?.error ?? res.statusText)
142
+ }
143
+ setId('')
144
+ setName('')
145
+ setLink('')
146
+ onRegistered()
147
+ } catch (e2) {
148
+ setErr((e2 as Error).message)
149
+ } finally {
150
+ setSubmitting(false)
151
+ }
152
+ }
153
+
154
+ return (
155
+ <form className="card" onSubmit={submit}>
156
+ <h3>register group</h3>
157
+ {err && <div className="err">{err}</div>}
158
+ <p className="muted">
159
+ Groups always live under <code>~/.bazilion/groups/&lt;slug&gt;/</code>. Leave the link
160
+ target blank to create a fresh directory; supply an absolute path to materialize the slot
161
+ as a symlink to your existing project tree instead.
162
+ </p>
163
+ <div className="flex gap-4">
164
+ <label className="flex-1">
165
+ id (slug)
166
+ <input
167
+ value={id}
168
+ onChange={(e) => setId(e.target.value)}
169
+ required
170
+ placeholder="myproject"
171
+ />
172
+ </label>
173
+ <label className="flex-1">
174
+ name
175
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="My Project" />
176
+ </label>
177
+ </div>
178
+ <label>
179
+ link target (optional, absolute path)
180
+ <input
181
+ value={link}
182
+ onChange={(e) => setLink(e.target.value)}
183
+ placeholder="/home/user/projects/myproject"
184
+ />
185
+ </label>
186
+ <button type="submit" disabled={submitting}>
187
+ {submitting ? 'registering…' : 'register'}
188
+ </button>
189
+ </form>
190
+ )
191
+ }