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,64 @@
1
+ type LogLevel = "info" | "warn" | "error";
2
+
3
+ interface LogFields {
4
+ [key: string]: unknown;
5
+ }
6
+
7
+ function normalizeError(error: unknown) {
8
+ if (error instanceof Error) {
9
+ return {
10
+ name: error.name,
11
+ message: error.message,
12
+ stack: error.stack,
13
+ };
14
+ }
15
+ return {
16
+ message: String(error),
17
+ };
18
+ }
19
+
20
+ function writeLog(level: LogLevel, scope: string, message: string, fields?: LogFields) {
21
+ const payload = {
22
+ level,
23
+ scope,
24
+ message,
25
+ at: new Date().toISOString(),
26
+ ...(fields ?? {}),
27
+ };
28
+ const line = JSON.stringify(payload);
29
+ if (level === "error") {
30
+ console.error(line);
31
+ return;
32
+ }
33
+ if (level === "warn") {
34
+ console.warn(line);
35
+ return;
36
+ }
37
+ console.log(line);
38
+ }
39
+
40
+ export function createLogger(scope: string) {
41
+ return {
42
+ info(message: string, fields?: LogFields) {
43
+ writeLog("info", scope, message, fields);
44
+ },
45
+ warn(message: string, fields?: LogFields) {
46
+ writeLog("warn", scope, message, fields);
47
+ },
48
+ error(message: string, fields?: LogFields) {
49
+ writeLog("error", scope, message, fields);
50
+ },
51
+ errorWithCause(message: string, error: unknown, fields?: LogFields) {
52
+ writeLog("error", scope, message, {
53
+ ...(fields ?? {}),
54
+ error: normalizeError(error),
55
+ });
56
+ },
57
+ warnWithCause(message: string, error: unknown, fields?: LogFields) {
58
+ writeLog("warn", scope, message, {
59
+ ...(fields ?? {}),
60
+ error: normalizeError(error),
61
+ });
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,326 @@
1
+ import type { Config } from "@opencode-ai/sdk/client";
2
+ import { parse as parseJsonc } from "jsonc-parser";
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import type {
8
+ AgentMockingbirdConfig,
9
+ ConfiguredMcpServer,
10
+ } from "../config/schema";
11
+ import {
12
+ createOpencodeClientFromConnection,
13
+ createOpencodeV2ClientFromConnection,
14
+ unwrapSdkData,
15
+ } from "../opencode/client";
16
+ import { resolveOpencodeConfigDir } from "../workspace/resolve";
17
+
18
+ type RuntimeMcpStatus =
19
+ | "connected"
20
+ | "disabled"
21
+ | "failed"
22
+ | "needs_auth"
23
+ | "needs_client_registration"
24
+ | "unknown";
25
+
26
+ interface RuntimeMcp {
27
+ id: string;
28
+ enabled: boolean;
29
+ status: RuntimeMcpStatus;
30
+ error?: string;
31
+ }
32
+
33
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
34
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
35
+ }
36
+
37
+ function stableSerialize(value: unknown): string {
38
+ if (value === null) return "null";
39
+ if (
40
+ typeof value === "string" ||
41
+ typeof value === "number" ||
42
+ typeof value === "boolean"
43
+ ) {
44
+ return JSON.stringify(value);
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
48
+ }
49
+ if (!isPlainObject(value)) return JSON.stringify(value);
50
+ const entries = Object.entries(value).sort(([left], [right]) =>
51
+ left.localeCompare(right),
52
+ );
53
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`).join(",")}}`;
54
+ }
55
+
56
+ function hashConfiguredMcpServers(servers: Array<ConfiguredMcpServer>) {
57
+ return createHash("sha256").update(stableSerialize(servers)).digest("hex");
58
+ }
59
+
60
+ function createMcpClient(config: AgentMockingbirdConfig) {
61
+ return createOpencodeV2ClientFromConnection({
62
+ baseUrl: config.runtime.opencode.baseUrl,
63
+ directory: config.workspace.pinnedDirectory,
64
+ });
65
+ }
66
+
67
+ function createConfigClient(config: AgentMockingbirdConfig) {
68
+ return createOpencodeClientFromConnection({
69
+ baseUrl: config.runtime.opencode.baseUrl,
70
+ directory: config.workspace.pinnedDirectory,
71
+ });
72
+ }
73
+
74
+ function normalizeRecordStringMap(value: unknown): Record<string, string> {
75
+ if (!isPlainObject(value)) return {};
76
+ return Object.fromEntries(
77
+ Object.entries(value)
78
+ .filter(
79
+ (entry): entry is [string, string] => typeof entry[1] === "string",
80
+ )
81
+ .map(([key, entry]) => [key, entry]),
82
+ );
83
+ }
84
+
85
+ function fromOpencodeMcp(
86
+ id: string,
87
+ value: unknown,
88
+ ): ConfiguredMcpServer | null {
89
+ if (!isPlainObject(value)) return null;
90
+ const enabled = value.enabled !== false;
91
+ if (value.type === "remote" && typeof value.url === "string") {
92
+ return {
93
+ id,
94
+ type: "remote",
95
+ enabled,
96
+ url: value.url,
97
+ headers: normalizeRecordStringMap(value.headers),
98
+ oauth: value.oauth === false ? "off" : "auto",
99
+ timeoutMs: typeof value.timeout === "number" ? value.timeout : undefined,
100
+ };
101
+ }
102
+ if (value.type === "local" && Array.isArray(value.command)) {
103
+ return {
104
+ id,
105
+ type: "local",
106
+ enabled,
107
+ command: value.command.filter(
108
+ (entry): entry is string =>
109
+ typeof entry === "string" && entry.trim().length > 0,
110
+ ),
111
+ environment: normalizeRecordStringMap(value.environment),
112
+ timeoutMs: typeof value.timeout === "number" ? value.timeout : undefined,
113
+ };
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function toOpencodeMcp(server: ConfiguredMcpServer): Record<string, unknown> {
119
+ if (server.type === "remote") {
120
+ return {
121
+ type: "remote",
122
+ enabled: server.enabled,
123
+ url: server.url,
124
+ headers: server.headers,
125
+ ...(server.oauth === "off" ? { oauth: false } : {}),
126
+ ...(typeof server.timeoutMs === "number"
127
+ ? { timeout: server.timeoutMs }
128
+ : {}),
129
+ };
130
+ }
131
+ return {
132
+ type: "local",
133
+ enabled: server.enabled,
134
+ command: server.command,
135
+ environment: server.environment,
136
+ ...(typeof server.timeoutMs === "number"
137
+ ? { timeout: server.timeoutMs }
138
+ : {}),
139
+ };
140
+ }
141
+
142
+ function normalizeMcpStatus(value: unknown): RuntimeMcpStatus {
143
+ if (value === "connected") return value;
144
+ if (value === "disabled") return value;
145
+ if (value === "failed") return value;
146
+ if (value === "needs_auth") return value;
147
+ if (value === "needs_client_registration") return value;
148
+ return "unknown";
149
+ }
150
+
151
+ function extractMcpError(value: unknown) {
152
+ if (!isPlainObject(value)) return undefined;
153
+ const error = value.error;
154
+ if (typeof error !== "string") return undefined;
155
+ const trimmed = error.trim();
156
+ return trimmed || undefined;
157
+ }
158
+
159
+ function opencodeConfigFilePath(config: AgentMockingbirdConfig) {
160
+ return path.join(resolveOpencodeConfigDir(config), "opencode.jsonc");
161
+ }
162
+
163
+ export function readConfiguredMcpServersFromWorkspaceConfig(
164
+ config: AgentMockingbirdConfig,
165
+ ): Array<ConfiguredMcpServer> {
166
+ try {
167
+ const raw = readFileSync(opencodeConfigFilePath(config), "utf8");
168
+ const parsed = parseJsonc(raw);
169
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.mcp)) return [];
170
+ return Object.entries(parsed.mcp)
171
+ .map(([id, value]) => fromOpencodeMcp(id.trim(), value))
172
+ .filter((server): server is ConfiguredMcpServer => Boolean(server))
173
+ .sort((left, right) => left.id.localeCompare(right.id));
174
+ } catch {
175
+ return [];
176
+ }
177
+ }
178
+
179
+ export function normalizeMcpIds(ids: Array<string>) {
180
+ const normalized = ids.map((id) => id.trim()).filter(Boolean);
181
+ return [...new Set(normalized)].sort((a, b) => a.localeCompare(b));
182
+ }
183
+
184
+ export function normalizeMcpServerDefinitions(
185
+ servers: Array<ConfiguredMcpServer>,
186
+ ) {
187
+ const deduped = new Map<string, ConfiguredMcpServer>();
188
+ for (const server of servers) {
189
+ const parsed = fromOpencodeMcp(server.id, toOpencodeMcp(server));
190
+ if (!parsed) continue;
191
+ deduped.set(parsed.id, parsed);
192
+ }
193
+ return [...deduped.values()].sort((left, right) =>
194
+ left.id.localeCompare(right.id),
195
+ );
196
+ }
197
+
198
+ export function resolveConfiguredMcpServers(config: AgentMockingbirdConfig) {
199
+ return normalizeMcpServerDefinitions(
200
+ readConfiguredMcpServersFromWorkspaceConfig(config),
201
+ );
202
+ }
203
+
204
+ export function resolveConfiguredMcpIds(config: AgentMockingbirdConfig) {
205
+ return normalizeMcpIds(
206
+ resolveConfiguredMcpServers(config)
207
+ .filter((server) => server.enabled)
208
+ .map((server) => server.id),
209
+ );
210
+ }
211
+
212
+ async function getWorkspaceMcpConfig(config: AgentMockingbirdConfig) {
213
+ const current = unwrapSdkData<Config>(
214
+ await createConfigClient(config).config.get({
215
+ responseStyle: "data",
216
+ throwOnError: true,
217
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
218
+ }),
219
+ );
220
+ const servers = readConfiguredMcpServersFromWorkspaceConfig(config);
221
+ return {
222
+ config: current,
223
+ servers,
224
+ hash: hashConfiguredMcpServers(servers),
225
+ };
226
+ }
227
+
228
+ export async function listRuntimeMcps(
229
+ config: AgentMockingbirdConfig,
230
+ ): Promise<RuntimeMcp[]> {
231
+ const { servers } = await getWorkspaceMcpConfig(config);
232
+ const enabled = new Set(
233
+ servers.filter((server) => server.enabled).map((server) => server.id),
234
+ );
235
+ const payload = unwrapSdkData<Record<string, unknown>>(
236
+ await createMcpClient(config).mcp.status(undefined, {
237
+ responseStyle: "data",
238
+ throwOnError: true,
239
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
240
+ }),
241
+ );
242
+
243
+ const discoveredIds = Object.keys(isPlainObject(payload) ? payload : {})
244
+ .map((id) => id.trim())
245
+ .filter(Boolean);
246
+ const configuredIds = servers.map((server) => server.id);
247
+ const allIds = new Set([...configuredIds, ...discoveredIds]);
248
+
249
+ return [...allIds]
250
+ .sort((a, b) => a.localeCompare(b))
251
+ .map((id) => {
252
+ const rawStatus = isPlainObject(payload) ? payload[id] : undefined;
253
+ const statusRecord = isPlainObject(rawStatus) ? rawStatus : {};
254
+ return {
255
+ id,
256
+ enabled: enabled.has(id),
257
+ status: normalizeMcpStatus(statusRecord.status),
258
+ error: extractMcpError(statusRecord),
259
+ } satisfies RuntimeMcp;
260
+ });
261
+ }
262
+
263
+ export async function connectRuntimeMcp(
264
+ config: AgentMockingbirdConfig,
265
+ id: string,
266
+ ) {
267
+ return unwrapSdkData<boolean>(
268
+ await createMcpClient(config).mcp.connect(
269
+ { name: id },
270
+ {
271
+ responseStyle: "data",
272
+ throwOnError: true,
273
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
274
+ },
275
+ ),
276
+ );
277
+ }
278
+
279
+ export async function disconnectRuntimeMcp(
280
+ config: AgentMockingbirdConfig,
281
+ id: string,
282
+ ) {
283
+ return unwrapSdkData<boolean>(
284
+ await createMcpClient(config).mcp.disconnect(
285
+ { name: id },
286
+ {
287
+ responseStyle: "data",
288
+ throwOnError: true,
289
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
290
+ },
291
+ ),
292
+ );
293
+ }
294
+
295
+ export async function startRuntimeMcpAuth(
296
+ config: AgentMockingbirdConfig,
297
+ id: string,
298
+ ) {
299
+ const response = unwrapSdkData<{ authorizationUrl: string }>(
300
+ await createMcpClient(config).mcp.auth.start(
301
+ { name: id },
302
+ {
303
+ responseStyle: "data",
304
+ throwOnError: true,
305
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
306
+ },
307
+ ),
308
+ );
309
+ return response.authorizationUrl;
310
+ }
311
+
312
+ export async function removeRuntimeMcpAuth(
313
+ config: AgentMockingbirdConfig,
314
+ id: string,
315
+ ) {
316
+ return unwrapSdkData<{ success: true }>(
317
+ await createMcpClient(config).mcp.auth.remove(
318
+ { name: id },
319
+ {
320
+ responseStyle: "data",
321
+ throwOnError: true,
322
+ signal: AbortSignal.timeout(config.runtime.opencode.timeoutMs),
323
+ },
324
+ ),
325
+ );
326
+ }
@@ -0,0 +1,170 @@
1
+ import { readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { migrateLegacyMemoryMarkdownToV2 } from "./records";
5
+ import {
6
+ getMemoryStatus,
7
+ lintMemory,
8
+ listMemoryWriteEvents,
9
+ rememberMemory,
10
+ searchMemoryDetailed,
11
+ searchMemory,
12
+ syncMemoryIndex,
13
+ } from "./service";
14
+
15
+ function usage() {
16
+ console.log("Usage:");
17
+ console.log(" bun run src/backend/memory/cli.ts status");
18
+ console.log(" bun run src/backend/memory/cli.ts sync");
19
+ console.log(" bun run src/backend/memory/cli.ts reindex");
20
+ console.log(" bun run src/backend/memory/cli.ts search <query>");
21
+ console.log(" bun run src/backend/memory/cli.ts search --debug <query>");
22
+ console.log(" bun run src/backend/memory/cli.ts remember <content>");
23
+ console.log(" bun run src/backend/memory/cli.ts activity [limit]");
24
+ console.log(" bun run src/backend/memory/cli.ts lint");
25
+ console.log(" bun run src/backend/memory/cli.ts migrate-format");
26
+ }
27
+
28
+ async function listMarkdownFiles(dir: string, output: string[]) {
29
+ let entries: Array<{ name: string; isFile: () => boolean; isDirectory: () => boolean }> = [];
30
+ try {
31
+ entries = await readdir(dir, { withFileTypes: true });
32
+ } catch {
33
+ return;
34
+ }
35
+ for (const entry of entries) {
36
+ const fullPath = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ await listMarkdownFiles(fullPath, output);
39
+ continue;
40
+ }
41
+ if (entry.isFile() && entry.name.endsWith(".md")) {
42
+ output.push(fullPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ async function collectWorkspaceMemoryFiles(workspaceDir: string) {
48
+ const files: string[] = [];
49
+ for (const candidate of [path.join(workspaceDir, "MEMORY.md"), path.join(workspaceDir, "memory.md")]) {
50
+ try {
51
+ const fileStat = await stat(candidate);
52
+ if (fileStat.isFile()) files.push(candidate);
53
+ } catch {
54
+ // noop
55
+ }
56
+ }
57
+ await listMarkdownFiles(path.join(workspaceDir, "memory"), files);
58
+ return [...new Set(files)];
59
+ }
60
+
61
+ async function run() {
62
+ const [command, ...args] = process.argv.slice(2);
63
+ if (!command) {
64
+ usage();
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+
69
+ switch (command) {
70
+ case "status": {
71
+ const status = await getMemoryStatus();
72
+ console.log(JSON.stringify(status, null, 2));
73
+ return;
74
+ }
75
+ case "sync": {
76
+ await syncMemoryIndex();
77
+ const status = await getMemoryStatus();
78
+ console.log(`Memory sync complete. Files=${status.files} Chunks=${status.chunks}`);
79
+ return;
80
+ }
81
+ case "reindex": {
82
+ await syncMemoryIndex({ force: true });
83
+ const status = await getMemoryStatus();
84
+ console.log(`Memory reindex complete. Files=${status.files} Chunks=${status.chunks}`);
85
+ return;
86
+ }
87
+ case "search": {
88
+ const debug = args.includes("--debug");
89
+ const normalizedArgs = args.filter(arg => arg !== "--debug");
90
+ const query = normalizedArgs.join(" ").trim();
91
+ if (!query) {
92
+ console.error("search requires a query");
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ if (debug) {
97
+ const detailed = await searchMemoryDetailed(query);
98
+ console.log(JSON.stringify({ query, results: detailed.results, debug: detailed.debug }, null, 2));
99
+ return;
100
+ }
101
+ const results = await searchMemory(query);
102
+ console.log(JSON.stringify({ query, results }, null, 2));
103
+ return;
104
+ }
105
+ case "remember": {
106
+ const content = args.join(" ").trim();
107
+ if (!content) {
108
+ console.error("remember requires <content>");
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ const result = await rememberMemory({
113
+ source: "system",
114
+ content,
115
+ });
116
+ console.log(JSON.stringify(result, null, 2));
117
+ return;
118
+ }
119
+ case "activity": {
120
+ const rawLimit = Number(args[0] ?? "20");
121
+ const limit = Number.isFinite(rawLimit) ? rawLimit : 20;
122
+ const events = await listMemoryWriteEvents(limit);
123
+ console.log(JSON.stringify({ count: events.length, events }, null, 2));
124
+ return;
125
+ }
126
+ case "lint": {
127
+ const report = await lintMemory();
128
+ console.log(JSON.stringify(report, null, 2));
129
+ process.exitCode = report.ok ? 0 : 2;
130
+ return;
131
+ }
132
+ case "migrate-format": {
133
+ const status = await getMemoryStatus();
134
+ const files = await collectWorkspaceMemoryFiles(status.workspaceDir);
135
+ let migratedFiles = 0;
136
+ let migratedRecords = 0;
137
+
138
+ for (const filePath of files) {
139
+ const raw = await readFile(filePath, "utf8");
140
+ const migrated = migrateLegacyMemoryMarkdownToV2(raw);
141
+ if (migrated.migrated <= 0) continue;
142
+ await writeFile(filePath, migrated.content, "utf8");
143
+ migratedFiles += 1;
144
+ migratedRecords += migrated.migrated;
145
+ }
146
+
147
+ await syncMemoryIndex({ force: true });
148
+ console.log(
149
+ JSON.stringify(
150
+ {
151
+ ok: true,
152
+ workspaceDir: status.workspaceDir,
153
+ filesScanned: files.length,
154
+ migratedFiles,
155
+ migratedRecords,
156
+ },
157
+ null,
158
+ 2,
159
+ ),
160
+ );
161
+ return;
162
+ }
163
+ default:
164
+ console.error(`Unknown command: ${command}`);
165
+ usage();
166
+ process.exitCode = 1;
167
+ }
168
+ }
169
+
170
+ void run();
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildConceptExpandedQueries } from "./conceptExpansion";
4
+
5
+ describe("buildConceptExpandedQueries", () => {
6
+ test("expands portfolio query to adjacent asset classes", () => {
7
+ const expanded = buildConceptExpandedQueries("How is my portfolio doing?", {
8
+ enabled: true,
9
+ maxPacks: 3,
10
+ maxTerms: 12,
11
+ });
12
+ expect(expanded.matchedConceptPacks).toContain("portfolio");
13
+ expect(expanded.expandedTokens).toContain("silver");
14
+ expect(expanded.expandedQueries.some(item => item.type === "lex")).toBe(true);
15
+ expect(expanded.expandedQueries.some(item => item.type === "vec")).toBe(true);
16
+ });
17
+
18
+ test("respects disable flag", () => {
19
+ const expanded = buildConceptExpandedQueries("portfolio", {
20
+ enabled: false,
21
+ maxPacks: 3,
22
+ maxTerms: 12,
23
+ });
24
+ expect(expanded.matchedConceptPacks).toHaveLength(0);
25
+ expect(expanded.expandedTokens).toHaveLength(0);
26
+ expect(expanded.expandedQueries).toHaveLength(0);
27
+ });
28
+ });
@@ -0,0 +1,80 @@
1
+ import { type ExpandedQuery } from "./qmdPort";
2
+
3
+ const TOKEN_RE = /[a-z0-9]{3,}/g;
4
+
5
+ interface ConceptPack {
6
+ name: string;
7
+ triggers: string[];
8
+ lex: string;
9
+ }
10
+
11
+ const CONCEPT_PACKS: ConceptPack[] = [
12
+ {
13
+ name: "family",
14
+ triggers: ["family", "relative", "relatives", "spouse", "daughter", "child", "children", "wife", "husband"],
15
+ lex: "spouse wife husband partner daughter son child children parent parents mother father siblings sister brother",
16
+ },
17
+ {
18
+ name: "portfolio",
19
+ triggers: ["portfolio", "investing", "investment", "allocation", "holdings", "assets", "networth", "net-worth"],
20
+ lex: "stocks etf etfs bonds treasuries commodities metals gold silver crypto bitcoin cash real estate allocation",
21
+ },
22
+ {
23
+ name: "career",
24
+ triggers: ["career", "job", "work", "company", "employer", "role", "promotion", "manager", "team"],
25
+ lex: "job role company employer manager team promotion compensation salary bonus",
26
+ },
27
+ ];
28
+
29
+ function collectTokens(text: string) {
30
+ const matched = text.toLowerCase().match(TOKEN_RE) ?? [];
31
+ return new Set(matched);
32
+ }
33
+
34
+ export function buildConceptExpandedQueries(
35
+ query: string,
36
+ options: { enabled: boolean; maxPacks: number; maxTerms: number },
37
+ ): {
38
+ expandedQueries: ExpandedQuery[];
39
+ expandedTokens: string[];
40
+ matchedConceptPacks: string[];
41
+ } {
42
+ if (!options.enabled) {
43
+ return {
44
+ expandedQueries: [],
45
+ expandedTokens: [],
46
+ matchedConceptPacks: [],
47
+ };
48
+ }
49
+
50
+ const queryTokens = collectTokens(query);
51
+ const matched = CONCEPT_PACKS.filter(pack => pack.triggers.some(trigger => queryTokens.has(trigger))).slice(
52
+ 0,
53
+ Math.max(0, options.maxPacks),
54
+ );
55
+
56
+ const expandedQueries: ExpandedQuery[] = [];
57
+ const expandedTokens = new Set<string>();
58
+ const matchedConceptPacks = matched.map(pack => pack.name);
59
+
60
+ for (const pack of matched) {
61
+ expandedQueries.push({ type: "lex", text: pack.lex });
62
+ expandedQueries.push({ type: "vec", text: `${query} ${pack.lex}`.trim() });
63
+ for (const token of collectTokens(pack.lex)) {
64
+ expandedTokens.add(token);
65
+ if (expandedTokens.size >= Math.max(1, options.maxTerms)) break;
66
+ }
67
+ if (expandedTokens.size >= Math.max(1, options.maxTerms)) break;
68
+ }
69
+
70
+ const hyde = `Information about ${query}`.trim();
71
+ if (hyde.length > "Information about".length) {
72
+ expandedQueries.push({ type: "hyde", text: hyde });
73
+ }
74
+
75
+ return {
76
+ expandedQueries,
77
+ expandedTokens: [...expandedTokens],
78
+ matchedConceptPacks,
79
+ };
80
+ }