agent-mockingbird 0.0.1

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 (227) hide show
  1. package/.agents/skills/btca-cli/SKILL.md +64 -0
  2. package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
  5. package/.env.example +36 -0
  6. package/.githooks/pre-commit +33 -0
  7. package/.github/workflows/ci.yml +309 -0
  8. package/.opencode/bun.lock +18 -0
  9. package/.opencode/package.json +5 -0
  10. package/.opencode/tools/agent_type_manager.ts +100 -0
  11. package/.opencode/tools/config_manager.ts +87 -0
  12. package/.opencode/tools/cron_manager.ts +145 -0
  13. package/.opencode/tools/memory_get.ts +43 -0
  14. package/.opencode/tools/memory_remember.ts +53 -0
  15. package/.opencode/tools/memory_search.ts +48 -0
  16. package/AGENTS.md +126 -0
  17. package/MEMORY.md +2 -0
  18. package/README.md +451 -0
  19. package/THIRD_PARTY_NOTICES.md +11 -0
  20. package/agent-mockingbird.config.example.json +135 -0
  21. package/apps/server/package.json +32 -0
  22. package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
  23. package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
  24. package/apps/server/src/backend/agents/openclawImport.ts +797 -0
  25. package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
  26. package/apps/server/src/backend/agents/service.ts +10 -0
  27. package/apps/server/src/backend/config/example-config.test.ts +20 -0
  28. package/apps/server/src/backend/config/orchestration.ts +243 -0
  29. package/apps/server/src/backend/config/policy.ts +158 -0
  30. package/apps/server/src/backend/config/schema.test.ts +15 -0
  31. package/apps/server/src/backend/config/schema.ts +391 -0
  32. package/apps/server/src/backend/config/semantic.test.ts +34 -0
  33. package/apps/server/src/backend/config/semantic.ts +149 -0
  34. package/apps/server/src/backend/config/service.test.ts +75 -0
  35. package/apps/server/src/backend/config/service.ts +207 -0
  36. package/apps/server/src/backend/config/smoke.ts +77 -0
  37. package/apps/server/src/backend/config/store.test.ts +123 -0
  38. package/apps/server/src/backend/config/store.ts +581 -0
  39. package/apps/server/src/backend/config/testFixtures.ts +5 -0
  40. package/apps/server/src/backend/config/types.ts +56 -0
  41. package/apps/server/src/backend/contracts/events.ts +320 -0
  42. package/apps/server/src/backend/contracts/runtime.ts +111 -0
  43. package/apps/server/src/backend/cron/executor.ts +435 -0
  44. package/apps/server/src/backend/cron/repository.ts +170 -0
  45. package/apps/server/src/backend/cron/service.ts +660 -0
  46. package/apps/server/src/backend/cron/storage.ts +92 -0
  47. package/apps/server/src/backend/cron/types.ts +138 -0
  48. package/apps/server/src/backend/cron/utils.ts +351 -0
  49. package/apps/server/src/backend/db/client.ts +20 -0
  50. package/apps/server/src/backend/db/migrate.ts +40 -0
  51. package/apps/server/src/backend/db/repository.ts +1762 -0
  52. package/apps/server/src/backend/db/schema.ts +113 -0
  53. package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
  54. package/apps/server/src/backend/db/wipe.ts +13 -0
  55. package/apps/server/src/backend/defaults.ts +32 -0
  56. package/apps/server/src/backend/env.ts +48 -0
  57. package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
  58. package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
  59. package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
  60. package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
  61. package/apps/server/src/backend/heartbeat/service.ts +176 -0
  62. package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
  63. package/apps/server/src/backend/heartbeat/state.ts +167 -0
  64. package/apps/server/src/backend/heartbeat/types.ts +54 -0
  65. package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
  66. package/apps/server/src/backend/http/boundedQueue.ts +92 -0
  67. package/apps/server/src/backend/http/parsers.ts +40 -0
  68. package/apps/server/src/backend/http/router.ts +61 -0
  69. package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
  70. package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
  71. package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
  72. package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
  73. package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
  74. package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
  75. package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
  76. package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
  77. package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
  78. package/apps/server/src/backend/http/routes/index.ts +101 -0
  79. package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
  80. package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
  81. package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
  82. package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
  83. package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
  84. package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
  85. package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
  86. package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
  87. package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
  88. package/apps/server/src/backend/http/schemas.ts +64 -0
  89. package/apps/server/src/backend/http/sse.ts +144 -0
  90. package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
  91. package/apps/server/src/backend/logging/logger.ts +64 -0
  92. package/apps/server/src/backend/mcp/service.ts +326 -0
  93. package/apps/server/src/backend/memory/cli.ts +170 -0
  94. package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
  95. package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
  96. package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
  97. package/apps/server/src/backend/memory/qmdPort.ts +61 -0
  98. package/apps/server/src/backend/memory/records.test.ts +66 -0
  99. package/apps/server/src/backend/memory/records.ts +229 -0
  100. package/apps/server/src/backend/memory/service.ts +2012 -0
  101. package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
  102. package/apps/server/src/backend/memory/types.ts +104 -0
  103. package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
  104. package/apps/server/src/backend/opencode/client.ts +98 -0
  105. package/apps/server/src/backend/opencode/models.ts +41 -0
  106. package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
  107. package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
  108. package/apps/server/src/backend/paths.ts +57 -0
  109. package/apps/server/src/backend/prompts/service.ts +100 -0
  110. package/apps/server/src/backend/queue/queue.test.ts +189 -0
  111. package/apps/server/src/backend/queue/service.ts +177 -0
  112. package/apps/server/src/backend/queue/types.ts +39 -0
  113. package/apps/server/src/backend/run/service.ts +576 -0
  114. package/apps/server/src/backend/run/storage.ts +47 -0
  115. package/apps/server/src/backend/run/types.ts +44 -0
  116. package/apps/server/src/backend/runtime/errors.ts +61 -0
  117. package/apps/server/src/backend/runtime/index.ts +72 -0
  118. package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
  119. package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
  120. package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
  121. package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
  122. package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
  123. package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
  124. package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
  125. package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
  126. package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
  127. package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
  128. package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
  129. package/apps/server/src/backend/skills/service.ts +442 -0
  130. package/apps/server/src/backend/workspace/resolve.ts +27 -0
  131. package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
  132. package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
  133. package/apps/server/src/cli/runtime-assets.mjs +269 -0
  134. package/apps/server/src/cli/runtime-assets.test.ts +52 -0
  135. package/apps/server/src/cli/runtime-layout.mjs +75 -0
  136. package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
  137. package/apps/server/src/cli/standaloneBuild.ts +19 -0
  138. package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
  139. package/apps/server/src/index.ts +178 -0
  140. package/apps/server/tsconfig.json +12 -0
  141. package/backlog.md +5 -0
  142. package/bin/agent-mockingbird +2522 -0
  143. package/bin/runtime-layout.mjs +75 -0
  144. package/build-bin.ts +34 -0
  145. package/build-cli.mjs +37 -0
  146. package/build.ts +40 -0
  147. package/bun-env.d.ts +11 -0
  148. package/bun.lock +888 -0
  149. package/bunfig.toml +2 -0
  150. package/components.json +21 -0
  151. package/config.json +130 -0
  152. package/deploy/RELEASE_INSTALL.md +112 -0
  153. package/deploy/docker-compose.yml +42 -0
  154. package/deploy/systemd/README.md +46 -0
  155. package/deploy/systemd/agent-mockingbird.service +28 -0
  156. package/deploy/systemd/opencode.service +25 -0
  157. package/docs/legacy-config-ui-reference.md +51 -0
  158. package/docs/memory-e2e-trace-2026-03-04.md +63 -0
  159. package/docs/memory-ops.md +96 -0
  160. package/docs/memory-runtime-contract.md +42 -0
  161. package/docs/memory-tuning-remote-2026-03-04.md +59 -0
  162. package/docs/opencode-rebase-workflow-plan.md +614 -0
  163. package/docs/opencode-startup-sync-plan.md +94 -0
  164. package/docs/vendor-opencode.md +41 -0
  165. package/drizzle/0000_famous_turbo.sql +49 -0
  166. package/drizzle/0001_cron_memory_aux.sql +160 -0
  167. package/drizzle/0002_runtime_session_bindings.sql +28 -0
  168. package/drizzle/0003_background_runs.sql +27 -0
  169. package/drizzle/0004_memory_open_write.sql +63 -0
  170. package/drizzle/0005_signal_channel.sql +47 -0
  171. package/drizzle/0006_usage_event_dimensions.sql +7 -0
  172. package/drizzle/meta/0000_snapshot.json +341 -0
  173. package/drizzle/meta/_journal.json +55 -0
  174. package/drizzle.config.ts +14 -0
  175. package/eslint.config.mjs +77 -0
  176. package/knip.json +18 -0
  177. package/memory/2026-03-04.md +4 -0
  178. package/opencode.lock.json +16 -0
  179. package/package.json +67 -0
  180. package/packages/agent-mockingbird-installer/README.md +31 -0
  181. package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
  182. package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
  183. package/packages/agent-mockingbird-installer/package.json +23 -0
  184. package/packages/contracts/package.json +19 -0
  185. package/packages/contracts/src/agentTypes.ts +122 -0
  186. package/packages/contracts/src/cron.ts +146 -0
  187. package/packages/contracts/src/dashboard.ts +378 -0
  188. package/packages/contracts/src/index.ts +3 -0
  189. package/packages/contracts/tsconfig.json +4 -0
  190. package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
  191. package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
  192. package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
  193. package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
  194. package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
  195. package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
  196. package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
  197. package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
  198. package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
  199. package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
  200. package/runtime-assets/opencode-config/opencode.jsonc +25 -0
  201. package/runtime-assets/opencode-config/package.json +5 -0
  202. package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
  203. package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
  204. package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
  205. package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
  206. package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
  207. package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
  208. package/runtime-assets/workspace/AGENTS.md +56 -0
  209. package/runtime-assets/workspace/MEMORY.md +4 -0
  210. package/scripts/build-release-bundle.sh +66 -0
  211. package/scripts/check-ship.ts +383 -0
  212. package/scripts/dev-opencode.sh +17 -0
  213. package/scripts/dev-stack-opencode.sh +15 -0
  214. package/scripts/dev-stack.sh +61 -0
  215. package/scripts/install-systemd.sh +87 -0
  216. package/scripts/memory-e2e.sh +76 -0
  217. package/scripts/memory-trace-e2e.sh +141 -0
  218. package/scripts/migrate-opencode-env.ts +108 -0
  219. package/scripts/onboard/bootstrap.sh +32 -0
  220. package/scripts/opencode-swap.ts +78 -0
  221. package/scripts/opencode-sync.ts +715 -0
  222. package/scripts/runtime-assets-sync.mjs +83 -0
  223. package/scripts/setup-git-hooks.ts +39 -0
  224. package/tsconfig.json +45 -0
  225. package/tui.json +98 -0
  226. package/turbo.json +36 -0
  227. package/vendor/OPENCODE_VENDOR.md +13 -0
@@ -0,0 +1,161 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type { createUiRoutes as CreateUiRoutesType } from "./uiRoutes";
7
+ import type * as RepositoryModuleType from "../../db/repository";
8
+
9
+ const testRoot = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-ui-routes-test-"));
10
+ const testDbPath = path.join(testRoot, "agent-mockingbird.ui-routes.test.db");
11
+ const testConfigPath = path.join(testRoot, "agent-mockingbird.ui-routes.config.json");
12
+ const testWorkspacePath = path.join(testRoot, "workspace");
13
+
14
+ process.env.NODE_ENV = "test";
15
+ process.env.AGENT_MOCKINGBIRD_DB_PATH = testDbPath;
16
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = testConfigPath;
17
+ process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR = testWorkspacePath;
18
+ process.env.AGENT_MOCKINGBIRD_MEMORY_EMBED_PROVIDER = "none";
19
+
20
+ interface RuntimeStub {
21
+ syncSessionMessages: (sessionId: string) => Promise<void>;
22
+ listBackgroundRuns: (input?: { parentSessionId?: string; limit?: number; inFlightOnly?: boolean }) => Promise<
23
+ Array<{
24
+ runId: string;
25
+ parentSessionId: string;
26
+ parentExternalSessionId: string;
27
+ childExternalSessionId: string;
28
+ childSessionId: string | null;
29
+ status: string;
30
+ startedAt: string | null;
31
+ completedAt: string | null;
32
+ error: string | null;
33
+ }>
34
+ >;
35
+ }
36
+
37
+ type CreateUiRoutesFn = typeof CreateUiRoutesType;
38
+ type RepositoryModule = typeof RepositoryModuleType;
39
+
40
+ let createUiRoutes: CreateUiRoutesFn;
41
+ let repository: RepositoryModule;
42
+
43
+ beforeAll(async () => {
44
+ await import("../../db/migrate");
45
+ const uiRoutesModule = await import("./uiRoutes");
46
+ const repositoryModule = await import("../../db/repository");
47
+ createUiRoutes = uiRoutesModule.createUiRoutes;
48
+ repository = repositoryModule;
49
+ });
50
+
51
+ beforeEach(() => {
52
+ repository.resetDatabaseToDefaults();
53
+ });
54
+
55
+ afterAll(() => {
56
+ rmSync(testRoot, { recursive: true, force: true });
57
+ });
58
+
59
+ function buildRuntimeStub(): RuntimeStub {
60
+ return {
61
+ syncSessionMessages: async () => undefined,
62
+ listBackgroundRuns: async () => [],
63
+ };
64
+ }
65
+
66
+ describe("uiRoutes contracts", () => {
67
+ test("GET /api/ui/session-screen/bootstrap returns screen bootstrap payload", async () => {
68
+ const session = repository.createSession({ title: "UI Routes Test" });
69
+ repository.appendChatExchange({
70
+ sessionId: session.id,
71
+ userContent: "hello",
72
+ assistantContent: "world",
73
+ source: "api",
74
+ usage: {
75
+ requestCountDelta: 1,
76
+ inputTokensDelta: 5,
77
+ outputTokensDelta: 8,
78
+ estimatedCostUsdDelta: 0.001,
79
+ },
80
+ });
81
+
82
+ const routes = createUiRoutes(buildRuntimeStub() as never);
83
+ const handler = routes["/api/ui/session-screen/bootstrap"]?.GET;
84
+ expect(handler).toBeDefined();
85
+
86
+ const response = await handler!(new Request(`http://localhost/api/ui/session-screen/bootstrap?sessionId=${encodeURIComponent(session.id)}`));
87
+ expect(response.status).toBe(200);
88
+
89
+ const payload = await response.json() as Record<string, unknown>;
90
+ expect(Array.isArray(payload.sessions)).toBe(true);
91
+ expect(payload.activeSessionId).toBe(session.id);
92
+ expect(Array.isArray(payload.messages)).toBe(true);
93
+ expect(typeof payload.usage).toBe("object");
94
+ expect(payload.usage).not.toBeNull();
95
+ expect(typeof payload.heartbeat).toBe("object");
96
+ expect(payload.heartbeat).not.toBeNull();
97
+ expect(typeof payload.featureFlags).toBe("object");
98
+ expect(payload.featureFlags).not.toBeNull();
99
+ });
100
+
101
+ test("GET /api/ui/sessions/:id/context returns context payload", async () => {
102
+ const session = repository.createSession({ title: "Context Session" });
103
+ repository.appendChatExchange({
104
+ sessionId: session.id,
105
+ userContent: "show me context",
106
+ assistantContent: "sure",
107
+ source: "api",
108
+ usage: {
109
+ requestCountDelta: 1,
110
+ inputTokensDelta: 9,
111
+ outputTokensDelta: 12,
112
+ estimatedCostUsdDelta: 0.0015,
113
+ },
114
+ });
115
+
116
+ const routes = createUiRoutes(buildRuntimeStub() as never);
117
+ const handler = routes["/api/ui/sessions/:id/context"]?.GET;
118
+ expect(handler).toBeDefined();
119
+
120
+ const request = Object.assign(new Request("http://localhost/api/ui/sessions/id/context"), {
121
+ params: { id: session.id },
122
+ });
123
+ const response = await handler!(request as never);
124
+ expect(response.status).toBe(200);
125
+
126
+ const payload = await response.json() as Record<string, unknown>;
127
+ expect(typeof payload.session).toBe("object");
128
+ expect(payload.session).not.toBeNull();
129
+ expect(typeof payload.metrics).toBe("object");
130
+ expect(payload.metrics).not.toBeNull();
131
+ expect(typeof payload.contextBreakdown).toBe("object");
132
+ expect(payload.contextBreakdown).not.toBeNull();
133
+
134
+ const metrics = payload.metrics as { totalMessages?: number };
135
+ expect(typeof metrics.totalMessages).toBe("number");
136
+ expect((metrics.totalMessages ?? 0) > 0).toBe(true);
137
+ });
138
+
139
+ test("GET /api/ui/sessions/:id/review returns placeholder review contract", async () => {
140
+ const session = repository.createSession({ title: "Review Placeholder" });
141
+ const routes = createUiRoutes(buildRuntimeStub() as never);
142
+ const handler = routes["/api/ui/sessions/:id/review"]?.GET;
143
+ expect(handler).toBeDefined();
144
+
145
+ const request = Object.assign(new Request("http://localhost/api/ui/sessions/id/review"), {
146
+ params: { id: session.id },
147
+ });
148
+ const response = await handler!(request as never);
149
+ expect(response.status).toBe(200);
150
+
151
+ const payload = await response.json() as {
152
+ enabled?: boolean;
153
+ reason?: string;
154
+ sessionId?: string;
155
+ };
156
+
157
+ expect(payload.enabled).toBe(false);
158
+ expect(payload.reason).toBe("review_not_yet_mapped");
159
+ expect(payload.sessionId).toBe(session.id);
160
+ });
161
+ });
@@ -0,0 +1,177 @@
1
+ import { buildWorkspaceBootstrapPromptContext } from "../../agents/bootstrapContext";
2
+ import type { RuntimeEngine } from "../../contracts/runtime";
3
+ import {
4
+ getHeartbeatSnapshot,
5
+ getSessionById,
6
+ getUsageSnapshot,
7
+ listMessagesForSession,
8
+ listSessions,
9
+ } from "../../db/repository";
10
+ import { isActiveHeartbeatSession } from "../../heartbeat/state";
11
+ import { listOpencodeModelOptions } from "../../opencode/models";
12
+ import { listPendingPrompts } from "../../prompts/service";
13
+
14
+ function clampPercent(value: number): number {
15
+ if (!Number.isFinite(value)) return 0;
16
+ return Math.max(0, Math.min(100, value));
17
+ }
18
+
19
+ function estimateContextBreakdown(messages: ReturnType<typeof listMessagesForSession>) {
20
+ const userChars = messages.filter(msg => msg.role === "user").reduce((sum, msg) => sum + msg.content.length, 0);
21
+ const assistantChars = messages.filter(msg => msg.role === "assistant").reduce((sum, msg) => sum + msg.content.length, 0);
22
+ const toolCount = messages
23
+ .filter(msg => msg.role === "assistant")
24
+ .flatMap(msg => msg.parts ?? [])
25
+ .filter(part => part.type === "tool_call").length;
26
+ const thinkingCount = messages
27
+ .filter(msg => msg.role === "assistant")
28
+ .flatMap(msg => msg.parts ?? [])
29
+ .filter(part => part.type === "thinking").length;
30
+
31
+ const weightedUser = userChars;
32
+ const weightedAssistant = assistantChars;
33
+ const weightedTools = toolCount * 320;
34
+ const weightedOther = thinkingCount * 120;
35
+ const weightedSystem = Math.max(800, Math.floor((weightedUser + weightedAssistant) * 0.06));
36
+ const total = Math.max(1, weightedUser + weightedAssistant + weightedTools + weightedOther + weightedSystem);
37
+
38
+ return {
39
+ system: clampPercent((weightedSystem / total) * 100),
40
+ user: clampPercent((weightedUser / total) * 100),
41
+ assistant: clampPercent((weightedAssistant / total) * 100),
42
+ tools: clampPercent((weightedTools / total) * 100),
43
+ other: clampPercent((weightedOther / total) * 100),
44
+ };
45
+ }
46
+
47
+ export function createUiRoutes(runtime: RuntimeEngine) {
48
+ return {
49
+ "/api/ui/session-screen/bootstrap": {
50
+ GET: async (req: Request) => {
51
+ const url = new URL(req.url);
52
+ const requestedSessionId = url.searchParams.get("sessionId")?.trim() ?? "";
53
+ const sessions = listSessions().filter(session => !isActiveHeartbeatSession(session.id));
54
+ const activeSessionId =
55
+ (requestedSessionId && !isActiveHeartbeatSession(requestedSessionId) ? requestedSessionId : "") ||
56
+ sessions[0]?.id ||
57
+ "";
58
+
59
+ if (activeSessionId && runtime.syncSessionMessages) {
60
+ try {
61
+ await runtime.syncSessionMessages(activeSessionId);
62
+ } catch {
63
+ // Best effort sync only.
64
+ }
65
+ }
66
+
67
+ const activeSession = activeSessionId ? getSessionById(activeSessionId) : null;
68
+ const messages = activeSessionId ? listMessagesForSession(activeSessionId) : [];
69
+ const usage = getUsageSnapshot();
70
+ const heartbeat = getHeartbeatSnapshot();
71
+ const modelsPayload = await listOpencodeModelOptions().catch(() => []);
72
+ const pendingPrompts = await listPendingPrompts().catch(() => ({
73
+ pendingPermissions: [],
74
+ pendingQuestions: [],
75
+ }));
76
+ const backgroundRuns = runtime.listBackgroundRuns
77
+ ? await runtime.listBackgroundRuns({ parentSessionId: activeSessionId || undefined, limit: 250 }).catch(() => [])
78
+ : [];
79
+ const workspaceBootstrap = buildWorkspaceBootstrapPromptContext();
80
+
81
+ return Response.json({
82
+ sessions,
83
+ activeSessionId,
84
+ activeSession,
85
+ messages,
86
+ usage,
87
+ heartbeat,
88
+ models: modelsPayload,
89
+ pendingPermissions: pendingPrompts.pendingPermissions,
90
+ pendingQuestions: pendingPrompts.pendingQuestions,
91
+ backgroundRuns,
92
+ workspaceBootstrap: {
93
+ mode: workspaceBootstrap.mode,
94
+ identity: workspaceBootstrap.identity,
95
+ files: workspaceBootstrap.files.map(file => ({
96
+ name: file.name,
97
+ missing: file.missing,
98
+ truncated: file.truncated,
99
+ })),
100
+ },
101
+ featureFlags: {
102
+ reviewEnabled: false,
103
+ },
104
+ });
105
+ },
106
+ },
107
+
108
+ "/api/ui/sessions/:id/context": {
109
+ GET: async (req: Request & { params: { id: string } }) => {
110
+ const sessionId = req.params.id;
111
+ const session = getSessionById(sessionId);
112
+ if (!session) {
113
+ return Response.json({ error: "Unknown session" }, { status: 404 });
114
+ }
115
+ if (runtime.syncSessionMessages) {
116
+ try {
117
+ await runtime.syncSessionMessages(sessionId);
118
+ } catch {
119
+ // Best effort sync only.
120
+ }
121
+ }
122
+
123
+ const messages = listMessagesForSession(sessionId);
124
+ const usage = getUsageSnapshot();
125
+ const bootstrap = buildWorkspaceBootstrapPromptContext();
126
+ const userMessages = messages.filter(msg => msg.role === "user");
127
+ const assistantMessages = messages.filter(msg => msg.role === "assistant");
128
+ const breakdown = estimateContextBreakdown(messages);
129
+
130
+ return Response.json({
131
+ session: {
132
+ id: session.id,
133
+ title: session.title,
134
+ model: session.model,
135
+ status: session.status,
136
+ createdAt: session.lastActiveAt,
137
+ lastActiveAt: session.lastActiveAt,
138
+ messageCount: session.messageCount,
139
+ },
140
+ metrics: {
141
+ userMessages: userMessages.length,
142
+ assistantMessages: assistantMessages.length,
143
+ totalMessages: messages.length,
144
+ inputTokens: usage.inputTokens,
145
+ outputTokens: usage.outputTokens,
146
+ totalTokens: usage.inputTokens + usage.outputTokens,
147
+ estimatedCostUsd: usage.estimatedCostUsd,
148
+ },
149
+ contextBreakdown: breakdown,
150
+ bootstrap: {
151
+ mode: bootstrap.mode,
152
+ files: bootstrap.files.map(file => ({
153
+ name: file.name,
154
+ path: file.path,
155
+ missing: file.missing,
156
+ truncated: file.truncated,
157
+ })),
158
+ },
159
+ });
160
+ },
161
+ },
162
+
163
+ "/api/ui/sessions/:id/review": {
164
+ GET: (req: Request & { params: { id: string } }) => {
165
+ const session = getSessionById(req.params.id);
166
+ if (!session) {
167
+ return Response.json({ error: "Unknown session" }, { status: 404 });
168
+ }
169
+ return Response.json({
170
+ enabled: false,
171
+ reason: "review_not_yet_mapped",
172
+ sessionId: session.id,
173
+ });
174
+ },
175
+ },
176
+ };
177
+ }
@@ -0,0 +1,104 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type { createUsageRoutes as CreateUsageRoutesType } from "./usageRoutes";
7
+ import type * as RepositoryModuleType from "../../db/repository";
8
+
9
+ const testRoot = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-usage-routes-test-"));
10
+ const testDbPath = path.join(testRoot, "agent-mockingbird.usage-routes.test.db");
11
+ const testConfigPath = path.join(testRoot, "agent-mockingbird.usage-routes.config.json");
12
+ const testWorkspacePath = path.join(testRoot, "workspace");
13
+
14
+ process.env.NODE_ENV = "test";
15
+ process.env.AGENT_MOCKINGBIRD_DB_PATH = testDbPath;
16
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = testConfigPath;
17
+ process.env.AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR = testWorkspacePath;
18
+ process.env.AGENT_MOCKINGBIRD_MEMORY_EMBED_PROVIDER = "none";
19
+
20
+ type CreateUsageRoutesFn = typeof CreateUsageRoutesType;
21
+ type RepositoryModule = typeof RepositoryModuleType;
22
+
23
+ let createUsageRoutes: CreateUsageRoutesFn;
24
+ let repository: RepositoryModule;
25
+
26
+ beforeAll(async () => {
27
+ await import("../../db/migrate");
28
+ ({ createUsageRoutes } = await import("./usageRoutes"));
29
+ repository = await import("../../db/repository");
30
+ });
31
+
32
+ beforeEach(() => {
33
+ repository.resetDatabaseToDefaults();
34
+ });
35
+
36
+ afterAll(() => {
37
+ rmSync(testRoot, { recursive: true, force: true });
38
+ });
39
+
40
+ describe("usage routes", () => {
41
+ test("GET /api/usage/dashboard returns grouped range-filtered usage data", async () => {
42
+ const session = repository.createSession({
43
+ title: "Usage Route Session",
44
+ model: "anthropic/claude-sonnet-4.5",
45
+ });
46
+ const createdAt = Date.now();
47
+ repository.recordUsageDelta({
48
+ sessionId: session.id,
49
+ requestCountDelta: 1,
50
+ inputTokensDelta: 12,
51
+ outputTokensDelta: 34,
52
+ estimatedCostUsdDelta: 0.1234,
53
+ source: "runtime",
54
+ createdAt,
55
+ });
56
+
57
+ const routes = createUsageRoutes();
58
+ const handler = routes["/api/usage/dashboard"]?.GET;
59
+ expect(handler).toBeDefined();
60
+
61
+ const response = await handler!(new Request(`http://localhost/api/usage/dashboard?startAt=${createdAt - 1}&endAtExclusive=${createdAt + 1}`));
62
+ expect(response.status).toBe(200);
63
+
64
+ const payload = await response.json() as {
65
+ rangeStartAt: string | null;
66
+ rangeEndAtExclusive: string | null;
67
+ totals: { totalTokens: number };
68
+ providers: Array<{ providerId: string }>;
69
+ models: Array<{ providerId: string; modelId: string }>;
70
+ forwardOnlyBreakdown: boolean;
71
+ };
72
+
73
+ expect(payload.rangeStartAt).not.toBeNull();
74
+ expect(payload.rangeEndAtExclusive).not.toBeNull();
75
+ expect(payload.totals.totalTokens).toBe(46);
76
+ expect(payload.providers).toHaveLength(1);
77
+ expect(payload.providers[0]).toMatchObject({ providerId: "anthropic" });
78
+ expect(payload.models).toHaveLength(1);
79
+ expect(payload.models[0]).toMatchObject({ providerId: "anthropic", modelId: "claude-sonnet-4.5" });
80
+ expect(payload.forwardOnlyBreakdown).toBe(true);
81
+ });
82
+
83
+ test("GET /usage returns standalone usage html", async () => {
84
+ const routes = createUsageRoutes();
85
+ const handler = routes["/usage"]?.GET;
86
+ expect(handler).toBeDefined();
87
+
88
+ const response = await handler!();
89
+ expect(response.status).toBe(200);
90
+ expect(response.headers.get("content-type")).toContain("text/html");
91
+
92
+ const html = await response.text();
93
+ expect(html).toContain("<title>Usage</title>");
94
+ expect(html).toContain("/api/usage/dashboard");
95
+ expect(html).toContain("Back to app");
96
+ expect(html).toContain("window.history.back()");
97
+ expect(html).toContain("window.location.origin");
98
+ expect(html).toContain("All time");
99
+ expect(html).toContain("Apply range");
100
+ expect(html).toContain("Month to date");
101
+ expect(html).toContain("usage-start-date");
102
+ expect(html).toContain("usage-end-date");
103
+ });
104
+ });