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,2522 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import readline from "node:readline/promises";
7
+ import { spawnSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ import {
10
+ opencodeEnvironment,
11
+ pathsFor,
12
+ prepareRuntimeAssetSources,
13
+ } from "./runtime-layout.mjs";
14
+ import { syncRuntimeWorkspaceAssets } from "./runtime-assets.mjs";
15
+
16
+ const { console, fetch } = globalThis;
17
+
18
+ const DEFAULT_SCOPE = "waffleophagus";
19
+ const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/";
20
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
21
+ const DEFAULT_TAG = "latest";
22
+ const DEFAULT_ROOT_DIR = path.join(os.homedir(), ".agent-mockingbird");
23
+ const USER_UNIT_DIR = path.join(os.homedir(), ".config", "systemd", "user");
24
+ const UNIT_OPENCODE = "opencode.service";
25
+ const UNIT_AGENT_MOCKINGBIRD = "agent-mockingbird.service";
26
+ const AGENT_MOCKINGBIRD_API_BASE_URL = "http://127.0.0.1:3001";
27
+ const DEFAULT_ENABLED_SKILLS = ["config-editor", "config-auditor", "runtime-diagnose", "memory-ops"];
28
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
29
+
30
+ const ANSI = {
31
+ reset: "\x1b[0m",
32
+ bold: "\x1b[1m",
33
+ dim: "\x1b[2m",
34
+ cyan: "\x1b[36m",
35
+ green: "\x1b[32m",
36
+ yellow: "\x1b[33m",
37
+ red: "\x1b[31m",
38
+ };
39
+
40
+ function parseArgs(argv) {
41
+ const args = {
42
+ command: undefined,
43
+ positionals: [],
44
+ yes: false,
45
+ json: false,
46
+ dryRun: false,
47
+ skipLinger: false,
48
+ purgeData: false,
49
+ keepData: false,
50
+ registryUrl: DEFAULT_REGISTRY_URL,
51
+ scope: DEFAULT_SCOPE,
52
+ tag: DEFAULT_TAG,
53
+ version: undefined,
54
+ rootDir: DEFAULT_ROOT_DIR,
55
+ legacyImportFlags: [],
56
+ };
57
+
58
+ const positionals = [];
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const arg = argv[i];
61
+ if (!arg.startsWith("-")) {
62
+ positionals.push(arg);
63
+ continue;
64
+ }
65
+ if (arg === "--yes" || arg === "-y") {
66
+ args.yes = true;
67
+ continue;
68
+ }
69
+ if (arg === "--json") {
70
+ args.json = true;
71
+ continue;
72
+ }
73
+ if (arg === "--dry-run") {
74
+ args.dryRun = true;
75
+ continue;
76
+ }
77
+ if (arg === "--skip-linger") {
78
+ args.skipLinger = true;
79
+ continue;
80
+ }
81
+ if (arg === "--purge-data") {
82
+ args.purgeData = true;
83
+ continue;
84
+ }
85
+ if (arg === "--keep-data") {
86
+ args.keepData = true;
87
+ continue;
88
+ }
89
+ if (arg === "--help" || arg === "-h") {
90
+ args.command = "help";
91
+ continue;
92
+ }
93
+ if (arg === "--skip-memory-sync") {
94
+ args.legacyImportFlags.push(arg);
95
+ continue;
96
+ }
97
+ const next = argv[i + 1];
98
+ if ((arg === "--registry-url" || arg === "--registry") && next) {
99
+ args.registryUrl = next;
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (arg === "--scope" && next) {
104
+ args.scope = next;
105
+ i += 1;
106
+ continue;
107
+ }
108
+ if (arg === "--tag" && next) {
109
+ args.tag = next;
110
+ i += 1;
111
+ continue;
112
+ }
113
+ if (arg === "--version" && next) {
114
+ args.version = next;
115
+ i += 1;
116
+ continue;
117
+ }
118
+ if (arg === "--root-dir" && next) {
119
+ args.rootDir = path.resolve(next);
120
+ i += 1;
121
+ continue;
122
+ }
123
+ if (arg === "--git" && next) {
124
+ args.legacyImportFlags.push(arg, next);
125
+ i += 1;
126
+ continue;
127
+ }
128
+ if (arg === "--path" && next) {
129
+ args.legacyImportFlags.push(arg, next);
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (arg === "--ref" && next) {
134
+ args.legacyImportFlags.push(arg, next);
135
+ i += 1;
136
+ continue;
137
+ }
138
+ if (arg === "--target-dir" && next) {
139
+ args.legacyImportFlags.push(arg, next);
140
+ i += 1;
141
+ continue;
142
+ }
143
+ if (arg === "--preview-id" && next) {
144
+ args.legacyImportFlags.push(arg, next);
145
+ i += 1;
146
+ continue;
147
+ }
148
+ if (arg === "--overwrite" && next) {
149
+ args.legacyImportFlags.push(arg, next);
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (arg === "--skip-path" && next) {
154
+ args.legacyImportFlags.push(arg, next);
155
+ i += 1;
156
+ continue;
157
+ }
158
+ throw new Error(`Unknown argument: ${arg}`);
159
+ }
160
+
161
+ if (positionals.length > 0) {
162
+ args.positionals = positionals;
163
+ if (positionals[0] === "import" && positionals[1] === "openclaw") {
164
+ args.command = "import-openclaw-legacy";
165
+ } else {
166
+ args.command = positionals[0];
167
+ }
168
+ }
169
+
170
+ args.registryUrl = normalizeRegistryUrl(args.registryUrl);
171
+ return args;
172
+ }
173
+
174
+ function normalizeRegistryUrl(url) {
175
+ const trimmed = (url || "").trim();
176
+ if (!trimmed) {
177
+ return DEFAULT_REGISTRY_URL;
178
+ }
179
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
180
+ }
181
+
182
+ function printHelp() {
183
+ console.log(`agent-mockingbird\n\nUsage:\n agent-mockingbird <install|update|onboard|status|restart|start|stop|uninstall> [flags]\n\nFlags:\n --registry-url <url> Scoped npm registry (default: ${DEFAULT_REGISTRY_URL})\n --scope <scope> Package scope (default: ${DEFAULT_SCOPE})\n --tag <tag> Dist-tag when --version not set (default: ${DEFAULT_TAG})\n --version <version> Exact agent-mockingbird version\n --root-dir <path> Install root (default: ${DEFAULT_ROOT_DIR})\n --yes, -y Non-interactive\n --json JSON output\n --dry-run Preview update actions without mutating (update only)\n --skip-linger Skip loginctl enable-linger\n --purge-data Uninstall: remove ${DEFAULT_ROOT_DIR}/data and workspace\n --keep-data Uninstall: keep data/workspace even when --yes\n --help, -h Show help`);
184
+ }
185
+
186
+ function colorEnabled() {
187
+ return process.stdout.isTTY && !process.env.NO_COLOR;
188
+ }
189
+
190
+ function paint(text, color) {
191
+ if (!colorEnabled()) return text;
192
+ return `${color}${text}${ANSI.reset}`;
193
+ }
194
+
195
+ function heading(text) {
196
+ return paint(text, `${ANSI.bold}${ANSI.cyan}`);
197
+ }
198
+
199
+ function info(text) {
200
+ return paint(text, ANSI.dim);
201
+ }
202
+
203
+ function success(text) {
204
+ return paint(text, ANSI.green);
205
+ }
206
+
207
+ function warn(text) {
208
+ return paint(text, ANSI.yellow);
209
+ }
210
+
211
+ function errorText(text) {
212
+ return paint(text, ANSI.red);
213
+ }
214
+
215
+ function summarizeActionPlan(title, lines) {
216
+ return [
217
+ heading(title),
218
+ ...lines,
219
+ ];
220
+ }
221
+
222
+ function sleep(ms) {
223
+ return new Promise(resolve => globalThis.setTimeout(resolve, ms));
224
+ }
225
+
226
+ function shell(command, args, options = {}) {
227
+ const result = spawnSync(command, args, {
228
+ encoding: "utf8",
229
+ stdio: options.stdio ?? "pipe",
230
+ env: options.env ?? process.env,
231
+ cwd: options.cwd,
232
+ });
233
+ return {
234
+ code: result.status ?? 1,
235
+ stdout: result.stdout ?? "",
236
+ stderr: result.stderr ?? "",
237
+ };
238
+ }
239
+
240
+ function must(command, args, options = {}) {
241
+ const result = shell(command, args, options);
242
+ if (result.code !== 0) {
243
+ throw new Error(`${command} ${args.join(" ")} failed: ${(result.stderr || result.stdout).trim()}`);
244
+ }
245
+ return result;
246
+ }
247
+
248
+ function commandExists(command) {
249
+ const result = shell("bash", ["-lc", `command -v ${command}`]);
250
+ return result.code === 0;
251
+ }
252
+
253
+ function ensureDir(dir) {
254
+ fs.mkdirSync(dir, { recursive: true });
255
+ }
256
+
257
+ function writeFile(file, content) {
258
+ ensureDir(path.dirname(file));
259
+ fs.writeFileSync(file, content, "utf8");
260
+ }
261
+
262
+ function readJson(file) {
263
+ return JSON.parse(fs.readFileSync(file, "utf8"));
264
+ }
265
+
266
+ async function promptRuntimeAssetConflictDecision(conflict) {
267
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
268
+ return "use-packaged";
269
+ }
270
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
271
+ try {
272
+ while (true) {
273
+ const answer = (
274
+ await rl.question(
275
+ `runtime-assets conflict for ${conflict.relativePath}\n [k]eep local or [u]se packaged? [k/u] `,
276
+ )
277
+ )
278
+ .trim()
279
+ .toLowerCase();
280
+ if (answer === "k" || answer === "keep" || answer === "keep-local") {
281
+ return "keep-local";
282
+ }
283
+ if (answer === "u" || answer === "use" || answer === "use-packaged") {
284
+ return "use-packaged";
285
+ }
286
+ }
287
+ } finally {
288
+ rl.close();
289
+ }
290
+ }
291
+
292
+ async function ensureDefaultRuntimeSkillsWhenEmpty(input = {}) {
293
+ const retries = typeof input.retries === "number" ? Math.max(1, input.retries) : 5;
294
+ const delayMs = typeof input.delayMs === "number" ? Math.max(100, input.delayMs) : 750;
295
+
296
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
297
+ try {
298
+ const skillsResponse = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config/skills`, { method: "GET" });
299
+ if (!skillsResponse.ok) {
300
+ throw new Error(`GET /api/config/skills failed (${skillsResponse.status})`);
301
+ }
302
+ const payload = await skillsResponse.json();
303
+ const currentSkills = Array.isArray(payload?.skills)
304
+ ? payload.skills.filter((value) => typeof value === "string" && value.trim().length > 0)
305
+ : [];
306
+ if (currentSkills.length > 0) {
307
+ return {
308
+ attempted: true,
309
+ updated: false,
310
+ reason: "existing skills preserved",
311
+ skills: currentSkills,
312
+ };
313
+ }
314
+
315
+ const catalogResponse = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config/skills/catalog`, { method: "GET" });
316
+ if (!catalogResponse.ok) {
317
+ throw new Error(`GET /api/config/skills/catalog failed (${catalogResponse.status})`);
318
+ }
319
+ const catalogPayload = await catalogResponse.json();
320
+ const availableSkillIds = Array.isArray(catalogPayload?.skills)
321
+ ? catalogPayload.skills
322
+ .map((skill) => (skill && typeof skill.id === "string" ? skill.id.trim() : ""))
323
+ .filter((value) => value.length > 0)
324
+ : [];
325
+ const defaultsToEnable = DEFAULT_ENABLED_SKILLS.filter((id) => availableSkillIds.includes(id));
326
+ if (defaultsToEnable.length === 0) {
327
+ return {
328
+ attempted: true,
329
+ updated: false,
330
+ reason: "no default runtime skills available in catalog",
331
+ skills: [],
332
+ };
333
+ }
334
+
335
+ const expectedHash = typeof payload?.hash === "string" ? payload.hash.trim() : "";
336
+
337
+ const updateResponse = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config/skills`, {
338
+ method: "PUT",
339
+ headers: { "Content-Type": "application/json" },
340
+ body: JSON.stringify({
341
+ skills: defaultsToEnable,
342
+ expectedHash: expectedHash || undefined,
343
+ }),
344
+ });
345
+
346
+ const updatePayload = await updateResponse.json().catch(() => ({}));
347
+ if (!updateResponse.ok) {
348
+ const message =
349
+ typeof updatePayload?.error === "string"
350
+ ? updatePayload.error
351
+ : `PUT /api/config/skills failed (${updateResponse.status})`;
352
+ throw new Error(message);
353
+ }
354
+
355
+ const nextSkills = Array.isArray(updatePayload?.skills)
356
+ ? updatePayload.skills
357
+ : defaultsToEnable;
358
+ return {
359
+ attempted: true,
360
+ updated: true,
361
+ reason: "initialized defaults",
362
+ skills: nextSkills,
363
+ };
364
+ } catch (error) {
365
+ if (attempt === retries) {
366
+ return {
367
+ attempted: true,
368
+ updated: false,
369
+ reason: error instanceof Error ? error.message : String(error),
370
+ skills: [],
371
+ };
372
+ }
373
+ await sleep(delayMs);
374
+ }
375
+ }
376
+
377
+ return {
378
+ attempted: false,
379
+ updated: false,
380
+ reason: "skipped",
381
+ skills: [],
382
+ };
383
+ }
384
+
385
+ function userName() {
386
+ return process.env.USER || process.env.LOGNAME || os.userInfo().username;
387
+ }
388
+
389
+ function firstExistingPath(candidates) {
390
+ for (const candidate of candidates) {
391
+ if (candidate && fs.existsSync(candidate)) {
392
+ return candidate;
393
+ }
394
+ }
395
+ return null;
396
+ }
397
+
398
+ function resolveAgentMockingbirdAppDir(paths) {
399
+ return firstExistingPath([paths.agentMockingbirdAppDirGlobal, paths.agentMockingbirdAppDirLocal]);
400
+ }
401
+
402
+ function resolveAgentMockingbirdBin(paths) {
403
+ return firstExistingPath([paths.agentMockingbirdBinGlobal, paths.agentMockingbirdBinLocal]);
404
+ }
405
+
406
+ function resolveAgentMockingbirdServiceEntrypoint(agentMockingbirdAppDir) {
407
+ const pkgPath = path.join(agentMockingbirdAppDir, "package.json");
408
+ const candidates = [];
409
+ if (fs.existsSync(pkgPath)) {
410
+ try {
411
+ const pkg = readJson(pkgPath);
412
+ if (typeof pkg.module === "string") {
413
+ candidates.push(pkg.module);
414
+ }
415
+ if (typeof pkg.main === "string") {
416
+ candidates.push(pkg.main);
417
+ }
418
+ } catch {
419
+ // Ignore parse errors and fall back to static candidates.
420
+ }
421
+ }
422
+
423
+ candidates.push(
424
+ "src/index.ts",
425
+ "src/index.js",
426
+ "dist/index.js",
427
+ "index.js",
428
+ "apps/server/src/index.ts",
429
+ "apps/server/src/index.js",
430
+ "apps/server/dist/index.js",
431
+ );
432
+ for (const relPath of candidates) {
433
+ const absolutePath = path.join(agentMockingbirdAppDir, relPath);
434
+ if (fs.existsSync(absolutePath)) {
435
+ return absolutePath;
436
+ }
437
+ }
438
+ return null;
439
+ }
440
+
441
+ function resolveOpencodeBin(paths) {
442
+ return firstExistingPath([paths.opencodeBinGlobal, paths.opencodeBinLocal]);
443
+ }
444
+
445
+ function resolveBunBinary(paths) {
446
+ if (commandExists("bun")) {
447
+ const out = shell("bash", ["-lc", "command -v bun"]);
448
+ return out.stdout.trim();
449
+ }
450
+ return firstExistingPath([paths.bunBinManagedGlobal, paths.bunBinManagedLocal, paths.bunBinTools]);
451
+ }
452
+
453
+ function tryInstallBun(paths) {
454
+ try {
455
+ npmInstall(paths.npmPrefix, ["bun@latest"], ["-g", "--registry", PUBLIC_NPM_REGISTRY]);
456
+ } catch {
457
+ // Fallback below.
458
+ }
459
+ if (resolveBunBinary(paths)) {
460
+ return;
461
+ }
462
+
463
+ if (!commandExists("curl")) {
464
+ throw new Error("bun is not installed and curl is unavailable for bun.com fallback install.");
465
+ }
466
+
467
+ ensureDir(path.join(paths.rootDir, "tools"));
468
+ const fallback = shell(
469
+ "bash",
470
+ [
471
+ "-lc",
472
+ `curl -fsSL https://bun.com/install | BUN_INSTALL="${path.join(paths.rootDir, "tools", "bun")}" bash`,
473
+ ],
474
+ { stdio: "inherit" },
475
+ );
476
+ if (fallback.code !== 0 || !resolveBunBinary(paths)) {
477
+ throw new Error("Failed to install bun via npm and bun.com install script fallback.");
478
+ }
479
+ }
480
+
481
+ function writeScopedNpmrc(paths, scope, registryUrl) {
482
+ const normalizedScope = scope.replace(/^@/, "");
483
+ writeFile(
484
+ paths.npmrcPath,
485
+ `registry=${PUBLIC_NPM_REGISTRY}\n@${normalizedScope}:registry=${registryUrl}\n`,
486
+ );
487
+ }
488
+
489
+ function npmInstall(prefix, packages, extraArgs = [], env = process.env) {
490
+ const args = ["install", "--no-audit", "--no-fund", "--prefix", prefix, ...extraArgs, ...packages];
491
+ must("npm", args, { stdio: "inherit", env });
492
+ }
493
+
494
+ function ensurePathExportInFile(filePath, exportLine) {
495
+ const exists = fs.existsSync(filePath);
496
+ const content = exists ? fs.readFileSync(filePath, "utf8") : "";
497
+ if (content.includes(exportLine) || content.includes(".local/bin")) {
498
+ return false;
499
+ }
500
+ const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
501
+ fs.appendFileSync(filePath, `${suffix}${exportLine}\n`, "utf8");
502
+ return true;
503
+ }
504
+
505
+ function ensureLocalBinPath(paths) {
506
+ const entries = (process.env.PATH || "").split(":");
507
+ if (entries.includes(paths.localBinDir)) {
508
+ return { inPath: true, updatedFiles: [] };
509
+ }
510
+
511
+ const exportLine = `export PATH="${paths.localBinDir}:$PATH"`;
512
+ const rcFiles = [".bashrc", ".zshrc", ".profile"].map(name => path.join(os.homedir(), name));
513
+ const updatedFiles = [];
514
+ for (const rc of rcFiles) {
515
+ if (fs.existsSync(rc) && ensurePathExportInFile(rc, exportLine)) {
516
+ updatedFiles.push(rc);
517
+ }
518
+ }
519
+
520
+ if (updatedFiles.length === 0) {
521
+ const profile = path.join(os.homedir(), ".profile");
522
+ if (ensurePathExportInFile(profile, exportLine)) {
523
+ updatedFiles.push(profile);
524
+ }
525
+ }
526
+
527
+ return { inPath: false, updatedFiles };
528
+ }
529
+
530
+ function writeAgentMockingbirdShim(paths, agentMockingbirdBin, opencodePackageVersion) {
531
+ ensureDir(paths.localBinDir);
532
+ const shim = `#!/usr/bin/env bash
533
+ set -euo pipefail
534
+ # managed-by: agent-mockingbird-installer
535
+ export AGENT_MOCKINGBIRD_OPENCODE_VERSION=${JSON.stringify(opencodePackageVersion)}
536
+ exec "${agentMockingbirdBin}" "$@"
537
+ `;
538
+ writeFile(paths.agentMockingbirdShimPath, shim);
539
+ fs.chmodSync(paths.agentMockingbirdShimPath, 0o755);
540
+ return paths.agentMockingbirdShimPath;
541
+ }
542
+
543
+ function writeOpencodeShim(paths, opencodeBin) {
544
+ ensureDir(paths.localBinDir);
545
+ const shim = `#!/usr/bin/env bash
546
+ set -euo pipefail
547
+ # managed-by: agent-mockingbird-installer
548
+ export OPENCODE_CONFIG_DIR=${JSON.stringify(paths.opencodeConfigDir)}
549
+ export OPENCODE_DISABLE_PROJECT_CONFIG=1
550
+ exec "${opencodeBin}" "$@"
551
+ `;
552
+ writeFile(paths.opencodeShimPath, shim);
553
+ fs.chmodSync(paths.opencodeShimPath, 0o755);
554
+ return paths.opencodeShimPath;
555
+ }
556
+
557
+ function removeAgentMockingbirdShim(paths) {
558
+ if (!fs.existsSync(paths.agentMockingbirdShimPath)) {
559
+ return false;
560
+ }
561
+ const content = fs.readFileSync(paths.agentMockingbirdShimPath, "utf8");
562
+ if (!content.includes("managed-by: agent-mockingbird-installer")) {
563
+ return false;
564
+ }
565
+ fs.rmSync(paths.agentMockingbirdShimPath, { force: true });
566
+ return true;
567
+ }
568
+
569
+ function removeOpencodeShim(paths) {
570
+ if (!fs.existsSync(paths.opencodeShimPath)) {
571
+ return false;
572
+ }
573
+ const content = fs.readFileSync(paths.opencodeShimPath, "utf8");
574
+ if (!content.includes("managed-by: agent-mockingbird-installer")) {
575
+ return false;
576
+ }
577
+ fs.rmSync(paths.opencodeShimPath, { force: true });
578
+ return true;
579
+ }
580
+
581
+ function shellEscapeSystemdArg(value) {
582
+ return `"${String(value).replace(/(["\\$`])/g, "\\$1")}"`;
583
+ }
584
+
585
+ function hasCompiledDashboardAssets(agentMockingbirdAppDir) {
586
+ return fs.existsSync(path.join(agentMockingbirdAppDir, "dist", "app", "index.html"));
587
+ }
588
+
589
+ function hasCompiledAgentMockingbirdRuntime(agentMockingbirdAppDir) {
590
+ return fs.existsSync(path.join(agentMockingbirdAppDir, "dist", "agent-mockingbird"));
591
+ }
592
+
593
+ function resolveAgentMockingbirdRuntimeCommand(agentMockingbirdAppDir, bunBin) {
594
+ const compiledBinary = path.join(agentMockingbirdAppDir, "dist", "agent-mockingbird");
595
+ if (hasCompiledAgentMockingbirdRuntime(agentMockingbirdAppDir) && hasCompiledDashboardAssets(agentMockingbirdAppDir)) {
596
+ return {
597
+ execStart: compiledBinary,
598
+ mode: "compiled",
599
+ };
600
+ }
601
+
602
+ const entrypoint = resolveAgentMockingbirdServiceEntrypoint(agentMockingbirdAppDir);
603
+ if (entrypoint && bunBin) {
604
+ return {
605
+ execStart: `${shellEscapeSystemdArg(bunBin)} ${shellEscapeSystemdArg(entrypoint)}`,
606
+ mode: "source",
607
+ };
608
+ }
609
+
610
+ return null;
611
+ }
612
+
613
+ function unitContents(paths, opencodeBin, agentMockingbirdExecStart, runtimeMode) {
614
+ const opencode = `[Unit]\nDescription=OpenCode Sidecar for Agent Mockingbird (user service)\nAfter=network.target\nWants=network.target\n\n[Service]\nType=simple\nWorkingDirectory=${paths.workspaceDir}\nEnvironment=AGENT_MOCKINGBIRD_PORT=3001\nEnvironment=AGENT_MOCKINGBIRD_MEMORY_API_BASE_URL=http://127.0.0.1:3001\nEnvironment=OPENCODE_CONFIG_DIR=${paths.opencodeConfigDir}\nEnvironment=OPENCODE_DISABLE_PROJECT_CONFIG=1\nEnvironment=OPENCODE_DISABLE_EXTERNAL_SKILLS=1\nExecStart=${opencodeBin} serve --hostname 127.0.0.1 --port 4096 --print-logs --log-level INFO\nRestart=always\nRestartSec=2\n\n[Install]\nWantedBy=default.target\n`;
615
+
616
+ const agentMockingbird = `[Unit]\nDescription=Agent Mockingbird API and Dashboard (user service)\nAfter=network.target ${UNIT_OPENCODE}\nWants=network.target ${UNIT_OPENCODE}\n\n[Service]\nType=simple\nWorkingDirectory=${paths.rootDir}\nEnvironment=NODE_ENV=production\nEnvironment=PORT=3001\nEnvironment=AGENT_MOCKINGBIRD_CONFIG_PATH=${path.join(paths.dataDir, "agent-mockingbird.config.json")}\nEnvironment=AGENT_MOCKINGBIRD_DB_PATH=${path.join(paths.dataDir, "agent-mockingbird.db")}\nEnvironment=AGENT_MOCKINGBIRD_OPENCODE_BASE_URL=http://127.0.0.1:4096\nEnvironment=AGENT_MOCKINGBIRD_MEMORY_WORKSPACE_DIR=${paths.workspaceDir}\nEnvironment=OPENCODE_CONFIG_DIR=${paths.opencodeConfigDir}\nEnvironment=OPENCODE_DISABLE_PROJECT_CONFIG=1\nEnvironment=AGENT_MOCKINGBIRD_RUNTIME_MODE=${runtimeMode}\nExecStart=${agentMockingbirdExecStart}\nRestart=always\nRestartSec=2\n\n[Install]\nWantedBy=default.target\n`;
617
+
618
+ return { opencode, agentMockingbird };
619
+ }
620
+
621
+ function ensureSystemdUserAvailable() {
622
+ const result = shell("systemctl", ["--user", "status"]);
623
+ if (result.code !== 0) {
624
+ throw new Error("systemd user services are unavailable (`systemctl --user status` failed)");
625
+ }
626
+ }
627
+
628
+ function ensureLinger(skipLinger) {
629
+ if (skipLinger) {
630
+ return { changed: false, skipped: true };
631
+ }
632
+ const user = userName();
633
+ const status = shell("loginctl", ["show-user", user, "-p", "Linger"]);
634
+ if (status.code !== 0) {
635
+ return { changed: false, skipped: true, warning: "Could not read linger status via loginctl." };
636
+ }
637
+ if (status.stdout.toLowerCase().includes("linger=yes")) {
638
+ return { changed: false, skipped: false };
639
+ }
640
+
641
+ const direct = shell("loginctl", ["enable-linger", user]);
642
+ if (direct.code === 0) {
643
+ return { changed: true, skipped: false };
644
+ }
645
+
646
+ const sudo = shell("sudo", ["loginctl", "enable-linger", user], { stdio: "inherit" });
647
+ if (sudo.code === 0) {
648
+ return { changed: true, skipped: false };
649
+ }
650
+
651
+ return {
652
+ changed: false,
653
+ skipped: false,
654
+ warning: `Failed to enable lingering automatically. Run: sudo loginctl enable-linger ${user}`,
655
+ };
656
+ }
657
+
658
+ async function healthCheck(url) {
659
+ try {
660
+ const response = await fetch(url, { method: "GET" });
661
+ return { ok: response.ok, status: response.status };
662
+ } catch {
663
+ return { ok: false, status: 0 };
664
+ }
665
+ }
666
+
667
+ async function healthCheckWithRetry(url, input = {}) {
668
+ const attempts = Number.isFinite(input.attempts) ? Math.max(1, Math.trunc(input.attempts)) : 6;
669
+ const delayMs = Number.isFinite(input.delayMs) ? Math.max(50, Math.trunc(input.delayMs)) : 500;
670
+ let last = { ok: false, status: 0 };
671
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
672
+ last = await healthCheck(url);
673
+ if (last.ok) {
674
+ return last;
675
+ }
676
+ if (attempt < attempts) {
677
+ await sleep(delayMs);
678
+ }
679
+ }
680
+ return last;
681
+ }
682
+
683
+ async function verifyFrontendAssets(baseUrl) {
684
+ try {
685
+ const htmlResponse = await fetch(baseUrl, { method: "GET" });
686
+ const html = await htmlResponse.text();
687
+ const stylesheetMatch = html.match(/<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["']/i);
688
+ const scriptMatch = html.match(/<script[^>]+type=["']module["'][^>]+src=["']([^"']+)["']/i);
689
+ if (!htmlResponse.ok) {
690
+ return {
691
+ ok: false,
692
+ pageOk: false,
693
+ cssOk: false,
694
+ scriptOk: false,
695
+ pageStatus: htmlResponse.status,
696
+ cssStatus: 0,
697
+ scriptStatus: 0,
698
+ cssUrl: "",
699
+ scriptUrl: "",
700
+ referencedFontCount: 0,
701
+ error: `dashboard page returned ${htmlResponse.status}`,
702
+ };
703
+ }
704
+ if (!stylesheetMatch) {
705
+ return {
706
+ ok: false,
707
+ pageOk: true,
708
+ cssOk: false,
709
+ scriptOk: false,
710
+ pageStatus: htmlResponse.status,
711
+ cssStatus: 0,
712
+ scriptStatus: 0,
713
+ cssUrl: "",
714
+ scriptUrl: "",
715
+ referencedFontCount: 0,
716
+ error: "dashboard HTML did not include a stylesheet link",
717
+ };
718
+ }
719
+ if (!scriptMatch) {
720
+ return {
721
+ ok: false,
722
+ pageOk: true,
723
+ cssOk: false,
724
+ scriptOk: false,
725
+ pageStatus: htmlResponse.status,
726
+ cssStatus: 0,
727
+ scriptStatus: 0,
728
+ cssUrl: "",
729
+ scriptUrl: "",
730
+ referencedFontCount: 0,
731
+ error: "dashboard HTML did not include a module script",
732
+ };
733
+ }
734
+
735
+ const cssUrl = new globalThis.URL(stylesheetMatch[1], baseUrl).toString();
736
+ const scriptUrl = new globalThis.URL(scriptMatch[1], baseUrl).toString();
737
+ const cssResponse = await fetch(cssUrl, { method: "GET" });
738
+ const cssText = await cssResponse.text();
739
+ const scriptResponse = await fetch(scriptUrl, { method: "GET" });
740
+ const referencedFontUrls = [
741
+ ...cssText.matchAll(/url\((['"]?)([^'")]*\.woff2)\1\)/gi),
742
+ ].map(match => new globalThis.URL(match[2], cssUrl).toString());
743
+ return {
744
+ ok:
745
+ htmlResponse.ok &&
746
+ cssResponse.ok &&
747
+ scriptResponse.ok &&
748
+ cssUrl.includes("/assets/") &&
749
+ scriptUrl.includes("/assets/") &&
750
+ referencedFontUrls.length > 0,
751
+ pageOk: htmlResponse.ok,
752
+ cssOk: cssResponse.ok && cssUrl.includes("/assets/"),
753
+ scriptOk: scriptResponse.ok && scriptUrl.includes("/assets/"),
754
+ pageStatus: htmlResponse.status,
755
+ cssStatus: cssResponse.status,
756
+ scriptStatus: scriptResponse.status,
757
+ cssUrl,
758
+ scriptUrl,
759
+ referencedFontCount: referencedFontUrls.length,
760
+ error:
761
+ !cssResponse.ok
762
+ ? `stylesheet returned ${cssResponse.status}`
763
+ : !scriptResponse.ok
764
+ ? `module script returned ${scriptResponse.status}`
765
+ : !cssUrl.includes("/assets/")
766
+ ? "stylesheet is not served from /assets/"
767
+ : !scriptUrl.includes("/assets/")
768
+ ? "module script is not served from /assets/"
769
+ : referencedFontUrls.length === 0
770
+ ? "stylesheet did not reference any .woff2 assets"
771
+ : "",
772
+ };
773
+ } catch (error) {
774
+ return {
775
+ ok: false,
776
+ pageOk: false,
777
+ cssOk: false,
778
+ scriptOk: false,
779
+ pageStatus: 0,
780
+ cssStatus: 0,
781
+ scriptStatus: 0,
782
+ cssUrl: "",
783
+ scriptUrl: "",
784
+ referencedFontCount: 0,
785
+ error: error instanceof Error ? error.message : String(error),
786
+ };
787
+ }
788
+ }
789
+
790
+ async function verifyFrontendAssetsWithRetry(baseUrl, input = {}) {
791
+ const attempts = typeof input.attempts === "number" ? Math.max(1, Math.trunc(input.attempts)) : 10;
792
+ const delayMs = typeof input.delayMs === "number" ? Math.max(100, Math.trunc(input.delayMs)) : 750;
793
+ let last = await verifyFrontendAssets(baseUrl);
794
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
795
+ if (last.ok) {
796
+ return last;
797
+ }
798
+ await sleep(delayMs);
799
+ last = await verifyFrontendAssets(baseUrl);
800
+ }
801
+ return last;
802
+ }
803
+
804
+ async function runPostInstallVerification() {
805
+ const agentMockingbirdStatus = shell("systemctl", ["--user", "status", UNIT_AGENT_MOCKINGBIRD, "--no-pager"]);
806
+ const opencodeStatus = shell("systemctl", ["--user", "status", UNIT_OPENCODE, "--no-pager"]);
807
+ const linger = shell("loginctl", ["show-user", userName(), "-p", "Linger"]);
808
+ const frontendAssets = await verifyFrontendAssetsWithRetry("http://127.0.0.1:3001/", {
809
+ attempts: 10,
810
+ delayMs: 750,
811
+ });
812
+ return {
813
+ agentMockingbirdServiceOk: agentMockingbirdStatus.code === 0,
814
+ opencodeServiceOk: opencodeStatus.code === 0,
815
+ lingerOk: linger.code === 0 && linger.stdout.toLowerCase().includes("linger=yes"),
816
+ frontendAssetsOk: frontendAssets.ok,
817
+ frontendAssets,
818
+ commandOutput: {
819
+ agentMockingbirdStatus: (agentMockingbirdStatus.stdout || agentMockingbirdStatus.stderr).trim(),
820
+ opencodeStatus: (opencodeStatus.stdout || opencodeStatus.stderr).trim(),
821
+ linger: (linger.stdout || linger.stderr).trim(),
822
+ },
823
+ };
824
+ }
825
+
826
+ function checkSystemdUserStatus() {
827
+ const result = shell("systemctl", ["--user", "status"]);
828
+ return result.code === 0;
829
+ }
830
+
831
+ function interactivePrompt(message) {
832
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
833
+ return rl.question(message).finally(() => rl.close());
834
+ }
835
+
836
+ async function promptYesNo(message, defaultValue = false) {
837
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
838
+ const answer = (await interactivePrompt(`${message} ${suffix} `)).trim().toLowerCase();
839
+ if (!answer) return defaultValue;
840
+ return answer === "y" || answer === "yes";
841
+ }
842
+
843
+ async function promptText(message, defaultValue = "") {
844
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
845
+ const answer = (await interactivePrompt(`${message}${suffix}: `)).trim();
846
+ return answer || defaultValue;
847
+ }
848
+
849
+ async function promptSelect(message, options, defaultIndex = 0) {
850
+ console.log(message);
851
+ for (let index = 0; index < options.length; index += 1) {
852
+ const option = options[index];
853
+ const marker = index === defaultIndex ? " (default)" : "";
854
+ console.log(` ${index + 1}. ${option.label}${marker}`);
855
+ if (option.hint) {
856
+ console.log(` ${info(option.hint)}`);
857
+ }
858
+ }
859
+ const raw = await interactivePrompt(`Select 1-${options.length}: `);
860
+ const parsed = Number.parseInt(raw.trim(), 10);
861
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > options.length) {
862
+ return options[defaultIndex];
863
+ }
864
+ return options[parsed - 1];
865
+ }
866
+
867
+ function buildInstallSummary({ args, paths }) {
868
+ const target = args.version ?? `tag:${args.tag}`;
869
+ const opencodePackageVersion = readOpenCodePackageVersion();
870
+ const hasBun = Boolean(resolveBunBinary(paths));
871
+ const hasSystemdUser = checkSystemdUserStatus();
872
+ const hasLoginctl = commandExists("loginctl");
873
+ const hasCurl = commandExists("curl");
874
+ return summarizeActionPlan("Install plan", [
875
+ `- Target package: @${args.scope.replace(/^@/, "")}/agent-mockingbird (${target})`,
876
+ `- Private registry scope: @${args.scope.replace(/^@/, "")} -> ${args.registryUrl}`,
877
+ `- Public registry fallback: ${PUBLIC_NPM_REGISTRY} (for non-scope deps, bun, opencode-ai)`,
878
+ `- Install root: ${paths.rootDir}`,
879
+ "",
880
+ "What will happen:",
881
+ `1. Validate required tools: npm + systemd user services.`,
882
+ ` - npm: ${commandExists("npm") ? success("found") : errorText("missing")}`,
883
+ ` - systemctl --user: ${hasSystemdUser ? success("available") : errorText("unavailable")}`,
884
+ "2. Ensure Bun runtime for service command.",
885
+ hasBun
886
+ ? ` - bun: ${success(`found at ${resolveBunBinary(paths)}`)}`
887
+ : ` - bun: ${warn(`not found, will install (npm bun@latest${hasCurl ? " with bun.com/install fallback" : ""})`)}`,
888
+ `3. Install/refresh OpenCode CLI dependency (\`opencode-ai@${opencodePackageVersion}\`) from npmjs.`,
889
+ `4. Install Agent Mockingbird package (@${args.scope.replace(/^@/, "")}/agent-mockingbird) from your scoped registry.`,
890
+ "5. Create/refresh runtime directories under the install root.",
891
+ `6. Install CLI shims at ${paths.agentMockingbirdShimPath} and ${paths.opencodeShimPath}, and ensure ${paths.localBinDir} is on PATH.`,
892
+ `7. Seed workspace skills from bundled package into ${path.join(paths.workspaceDir, ".agents", "skills")}.`,
893
+ `8. Write user services: ${paths.opencodeUnitPath} and ${paths.agentMockingbirdUnitPath}.`,
894
+ "9. Reload systemd user daemon and enable/start both services.",
895
+ args.skipLinger
896
+ ? "10. Skip linger configuration (--skip-linger set)."
897
+ : `10. Attempt loginctl linger so services survive logout/reboot${hasLoginctl ? "" : " (loginctl missing; may require manual setup)"}.`,
898
+ "11. Run health checks, and initialize default enabled skills if config has none.",
899
+ "",
900
+ info("After install (interactive only), a provider onboarding wizard can launch OpenCode auth and set a default model."),
901
+ ]);
902
+ }
903
+
904
+ function buildUpdateSummary({ args, paths }) {
905
+ const target = args.version ?? `tag:${args.tag}`;
906
+ const hasBun = Boolean(resolveBunBinary(paths));
907
+ const hasSystemdUser = checkSystemdUserStatus();
908
+ const hasLoginctl = commandExists("loginctl");
909
+ const hasCurl = commandExists("curl");
910
+ return summarizeActionPlan("Update plan", [
911
+ `- Update target: @${args.scope.replace(/^@/, "")}/agent-mockingbird (${target})`,
912
+ `- Install root: ${paths.rootDir}`,
913
+ "",
914
+ "What this update does:",
915
+ "1. Refresh Agent Mockingbird package + OpenCode CLI dependency.",
916
+ `2. Ensure Bun runtime is available${hasBun ? ` (${success("already present")})` : ` (${warn(`will install${hasCurl ? " with curl fallback" : ""}`)})`}.`,
917
+ "3. Re-seed workspace skills from bundled package.",
918
+ "4. Re-write CLI shim + systemd user units to current paths/entrypoint.",
919
+ " - Includes agent-mockingbird + opencode shims in ~/.local/bin",
920
+ "5. Reload daemon, enable/start services, then force restart both units.",
921
+ args.skipLinger
922
+ ? "6. Skip linger configuration (--skip-linger set)."
923
+ : `6. Re-check linger and enable when missing${hasLoginctl ? "" : " (loginctl missing; may require manual setup)"}.`,
924
+ "7. Run health + service verification, and initialize default enabled skills if config has none.",
925
+ "",
926
+ "What this update does not do:",
927
+ `- It does not wipe ${paths.dataDir} or ${paths.workspaceDir}.`,
928
+ "- It does not uninstall/recreate services from scratch unless unit contents changed.",
929
+ "- It does not reset runtime configuration, DB data, sessions, skills, or agents.",
930
+ `- It does not rerun full onboarding unless you manually run ${paint("agent-mockingbird install", ANSI.bold)} again.`,
931
+ "",
932
+ `Precheck: systemctl --user ${hasSystemdUser ? success("available") : errorText("unavailable (update will fail)")}`,
933
+ ]);
934
+ }
935
+
936
+ function buildUpdateDryRun({ args, paths }) {
937
+ const target = args.version ?? `tag:${args.tag}`;
938
+ const hasBun = Boolean(resolveBunBinary(paths));
939
+ const hasSystemdUser = checkSystemdUserStatus();
940
+ const hasLoginctl = commandExists("loginctl");
941
+
942
+ const actions = [
943
+ `Refresh package @${args.scope.replace(/^@/, "")}/agent-mockingbird (${target})`,
944
+ "Refresh opencode-ai dependency",
945
+ hasBun ? "Reuse existing Bun runtime" : "Install Bun runtime if missing",
946
+ "Reseed workspace skills from bundled package",
947
+ "Rewrite agent-mockingbird CLI shim",
948
+ "Rewrite opencode CLI shim",
949
+ "Rewrite systemd user unit files for opencode + agent-mockingbird",
950
+ "systemctl --user daemon-reload + enable --now opencode.service agent-mockingbird.service",
951
+ "systemctl --user restart opencode.service agent-mockingbird.service",
952
+ args.skipLinger
953
+ ? "Skip loginctl linger step (--skip-linger)"
954
+ : "Check/enable loginctl linger when needed",
955
+ `GET ${AGENT_MOCKINGBIRD_API_BASE_URL}/api/health`,
956
+ "Run service verification checks",
957
+ "Initialize default enabled skills if runtime config currently has none",
958
+ ];
959
+
960
+ const nonActions = [
961
+ `No deletion of ${paths.dataDir} or ${paths.workspaceDir}`,
962
+ "No reset of config, DB, sessions, skills, MCPs, or agents",
963
+ "No onboarding rerun",
964
+ ];
965
+
966
+ return {
967
+ mode: "update-dry-run",
968
+ rootDir: paths.rootDir,
969
+ registryUrl: args.registryUrl,
970
+ target,
971
+ precheck: {
972
+ npm: commandExists("npm"),
973
+ systemdUser: hasSystemdUser,
974
+ loginctl: hasLoginctl,
975
+ bunPresent: hasBun,
976
+ },
977
+ actions,
978
+ nonActions,
979
+ };
980
+ }
981
+
982
+ async function runOnboardingCommand(args) {
983
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
984
+ throw new Error("Onboarding command requires an interactive TTY.");
985
+ }
986
+ const paths = pathsFor({ rootDir: args.rootDir, scope: args.scope, userUnitDir: USER_UNIT_DIR });
987
+ const opencodeBin = resolveOpencodeBin(paths) ?? (commandExists("opencode") ? "opencode" : null);
988
+ if (!opencodeBin) {
989
+ throw new Error("opencode binary not found. Run `agent-mockingbird install` first.");
990
+ }
991
+ const onboarding = await runInteractiveProviderOnboarding({
992
+ opencodeBin,
993
+ workspaceDir: paths.workspaceDir,
994
+ });
995
+ return {
996
+ mode: "onboard",
997
+ rootDir: paths.rootDir,
998
+ onboarding,
999
+ };
1000
+ }
1001
+
1002
+ async function migrateOpenclawWorkspace(input) {
1003
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config/opencode/bootstrap/import-openclaw`, {
1004
+ method: "POST",
1005
+ headers: { "Content-Type": "application/json" },
1006
+ body: JSON.stringify({
1007
+ source: input.source,
1008
+ targetDirectory: input.targetDirectory || undefined,
1009
+ }),
1010
+ });
1011
+ let payload = {};
1012
+ try {
1013
+ payload = await response.json();
1014
+ } catch {
1015
+ payload = {};
1016
+ }
1017
+ if (!response.ok) {
1018
+ const message = typeof payload?.error === "string" ? payload.error : "Failed to migrate OpenClaw workspace";
1019
+ throw new Error(message);
1020
+ }
1021
+ return payload.migration ?? {};
1022
+ }
1023
+
1024
+ async function fetchMemoryStatus() {
1025
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/memory/status`, { method: "GET" });
1026
+ if (!response.ok) return null;
1027
+ const payload = await response.json();
1028
+ return payload?.status ?? null;
1029
+ }
1030
+
1031
+ async function syncMemoryNow() {
1032
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/memory/sync`, { method: "POST" });
1033
+ if (!response.ok) {
1034
+ let payload = {};
1035
+ try {
1036
+ payload = await response.json();
1037
+ } catch {
1038
+ payload = {};
1039
+ }
1040
+ throw new Error(typeof payload?.error === "string" ? payload.error : "Memory sync failed");
1041
+ }
1042
+ }
1043
+
1044
+ async function confirmInstall(args, paths, mode) {
1045
+ if (args.yes) {
1046
+ return;
1047
+ }
1048
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1049
+ throw new Error("Install is interactive by default. Re-run with --yes in non-interactive environments.");
1050
+ }
1051
+
1052
+ const summaryLines =
1053
+ mode === "update" ? buildUpdateSummary({ args, paths }) : buildInstallSummary({ args, paths });
1054
+ for (const line of summaryLines) {
1055
+ console.log(line);
1056
+ }
1057
+ console.log("");
1058
+
1059
+ const proceed = await promptYesNo(`Proceed with ${mode === "update" ? "update" : "install"}?`, false);
1060
+ if (!proceed) {
1061
+ throw new Error("Aborted by user.");
1062
+ }
1063
+ }
1064
+
1065
+ function packageSpec(scope, version, tag) {
1066
+ const normalizedScope = scope.replace(/^@/, "");
1067
+ const target = version || tag;
1068
+ return `@${normalizedScope}/agent-mockingbird@${target}`;
1069
+ }
1070
+
1071
+ function readInstalledVersion(paths) {
1072
+ const appDir = resolveAgentMockingbirdAppDir(paths);
1073
+ if (!appDir) {
1074
+ return null;
1075
+ }
1076
+ const pkgPath = path.join(appDir, "package.json");
1077
+ if (!fs.existsSync(pkgPath)) {
1078
+ return null;
1079
+ }
1080
+ return readJson(pkgPath).version ?? null;
1081
+ }
1082
+
1083
+ function readInstalledOpenCodeVersion(paths) {
1084
+ const pkgPath = path.join(paths.npmPrefix, "lib", "node_modules", "opencode-ai", "package.json");
1085
+ if (!fs.existsSync(pkgPath)) {
1086
+ return null;
1087
+ }
1088
+ return readJson(pkgPath).version ?? null;
1089
+ }
1090
+
1091
+ function readInstalledRuntimeMode(paths) {
1092
+ if (!fs.existsSync(paths.agentMockingbirdUnitPath)) {
1093
+ return null;
1094
+ }
1095
+ const execStartLine = fs
1096
+ .readFileSync(paths.agentMockingbirdUnitPath, "utf8")
1097
+ .split("\n")
1098
+ .find(line => line.startsWith("ExecStart="));
1099
+ if (!execStartLine) {
1100
+ return null;
1101
+ }
1102
+ if (execStartLine.includes("/dist/agent-mockingbird")) {
1103
+ return "compiled";
1104
+ }
1105
+ if (
1106
+ execStartLine.includes("apps/server/src/index.ts") ||
1107
+ execStartLine.includes("apps/server/src/index.js") ||
1108
+ execStartLine.includes("apps/server/dist/index.js") ||
1109
+ execStartLine.includes("/dist/index.js")
1110
+ ) {
1111
+ return "source";
1112
+ }
1113
+ return null;
1114
+ }
1115
+
1116
+ async function fetchRuntimeDefaultModel() {
1117
+ try {
1118
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config`, { method: "GET" });
1119
+ if (!response.ok) return "";
1120
+ const payload = await response.json();
1121
+ const providerId = payload?.config?.runtime?.opencode?.providerId;
1122
+ const modelId = payload?.config?.runtime?.opencode?.modelId;
1123
+ if (typeof providerId !== "string" || typeof modelId !== "string") {
1124
+ return "";
1125
+ }
1126
+ const provider = providerId.trim();
1127
+ const model = modelId.trim();
1128
+ return provider && model ? `${provider}/${model}` : "";
1129
+ } catch {
1130
+ return "";
1131
+ }
1132
+ }
1133
+
1134
+ async function fetchRuntimeModelOptions() {
1135
+ try {
1136
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/opencode/models`, { method: "GET" });
1137
+ const payload = await response.json();
1138
+ if (!response.ok || !Array.isArray(payload?.models)) {
1139
+ return [];
1140
+ }
1141
+ return payload.models
1142
+ .map(model => ({
1143
+ id: typeof model?.id === "string" ? model.id.trim() : "",
1144
+ providerId: typeof model?.providerId === "string" ? model.providerId.trim() : "",
1145
+ modelId: typeof model?.modelId === "string" ? model.modelId.trim() : "",
1146
+ label: typeof model?.label === "string" ? model.label.trim() : "",
1147
+ }))
1148
+ .filter(model => model.id);
1149
+ } catch {
1150
+ return [];
1151
+ }
1152
+ }
1153
+
1154
+ async function fetchRuntimeModelOptionsWithRetry(input = {}) {
1155
+ const attempts = typeof input.attempts === "number" ? Math.max(1, input.attempts) : 8;
1156
+ const delayMs = typeof input.delayMs === "number" ? Math.max(100, input.delayMs) : 1_000;
1157
+ let last = [];
1158
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1159
+ last = await fetchRuntimeModelOptions();
1160
+ if (last.length > 0) return last;
1161
+ if (attempt < attempts - 1) {
1162
+ await sleep(delayMs);
1163
+ }
1164
+ }
1165
+ return last;
1166
+ }
1167
+
1168
+ function buildEmptyModelDiscoveryDiagnostics(input) {
1169
+ const lines = [
1170
+ "No runtime models were discovered after provider setup.",
1171
+ `Workspace: ${input.workspaceDir}`,
1172
+ `OpenCode config dir: ${input.opencodeConfigDir}`,
1173
+ ];
1174
+ if (input.currentModel) {
1175
+ lines.push(`Current runtime default: ${input.currentModel}`);
1176
+ }
1177
+ if (input.authAttempts > 0) {
1178
+ lines.push(
1179
+ `Provider auth attempts: ${input.authAttempts} (${input.authSuccess ? "at least one succeeded" : "none succeeded"})`,
1180
+ );
1181
+ }
1182
+ if (input.authRefresh) {
1183
+ lines.push(`OpenCode auth refresh: ${input.authRefresh.message}`);
1184
+ }
1185
+ lines.push("This usually means the runtime workspace config and the saved provider credentials are still out of sync.");
1186
+ lines.push("Recommended checks:");
1187
+ lines.push(`- OPENCODE_CONFIG_DIR=${input.opencodeConfigDir} OPENCODE_DISABLE_PROJECT_CONFIG=1 opencode auth list`);
1188
+ lines.push("- curl -sS http://127.0.0.1:3001/api/opencode/models");
1189
+ lines.push("- verify the provider you authenticated actually exposes models in this workspace configuration");
1190
+ return lines;
1191
+ }
1192
+
1193
+ async function restartOpencodeServiceForAuthRefresh() {
1194
+ if (!checkSystemdUserStatus()) {
1195
+ return {
1196
+ attempted: false,
1197
+ ok: false,
1198
+ message: "systemctl --user unavailable; skipping automatic OpenCode restart.",
1199
+ };
1200
+ }
1201
+ const loadState = shell("systemctl", ["--user", "show", "--property=LoadState", "--value", UNIT_OPENCODE]);
1202
+ if (loadState.code === 0 && loadState.stdout.trim() === "not-found") {
1203
+ return {
1204
+ attempted: false,
1205
+ ok: false,
1206
+ message: `${UNIT_OPENCODE} is not installed as a user service; provider credentials were saved without a restart.`,
1207
+ };
1208
+ }
1209
+ const restarted = shell("systemctl", ["--user", "restart", UNIT_OPENCODE]);
1210
+ if (restarted.code !== 0) {
1211
+ const detail = (restarted.stderr || restarted.stdout).trim() || "unknown error";
1212
+ return {
1213
+ attempted: true,
1214
+ ok: false,
1215
+ message: `Failed to restart ${UNIT_OPENCODE}: ${detail}`,
1216
+ };
1217
+ }
1218
+ await sleep(1_500);
1219
+ return {
1220
+ attempted: true,
1221
+ ok: true,
1222
+ message: `${UNIT_OPENCODE} restarted to refresh provider credentials.`,
1223
+ };
1224
+ }
1225
+
1226
+ function isValidHttpUrl(value) {
1227
+ try {
1228
+ const parsed = new globalThis.URL(value);
1229
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
1230
+ } catch {
1231
+ return false;
1232
+ }
1233
+ }
1234
+
1235
+ function modelOptionSearchText(option) {
1236
+ return `${option.label || ""} ${option.id || ""} ${option.providerId || ""} ${option.modelId || ""}`
1237
+ .toLowerCase()
1238
+ .trim();
1239
+ }
1240
+
1241
+ function matchesSearchQuery(option, query) {
1242
+ const normalizedQuery = String(query ?? "")
1243
+ .toLowerCase()
1244
+ .trim();
1245
+ if (!normalizedQuery) return true;
1246
+ const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
1247
+ const haystack = modelOptionSearchText(option);
1248
+ return tokens.every(token => haystack.includes(token));
1249
+ }
1250
+
1251
+ async function promptSearchableModelChoice(input) {
1252
+ const { modelOptions, currentModel } = input;
1253
+ const pageSize = 12;
1254
+ let query = "";
1255
+ let page = 0;
1256
+
1257
+ while (true) {
1258
+ const filtered = modelOptions.filter(option => matchesSearchQuery(option, query));
1259
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
1260
+ if (page >= totalPages) page = 0;
1261
+
1262
+ const startIndex = page * pageSize;
1263
+ const pageItems = filtered.slice(startIndex, startIndex + pageSize);
1264
+
1265
+ const titleParts = ["Select a default model"];
1266
+ if (query) titleParts.push(`search="${query}"`);
1267
+ titleParts.push(`matches=${filtered.length}`);
1268
+ titleParts.push(`page=${page + 1}/${totalPages}`);
1269
+
1270
+ const options = [
1271
+ ...pageItems.map(option => ({
1272
+ value: option.id,
1273
+ label: option.label || option.id,
1274
+ hint: option.id,
1275
+ })),
1276
+ ];
1277
+
1278
+ if (filtered.length > 0 && page < totalPages - 1) {
1279
+ options.push({ value: "__next__", label: "Next page", hint: "Show more results" });
1280
+ }
1281
+ if (filtered.length > 0 && page > 0) {
1282
+ options.push({ value: "__prev__", label: "Previous page" });
1283
+ }
1284
+ options.push({ value: "__search__", label: "Change search query", hint: query ? "Edit query" : "Find by provider/model name" });
1285
+ options.push({ value: "__manual__", label: "Enter manually", hint: "Type provider/model yourself" });
1286
+ options.push({ value: "__keep__", label: "Keep current", hint: currentModel || "No change" });
1287
+
1288
+ const selection = await promptSelect(titleParts.join(" | "), options, 0);
1289
+ if (selection.value === "__next__") {
1290
+ page += 1;
1291
+ continue;
1292
+ }
1293
+ if (selection.value === "__prev__") {
1294
+ page = Math.max(0, page - 1);
1295
+ continue;
1296
+ }
1297
+ if (selection.value === "__search__") {
1298
+ query = (await promptText("Search models (provider, model, id)", query)).trim();
1299
+ page = 0;
1300
+ continue;
1301
+ }
1302
+ return selection.value;
1303
+ }
1304
+ }
1305
+
1306
+ async function setRuntimeDefaultModel(modelRef) {
1307
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/runtime/default-model`, {
1308
+ method: "PUT",
1309
+ headers: { "Content-Type": "application/json" },
1310
+ body: JSON.stringify({ model: modelRef }),
1311
+ });
1312
+ let payload = {};
1313
+ try {
1314
+ payload = await response.json();
1315
+ } catch {
1316
+ payload = {};
1317
+ }
1318
+ if (!response.ok) {
1319
+ const message = typeof payload?.error === "string" ? payload.error : "Failed to set runtime default model";
1320
+ throw new Error(message);
1321
+ }
1322
+ }
1323
+
1324
+ async function fetchRuntimeMemoryConfig() {
1325
+ try {
1326
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config`, { method: "GET" });
1327
+ if (!response.ok) {
1328
+ return {
1329
+ enabled: true,
1330
+ embedModel: "qwen3-embedding:4b",
1331
+ ollamaBaseUrl: "http://127.0.0.1:11434",
1332
+ };
1333
+ }
1334
+ const payload = await response.json();
1335
+ const memory = payload?.config?.runtime?.memory ?? {};
1336
+ return {
1337
+ enabled: typeof memory?.enabled === "boolean" ? memory.enabled : true,
1338
+ embedModel: typeof memory?.embedModel === "string" && memory.embedModel.trim()
1339
+ ? memory.embedModel.trim()
1340
+ : "qwen3-embedding:4b",
1341
+ ollamaBaseUrl: typeof memory?.ollamaBaseUrl === "string" && memory.ollamaBaseUrl.trim()
1342
+ ? memory.ollamaBaseUrl.trim()
1343
+ : "http://127.0.0.1:11434",
1344
+ };
1345
+ } catch {
1346
+ return {
1347
+ enabled: true,
1348
+ embedModel: "qwen3-embedding:4b",
1349
+ ollamaBaseUrl: "http://127.0.0.1:11434",
1350
+ };
1351
+ }
1352
+ }
1353
+
1354
+ async function fetchRuntimeConfigHash() {
1355
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config`, { method: "GET" });
1356
+ if (!response.ok) {
1357
+ throw new Error(`Failed to read runtime config hash (${response.status})`);
1358
+ }
1359
+ const payload = await response.json();
1360
+ const expectedHash = typeof payload?.hash === "string" ? payload.hash.trim() : "";
1361
+ if (!expectedHash) {
1362
+ throw new Error("Runtime config hash missing from /api/config response");
1363
+ }
1364
+ return expectedHash;
1365
+ }
1366
+
1367
+ async function setRuntimeMemoryEmbeddingConfig(input) {
1368
+ const expectedHash = await fetchRuntimeConfigHash();
1369
+ const response = await fetch(`${AGENT_MOCKINGBIRD_API_BASE_URL}/api/config/patch-safe`, {
1370
+ method: "POST",
1371
+ headers: { "Content-Type": "application/json" },
1372
+ body: JSON.stringify({
1373
+ patch: {
1374
+ runtime: {
1375
+ memory: {
1376
+ enabled: input.enabled,
1377
+ embedProvider: "ollama",
1378
+ embedModel: input.embedModel,
1379
+ ollamaBaseUrl: input.ollamaBaseUrl,
1380
+ },
1381
+ },
1382
+ },
1383
+ expectedHash,
1384
+ runSmokeTest: false,
1385
+ }),
1386
+ });
1387
+ let payload = {};
1388
+ try {
1389
+ payload = await response.json();
1390
+ } catch {
1391
+ payload = {};
1392
+ }
1393
+ if (!response.ok) {
1394
+ const message = typeof payload?.error === "string" ? payload.error : "Failed to update memory embedding config";
1395
+ throw new Error(message);
1396
+ }
1397
+ }
1398
+
1399
+ async function fetchOllamaModels(ollamaBaseUrl) {
1400
+ const base = ollamaBaseUrl.replace(/\/+$/, "");
1401
+ const response = await fetch(`${base}/api/tags`, { method: "GET" });
1402
+ if (!response.ok) {
1403
+ const text = await response.text();
1404
+ throw new Error(`Ollama tags request failed: ${response.status} ${text}`.trim());
1405
+ }
1406
+ const payload = await response.json();
1407
+ const models = Array.isArray(payload?.models) ? payload.models : [];
1408
+ const names = models
1409
+ .map(model => {
1410
+ if (typeof model?.name === "string" && model.name.trim()) return model.name.trim();
1411
+ if (typeof model?.model === "string" && model.model.trim()) return model.model.trim();
1412
+ return "";
1413
+ })
1414
+ .filter(Boolean);
1415
+ return [...new Set(names)].sort((a, b) => a.localeCompare(b));
1416
+ }
1417
+
1418
+ async function promptSearchableStringChoice(input) {
1419
+ const { title = "Select value", values, currentValue, searchPrompt = "Search", manualLabel = "Enter manually", keepLabel = "Keep current" } = input;
1420
+ const pageSize = 12;
1421
+ let query = "";
1422
+ let page = 0;
1423
+
1424
+ while (true) {
1425
+ const filtered = values.filter(value => matchesSearchQuery({ label: value, id: value, providerId: "", modelId: "" }, query));
1426
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
1427
+ if (page >= totalPages) page = 0;
1428
+ const pageItems = filtered.slice(page * pageSize, page * pageSize + pageSize);
1429
+
1430
+ const options = [
1431
+ ...pageItems.map(value => ({
1432
+ value,
1433
+ label: value,
1434
+ })),
1435
+ ];
1436
+ if (filtered.length > 0 && page < totalPages - 1) {
1437
+ options.push({ value: "__next__", label: "Next page" });
1438
+ }
1439
+ if (filtered.length > 0 && page > 0) {
1440
+ options.push({ value: "__prev__", label: "Previous page" });
1441
+ }
1442
+ options.push({ value: "__search__", label: "Change search query", hint: query ? `Current: ${query}` : "Filter the list" });
1443
+ options.push({ value: "__manual__", label: manualLabel });
1444
+ options.push({ value: "__keep__", label: keepLabel, hint: currentValue || "No change" });
1445
+
1446
+ const selection = await promptSelect(
1447
+ `${title} | matches=${filtered.length} | page=${page + 1}/${totalPages}`,
1448
+ options,
1449
+ 0,
1450
+ );
1451
+ if (selection.value === "__next__") {
1452
+ page += 1;
1453
+ continue;
1454
+ }
1455
+ if (selection.value === "__prev__") {
1456
+ page = Math.max(0, page - 1);
1457
+ continue;
1458
+ }
1459
+ if (selection.value === "__search__") {
1460
+ query = (await promptText(searchPrompt, query)).trim();
1461
+ page = 0;
1462
+ continue;
1463
+ }
1464
+ return selection.value;
1465
+ }
1466
+ }
1467
+
1468
+ async function runOpenclawMigrationWizard() {
1469
+ console.log("");
1470
+ console.log(heading("OpenClaw migration"));
1471
+
1472
+ const sourceChoice = await promptSelect("Import source", [
1473
+ { value: "git", label: "Clone from git repository" },
1474
+ { value: "local", label: "Copy from local directory" },
1475
+ { value: "skip", label: "Skip OpenClaw migration" },
1476
+ ]);
1477
+ if (sourceChoice.value === "skip") {
1478
+ return { attempted: false, skipped: true, reason: "user-skip" };
1479
+ }
1480
+
1481
+ let source;
1482
+ if (sourceChoice.value === "git") {
1483
+ const url = (await promptText("Git repository URL", "")).trim();
1484
+ if (!url) {
1485
+ return { attempted: false, skipped: true, reason: "missing-git-url" };
1486
+ }
1487
+ const ref = (await promptText("Git ref (optional)", "")).trim();
1488
+ source = { mode: "git", url, ref: ref || undefined };
1489
+ } else {
1490
+ const sourcePath = (await promptText("OpenClaw workspace directory", "")).trim();
1491
+ if (!sourcePath) {
1492
+ return { attempted: false, skipped: true, reason: "missing-path" };
1493
+ }
1494
+ source = { mode: "local", path: path.resolve(sourcePath) };
1495
+ }
1496
+
1497
+ const customTarget = await promptYesNo("Use a custom migration target directory?", false);
1498
+ let targetDirectory;
1499
+ if (customTarget) {
1500
+ const entered = (await promptText("Target directory", "")).trim();
1501
+ if (entered) {
1502
+ targetDirectory = path.resolve(entered);
1503
+ }
1504
+ }
1505
+
1506
+ console.log(info("Running one-shot migration..."));
1507
+ const migration = await migrateOpenclawWorkspace({
1508
+ source,
1509
+ targetDirectory,
1510
+ });
1511
+
1512
+ let memorySync = { attempted: false, completed: false, reason: "memory-disabled" };
1513
+ try {
1514
+ const memoryStatus = await fetchMemoryStatus();
1515
+ if (memoryStatus?.enabled) {
1516
+ console.log(info("Memory is enabled; syncing memory index after migration..."));
1517
+ await syncMemoryNow();
1518
+ memorySync = { attempted: true, completed: true };
1519
+ }
1520
+ } catch (error) {
1521
+ memorySync = {
1522
+ attempted: true,
1523
+ completed: false,
1524
+ reason: error instanceof Error ? error.message : String(error),
1525
+ };
1526
+ console.log(warn(`Post-migration memory sync failed: ${memorySync.reason}`));
1527
+ }
1528
+
1529
+ return {
1530
+ attempted: true,
1531
+ skipped: false,
1532
+ migration,
1533
+ memorySync,
1534
+ };
1535
+ }
1536
+
1537
+ async function runInteractiveProviderOnboarding(input) {
1538
+ const { opencodeBin, workspaceDir, opencodeEnv } = input;
1539
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1540
+ return { status: "skipped", reason: "non-interactive" };
1541
+ }
1542
+
1543
+ console.log("");
1544
+ console.log(heading("Agent Mockingbird onboarding"));
1545
+ console.log(info("Optional: connect inference providers through OpenCode and pick a default runtime model."));
1546
+
1547
+ const pathChoice = await promptSelect("Choose onboarding flow", [
1548
+ {
1549
+ value: "quickstart",
1550
+ label: "Quick start (recommended)",
1551
+ hint: "Connect at least one provider, then pick a default model",
1552
+ },
1553
+ {
1554
+ value: "model-only",
1555
+ label: "Model only",
1556
+ hint: "Skip provider auth and only set default model",
1557
+ },
1558
+ {
1559
+ value: "memory-only",
1560
+ label: "Memory only",
1561
+ hint: "Configure Ollama memory embedding settings only",
1562
+ },
1563
+ {
1564
+ value: "openclaw-only",
1565
+ label: "OpenClaw only",
1566
+ hint: "Run OpenClaw workspace migration only",
1567
+ },
1568
+ {
1569
+ value: "skip",
1570
+ label: "Skip for now",
1571
+ hint: "You can rerun later with agent-mockingbird status + dashboard settings",
1572
+ },
1573
+ ]);
1574
+
1575
+ if (pathChoice.value === "skip") {
1576
+ return { status: "skipped", reason: "user-skip" };
1577
+ }
1578
+ const modelOnly = pathChoice.value === "model-only";
1579
+ const memoryOnly = pathChoice.value === "memory-only";
1580
+ const openclawOnly = pathChoice.value === "openclaw-only";
1581
+
1582
+ let authAttempts = 0;
1583
+ let authSuccess = false;
1584
+ let authRefresh = null;
1585
+ if (pathChoice.value === "quickstart") {
1586
+ console.log("");
1587
+ console.log(heading("Provider auth"));
1588
+ console.log(info("Current OpenCode credentials:"));
1589
+ shell(opencodeBin, ["auth", "list"], { stdio: "inherit", cwd: workspaceDir, env: opencodeEnv });
1590
+
1591
+ while (true) {
1592
+ const selection = await promptSelect(
1593
+ "Connect a provider",
1594
+ [
1595
+ {
1596
+ value: "__picker__",
1597
+ label: "OpenCode interactive provider picker (recommended)",
1598
+ hint: "Lets OpenCode show supported providers directly",
1599
+ },
1600
+ {
1601
+ value: "__manual_url__",
1602
+ label: "Enter provider auth URL manually",
1603
+ hint: "Use when provider requires a custom auth endpoint URL",
1604
+ },
1605
+ {
1606
+ value: "__done__",
1607
+ label: "Done with provider auth",
1608
+ },
1609
+ ],
1610
+ 0,
1611
+ );
1612
+
1613
+ if (selection.value === "__done__") break;
1614
+
1615
+ let result;
1616
+ if (selection.value === "__picker__") {
1617
+ authAttempts += 1;
1618
+ result = shell(opencodeBin, ["auth", "login"], { stdio: "inherit", cwd: workspaceDir, env: opencodeEnv });
1619
+ } else if (selection.value === "__manual_url__") {
1620
+ const providerUrl = (await promptText("Provider auth URL", "")).trim();
1621
+ if (!providerUrl) {
1622
+ const continueChoice = await promptYesNo("No URL provided. Continue auth flow?", true);
1623
+ if (!continueChoice) break;
1624
+ continue;
1625
+ }
1626
+ if (!isValidHttpUrl(providerUrl)) {
1627
+ console.log(warn("Invalid URL. Enter a full http(s) URL, for example https://example.com."));
1628
+ const continueChoice = await promptYesNo("Continue auth flow?", true);
1629
+ if (!continueChoice) break;
1630
+ continue;
1631
+ }
1632
+ authAttempts += 1;
1633
+ result = shell(opencodeBin, ["auth", "login", providerUrl], { stdio: "inherit", cwd: workspaceDir, env: opencodeEnv });
1634
+ } else {
1635
+ continue;
1636
+ }
1637
+
1638
+ if (result.code === 0) {
1639
+ authSuccess = true;
1640
+ shell(opencodeBin, ["auth", "list"], { stdio: "inherit", cwd: workspaceDir, env: opencodeEnv });
1641
+ } else {
1642
+ console.log(warn("OpenCode login attempt did not complete successfully."));
1643
+ }
1644
+ const addAnother = await promptYesNo("Connect another provider?", false);
1645
+ if (!addAnother) break;
1646
+ }
1647
+ if (authAttempts > 0) {
1648
+ console.log("");
1649
+ console.log(info("Applying provider auth changes before model selection..."));
1650
+ authRefresh = await restartOpencodeServiceForAuthRefresh();
1651
+ if (authRefresh.ok) {
1652
+ console.log(success(authRefresh.message));
1653
+ } else {
1654
+ console.log(warn(authRefresh.message));
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ const allowModelSetup = !memoryOnly && !openclawOnly;
1660
+ const setModelNow = allowModelSetup
1661
+ ? (modelOnly ? true : await promptYesNo("Set runtime default model now?", true))
1662
+ : false;
1663
+ let selectedModel = "";
1664
+ if (setModelNow) {
1665
+ const currentModel = await fetchRuntimeDefaultModel();
1666
+ const modelOptions =
1667
+ authAttempts > 0
1668
+ ? await fetchRuntimeModelOptionsWithRetry({ attempts: 10, delayMs: 1_000 })
1669
+ : await fetchRuntimeModelOptions();
1670
+ console.log("");
1671
+ console.log(heading("Default model"));
1672
+ if (currentModel) {
1673
+ console.log(info(`Current runtime default: ${currentModel}`));
1674
+ }
1675
+ if (modelOptions.length === 0) {
1676
+ const diagnostics = buildEmptyModelDiscoveryDiagnostics({
1677
+ workspaceDir,
1678
+ opencodeConfigDir: opencodeEnv.OPENCODE_CONFIG_DIR,
1679
+ currentModel,
1680
+ authAttempts,
1681
+ authSuccess,
1682
+ authRefresh,
1683
+ });
1684
+ console.log("");
1685
+ console.log(warn(diagnostics[0]));
1686
+ for (const line of diagnostics.slice(1)) {
1687
+ console.log(info(line));
1688
+ }
1689
+ return {
1690
+ status: "error",
1691
+ message: diagnostics[0],
1692
+ flow: pathChoice.value,
1693
+ authAttempts,
1694
+ authSuccess,
1695
+ authRefresh,
1696
+ selectedModel: null,
1697
+ diagnostics,
1698
+ };
1699
+ } else {
1700
+ const selection = await promptSearchableModelChoice({
1701
+ modelOptions,
1702
+ currentModel,
1703
+ });
1704
+ if (selection === "__manual__") {
1705
+ const manual = (await promptText("Enter provider/model", currentModel || "")).trim();
1706
+ if (manual) {
1707
+ await setRuntimeDefaultModel(manual);
1708
+ selectedModel = manual;
1709
+ }
1710
+ } else if (selection !== "__keep__") {
1711
+ await setRuntimeDefaultModel(selection);
1712
+ selectedModel = selection;
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ const configureMemoryNow = memoryOnly
1718
+ ? true
1719
+ : (!modelOnly && !openclawOnly
1720
+ ? await promptYesNo("Configure memory embedding model (Ollama) now?", true)
1721
+ : false);
1722
+ let memoryEmbedding = null;
1723
+ if (configureMemoryNow) {
1724
+ const currentMemory = await fetchRuntimeMemoryConfig();
1725
+ console.log("");
1726
+ console.log(heading("Memory embeddings"));
1727
+ console.log(info(`Current provider: ollama`));
1728
+ console.log(info(`Current Ollama URL: ${currentMemory.ollamaBaseUrl}`));
1729
+ console.log(info(`Current embedding model: ${currentMemory.embedModel}`));
1730
+
1731
+ const memoryEnabled = await promptYesNo("Enable memory features?", currentMemory.enabled);
1732
+ if (!memoryEnabled) {
1733
+ await setRuntimeMemoryEmbeddingConfig({
1734
+ enabled: false,
1735
+ embedModel: currentMemory.embedModel,
1736
+ ollamaBaseUrl: currentMemory.ollamaBaseUrl,
1737
+ });
1738
+ memoryEmbedding = {
1739
+ configured: true,
1740
+ enabled: false,
1741
+ ollamaBaseUrl: currentMemory.ollamaBaseUrl,
1742
+ embedModel: currentMemory.embedModel,
1743
+ };
1744
+ } else {
1745
+ let ollamaBaseUrl = await promptText("Ollama base URL", currentMemory.ollamaBaseUrl);
1746
+ let models = [];
1747
+
1748
+ while (true) {
1749
+ try {
1750
+ models = await fetchOllamaModels(ollamaBaseUrl);
1751
+ if (models.length === 0) {
1752
+ console.log(warn("Connected to Ollama but no models were returned from /api/tags."));
1753
+ } else {
1754
+ console.log(success(`Discovered ${models.length} model${models.length === 1 ? "" : "s"} from Ollama.`));
1755
+ }
1756
+ break;
1757
+ } catch (error) {
1758
+ const message = error instanceof Error ? error.message : String(error);
1759
+ console.log(warn(`Could not query Ollama at ${ollamaBaseUrl}: ${message}`));
1760
+ const retryChoice = await promptSelect("Ollama model discovery failed", [
1761
+ { value: "retry", label: "Retry same URL" },
1762
+ { value: "change", label: "Change Ollama URL" },
1763
+ { value: "manual", label: "Skip discovery and enter model manually" },
1764
+ { value: "skip", label: "Skip memory embedding setup" },
1765
+ ]);
1766
+ if (retryChoice.value === "retry") continue;
1767
+ if (retryChoice.value === "change") {
1768
+ ollamaBaseUrl = await promptText("Ollama base URL", ollamaBaseUrl);
1769
+ continue;
1770
+ }
1771
+ if (retryChoice.value === "manual") {
1772
+ models = [];
1773
+ break;
1774
+ }
1775
+ memoryEmbedding = {
1776
+ configured: false,
1777
+ reason: "model-discovery-skipped",
1778
+ };
1779
+ break;
1780
+ }
1781
+ }
1782
+
1783
+ if (!memoryEmbedding) {
1784
+ let embedModel = currentMemory.embedModel;
1785
+ if (models.length === 0) {
1786
+ const manualModel = (await promptText("Embedding model (manual)", currentMemory.embedModel)).trim();
1787
+ if (manualModel) {
1788
+ embedModel = manualModel;
1789
+ }
1790
+ } else {
1791
+ const selection = await promptSearchableStringChoice({
1792
+ title: "Select Ollama embedding model",
1793
+ values: models,
1794
+ currentValue: currentMemory.embedModel,
1795
+ searchPrompt: "Search Ollama models",
1796
+ manualLabel: "Enter model manually",
1797
+ keepLabel: "Keep current model",
1798
+ });
1799
+ if (selection === "__manual__") {
1800
+ const manualModel = (await promptText("Embedding model (manual)", currentMemory.embedModel)).trim();
1801
+ if (manualModel) {
1802
+ embedModel = manualModel;
1803
+ }
1804
+ } else if (selection !== "__keep__") {
1805
+ embedModel = selection;
1806
+ }
1807
+ }
1808
+
1809
+ await setRuntimeMemoryEmbeddingConfig({
1810
+ enabled: true,
1811
+ embedModel,
1812
+ ollamaBaseUrl,
1813
+ });
1814
+ memoryEmbedding = {
1815
+ configured: true,
1816
+ enabled: true,
1817
+ ollamaBaseUrl,
1818
+ embedModel,
1819
+ discoveredModelCount: models.length,
1820
+ };
1821
+ }
1822
+ }
1823
+ }
1824
+
1825
+ let openclawMigration = null;
1826
+ const runMigration = openclawOnly
1827
+ ? true
1828
+ : (!modelOnly && !memoryOnly
1829
+ ? await promptYesNo("Import an OpenClaw workspace now?", false)
1830
+ : false);
1831
+ if (runMigration) {
1832
+ try {
1833
+ openclawMigration = await runOpenclawMigrationWizard();
1834
+ } catch (error) {
1835
+ openclawMigration = {
1836
+ attempted: true,
1837
+ skipped: false,
1838
+ error: error instanceof Error ? error.message : String(error),
1839
+ };
1840
+ console.log(warn(`OpenClaw migration failed: ${openclawMigration.error}`));
1841
+ }
1842
+ } else {
1843
+ openclawMigration = {
1844
+ attempted: false,
1845
+ skipped: true,
1846
+ reason: modelOnly || memoryOnly ? "flow-skip" : "user-skip",
1847
+ };
1848
+ }
1849
+
1850
+ return {
1851
+ status: "completed",
1852
+ flow: pathChoice.value,
1853
+ authAttempts,
1854
+ authSuccess,
1855
+ authRefresh,
1856
+ selectedModel: selectedModel || null,
1857
+ memoryEmbedding,
1858
+ openclawMigration,
1859
+ };
1860
+ }
1861
+
1862
+ export const testing = {
1863
+ buildEmptyModelDiscoveryDiagnostics,
1864
+ readOpenCodePackageVersion,
1865
+ }
1866
+
1867
+ /**
1868
+ * @typedef {{
1869
+ * agentMockingbirdAppDirGlobal?: string,
1870
+ * agentMockingbirdAppDirLocal?: string,
1871
+ * }} ReadOpenCodePackageVersionPaths
1872
+ */
1873
+
1874
+ /**
1875
+ * @typedef {{
1876
+ * paths?: ReadOpenCodePackageVersionPaths,
1877
+ * moduleDir?: string,
1878
+ * argv?: string[],
1879
+ * env?: NodeJS.ProcessEnv,
1880
+ * }} ReadOpenCodePackageVersionOptions
1881
+ */
1882
+
1883
+ /**
1884
+ * @param {ReadOpenCodePackageVersionOptions} [options]
1885
+ */
1886
+ function candidateLockPaths({ paths, moduleDir = MODULE_DIR, argv = process.argv, env = process.env } = {}) {
1887
+ const candidates = [];
1888
+ const seen = new Set();
1889
+ const add = (value) => {
1890
+ if (!value) return;
1891
+ const resolved = path.resolve(value);
1892
+ if (seen.has(resolved)) return;
1893
+ seen.add(resolved);
1894
+ candidates.push(resolved);
1895
+ };
1896
+
1897
+ add(path.resolve(moduleDir, "../../../../opencode.lock.json"));
1898
+ add(path.resolve(moduleDir, "../opencode.lock.json"));
1899
+
1900
+ const invokedScript = argv[1];
1901
+ if (typeof invokedScript === "string" && invokedScript.trim()) {
1902
+ add(path.resolve(path.dirname(invokedScript), "../opencode.lock.json"));
1903
+ try {
1904
+ const realScript = fs.realpathSync(invokedScript);
1905
+ add(path.resolve(path.dirname(realScript), "../opencode.lock.json"));
1906
+ add(path.resolve(path.dirname(realScript), "../../opencode.lock.json"));
1907
+ } catch {
1908
+ // Ignore missing or unreadable realpaths.
1909
+ }
1910
+ }
1911
+
1912
+ if (paths) {
1913
+ add(path.join(paths.agentMockingbirdAppDirGlobal, "opencode.lock.json"));
1914
+ add(path.join(paths.agentMockingbirdAppDirLocal, "opencode.lock.json"));
1915
+ }
1916
+
1917
+ const explicitRoot = env.AGENT_MOCKINGBIRD_ROOT_DIR?.trim();
1918
+ const explicitScope = env.AGENT_MOCKINGBIRD_INSTALLER_SCOPE?.trim() || env.AGENT_MOCKINGBIRD_SCOPE?.trim();
1919
+ if (explicitRoot && explicitScope) {
1920
+ const derivedPaths = pathsFor({
1921
+ rootDir: path.resolve(explicitRoot),
1922
+ scope: explicitScope,
1923
+ userUnitDir: USER_UNIT_DIR,
1924
+ });
1925
+ add(path.join(derivedPaths.agentMockingbirdAppDirGlobal, "opencode.lock.json"));
1926
+ add(path.join(derivedPaths.agentMockingbirdAppDirLocal, "opencode.lock.json"));
1927
+ }
1928
+
1929
+ return candidates;
1930
+ }
1931
+
1932
+ /**
1933
+ * @param {ReadOpenCodePackageVersionOptions} [options]
1934
+ */
1935
+ function readOpenCodePackageVersion({ paths, moduleDir = MODULE_DIR, argv = process.argv, env = process.env } = {}) {
1936
+ const envVersion = env.AGENT_MOCKINGBIRD_OPENCODE_VERSION?.trim()
1937
+ if (envVersion) {
1938
+ return envVersion
1939
+ }
1940
+ const candidatePaths = candidateLockPaths({ paths, moduleDir, argv, env });
1941
+ for (const candidatePath of candidatePaths) {
1942
+ if (!fs.existsSync(candidatePath)) {
1943
+ continue;
1944
+ }
1945
+ const parsed = JSON.parse(fs.readFileSync(candidatePath, "utf8"));
1946
+ if (typeof parsed.packageVersion !== "string" || parsed.packageVersion.length === 0) {
1947
+ throw new Error(`Invalid packageVersion in ${candidatePath}`);
1948
+ }
1949
+ return parsed.packageVersion;
1950
+ }
1951
+ throw new Error("Unable to locate opencode.lock.json for installer version pinning.");
1952
+ }
1953
+
1954
+ async function installOrUpdate(args, mode) {
1955
+ if (!commandExists("npm")) {
1956
+ throw new Error("npm is required. Please install npm and run again.");
1957
+ }
1958
+
1959
+ const paths = pathsFor({ rootDir: args.rootDir, scope: args.scope, userUnitDir: USER_UNIT_DIR });
1960
+ const opencodePackageVersion = readOpenCodePackageVersion({ paths });
1961
+ await confirmInstall(args, paths, mode);
1962
+ ensureDir(paths.rootDir);
1963
+ ensureDir(paths.npmPrefix);
1964
+ ensureDir(paths.dataDir);
1965
+ ensureDir(paths.workspaceDir);
1966
+ ensureDir(paths.opencodeConfigDir);
1967
+ ensureDir(paths.logsDir);
1968
+ ensureDir(paths.etcDir);
1969
+
1970
+ ensureSystemdUserAvailable();
1971
+ writeScopedNpmrc(paths, args.scope, args.registryUrl);
1972
+
1973
+ if (!resolveBunBinary(paths)) {
1974
+ tryInstallBun(paths);
1975
+ }
1976
+
1977
+ npmInstall(
1978
+ paths.npmPrefix,
1979
+ [`opencode-ai@${opencodePackageVersion}`],
1980
+ ["-g", "--registry", PUBLIC_NPM_REGISTRY],
1981
+ );
1982
+
1983
+ const env = {
1984
+ ...process.env,
1985
+ npm_config_userconfig: paths.npmrcPath,
1986
+ npm_config_registry: PUBLIC_NPM_REGISTRY,
1987
+ };
1988
+
1989
+ npmInstall(paths.npmPrefix, [packageSpec(args.scope, args.version, args.tag)], ["-g"], env);
1990
+
1991
+ const agentMockingbirdBin = resolveAgentMockingbirdBin(paths);
1992
+ if (!agentMockingbirdBin) {
1993
+ throw new Error(
1994
+ `agent-mockingbird binary missing: looked in ${paths.agentMockingbirdBinGlobal} and ${paths.agentMockingbirdBinLocal}`,
1995
+ );
1996
+ }
1997
+ const shimPath = writeAgentMockingbirdShim(paths, agentMockingbirdBin, opencodePackageVersion);
1998
+ const pathSetup = ensureLocalBinPath(paths);
1999
+
2000
+ const bunBin = resolveBunBinary(paths);
2001
+ if (!bunBin) {
2002
+ throw new Error("bun binary was not found after install.");
2003
+ }
2004
+
2005
+ const agentMockingbirdAppDir = resolveAgentMockingbirdAppDir(paths);
2006
+ if (!agentMockingbirdAppDir) {
2007
+ throw new Error(
2008
+ `agent-mockingbird package directory missing: looked in ${paths.agentMockingbirdAppDirGlobal} and ${paths.agentMockingbirdAppDirLocal}`,
2009
+ );
2010
+ }
2011
+ const runtimeAssetsSource = prepareRuntimeAssetSources(agentMockingbirdAppDir);
2012
+ const workspaceRuntimeAssets = await syncRuntimeWorkspaceAssets({
2013
+ sourceWorkspaceDir: runtimeAssetsSource.workspaceSourceDir,
2014
+ targetWorkspaceDir: paths.workspaceDir,
2015
+ stateFilePath: path.join(paths.dataDir, "runtime-assets-workspace-state.json"),
2016
+ mode,
2017
+ interactive: mode === "update" && !args.yes,
2018
+ onConflict: promptRuntimeAssetConflictDecision,
2019
+ });
2020
+ const opencodeRuntimeAssets = await syncRuntimeWorkspaceAssets({
2021
+ sourceWorkspaceDir: runtimeAssetsSource.opencodeConfigSourceDir,
2022
+ targetWorkspaceDir: paths.opencodeConfigDir,
2023
+ stateFilePath: path.join(paths.dataDir, "runtime-assets-opencode-config-state.json"),
2024
+ mode,
2025
+ interactive: mode === "update" && !args.yes,
2026
+ onConflict: promptRuntimeAssetConflictDecision,
2027
+ });
2028
+ const opencodePackagePath = path.join(paths.opencodeConfigDir, "package.json");
2029
+ const opencodeLockPath = path.join(paths.opencodeConfigDir, "bun.lock");
2030
+ if (fs.existsSync(opencodePackagePath)) {
2031
+ const installArgs = ["install"];
2032
+ if (fs.existsSync(opencodeLockPath)) {
2033
+ installArgs.push("--frozen-lockfile");
2034
+ }
2035
+ must(bunBin, installArgs, { cwd: paths.opencodeConfigDir });
2036
+ }
2037
+ const agentMockingbirdRuntime = resolveAgentMockingbirdRuntimeCommand(agentMockingbirdAppDir, bunBin);
2038
+ if (!agentMockingbirdRuntime) {
2039
+ throw new Error(
2040
+ `agent-mockingbird runtime missing in ${agentMockingbirdAppDir} (checked compiled dist bundle, then package module/main entry files).`,
2041
+ );
2042
+ }
2043
+ const opencodeBin = resolveOpencodeBin(paths);
2044
+ if (!opencodeBin) {
2045
+ throw new Error(
2046
+ `opencode binary missing: looked in ${paths.opencodeBinGlobal} and ${paths.opencodeBinLocal}`,
2047
+ );
2048
+ }
2049
+ const opencodeShimPath = writeOpencodeShim(paths, opencodeBin);
2050
+
2051
+ const units = unitContents(paths, opencodeBin, agentMockingbirdRuntime.execStart, agentMockingbirdRuntime.mode);
2052
+ writeFile(paths.opencodeUnitPath, units.opencode);
2053
+ writeFile(paths.agentMockingbirdUnitPath, units.agentMockingbird);
2054
+
2055
+ must("systemctl", ["--user", "daemon-reload"]);
2056
+ must("systemctl", ["--user", "enable", "--now", UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD]);
2057
+ if (mode === "update") {
2058
+ must("systemctl", ["--user", "restart", UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD]);
2059
+ }
2060
+
2061
+ const linger = ensureLinger(args.skipLinger);
2062
+ const health = await healthCheckWithRetry("http://127.0.0.1:3001/api/health", {
2063
+ attempts: 8,
2064
+ delayMs: 500,
2065
+ });
2066
+ const defaultSkillSync =
2067
+ health.ok
2068
+ ? await ensureDefaultRuntimeSkillsWhenEmpty({ retries: 6, delayMs: 800 })
2069
+ : {
2070
+ attempted: false,
2071
+ updated: false,
2072
+ reason: "skipped (runtime health failed)",
2073
+ skills: [],
2074
+ };
2075
+ const verify = await runPostInstallVerification();
2076
+ let onboarding = null;
2077
+ if (mode === "install" && !args.yes) {
2078
+ try {
2079
+ onboarding = await runInteractiveProviderOnboarding({
2080
+ opencodeBin: opencodeShimPath,
2081
+ workspaceDir: paths.workspaceDir,
2082
+ opencodeEnv: opencodeEnvironment(paths),
2083
+ });
2084
+ } catch (error) {
2085
+ onboarding = {
2086
+ status: "error",
2087
+ message: error instanceof Error ? error.message : String(error),
2088
+ };
2089
+ }
2090
+ }
2091
+
2092
+ return {
2093
+ mode,
2094
+ rootDir: paths.rootDir,
2095
+ registryUrl: args.registryUrl,
2096
+ agentMockingbirdVersion: readInstalledVersion(paths),
2097
+ opencodeVersion: readInstalledOpenCodeVersion(paths),
2098
+ runtimeMode: agentMockingbirdRuntime.mode,
2099
+ shimPath,
2100
+ opencodeShimPath,
2101
+ pathSetup,
2102
+ units: [UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD],
2103
+ runtimeAssets: {
2104
+ workspace: workspaceRuntimeAssets,
2105
+ opencodeConfig: opencodeRuntimeAssets,
2106
+ },
2107
+ defaultSkillSync,
2108
+ health,
2109
+ linger,
2110
+ verify,
2111
+ onboarding,
2112
+ };
2113
+ }
2114
+
2115
+ async function status(args) {
2116
+ const paths = pathsFor({ rootDir: args.rootDir, scope: args.scope, userUnitDir: USER_UNIT_DIR });
2117
+ const unitStates = {};
2118
+ for (const unit of [UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD]) {
2119
+ const result = shell("systemctl", ["--user", "is-active", unit]);
2120
+ unitStates[unit] = result.code === 0 ? result.stdout.trim() : "inactive";
2121
+ }
2122
+ const health = await healthCheck("http://127.0.0.1:3001/api/health");
2123
+
2124
+ return {
2125
+ mode: "status",
2126
+ rootDir: paths.rootDir,
2127
+ agentMockingbirdVersion: readInstalledVersion(paths),
2128
+ opencodeVersion: readInstalledOpenCodeVersion(paths),
2129
+ runtimeMode: readInstalledRuntimeMode(paths),
2130
+ unitStates,
2131
+ health,
2132
+ };
2133
+ }
2134
+
2135
+ function serviceCommand(action) {
2136
+ must("systemctl", ["--user", action, UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD]);
2137
+ }
2138
+
2139
+ async function manageService(args, action) {
2140
+ ensureSystemdUserAvailable();
2141
+ serviceCommand(action);
2142
+ const base = await status(args);
2143
+ return {
2144
+ ...base,
2145
+ mode: action,
2146
+ };
2147
+ }
2148
+
2149
+ async function uninstall(args) {
2150
+ const paths = pathsFor({ rootDir: args.rootDir, scope: args.scope, userUnitDir: USER_UNIT_DIR });
2151
+
2152
+ if (args.purgeData && args.keepData) {
2153
+ throw new Error("Choose only one of --purge-data or --keep-data.");
2154
+ }
2155
+
2156
+ if (!args.yes && process.stdin.isTTY && process.stdout.isTTY) {
2157
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2158
+ const answer = (await rl.question(`Remove user services at ${paths.rootDir}? [y/N] `)).trim().toLowerCase();
2159
+ if (answer !== "y" && answer !== "yes") {
2160
+ rl.close();
2161
+ throw new Error("Aborted by user.");
2162
+ }
2163
+ const purgeAnswer = (await rl.question("Purge data/workspace under ~/.agent-mockingbird/data and ~/.agent-mockingbird/workspace? [y/N] ")).trim().toLowerCase();
2164
+ args.purgeData = purgeAnswer === "y" || purgeAnswer === "yes";
2165
+ rl.close();
2166
+ } else if (!args.yes) {
2167
+ throw new Error("Uninstall requires confirmation. Re-run with --yes in non-interactive environments.");
2168
+ }
2169
+
2170
+ if (args.yes && !args.purgeData) {
2171
+ args.keepData = true;
2172
+ }
2173
+
2174
+ shell("systemctl", ["--user", "disable", "--now", UNIT_AGENT_MOCKINGBIRD, UNIT_OPENCODE]);
2175
+ if (fs.existsSync(paths.agentMockingbirdUnitPath)) {
2176
+ fs.rmSync(paths.agentMockingbirdUnitPath, { force: true });
2177
+ }
2178
+ if (fs.existsSync(paths.opencodeUnitPath)) {
2179
+ fs.rmSync(paths.opencodeUnitPath, { force: true });
2180
+ }
2181
+ shell("systemctl", ["--user", "daemon-reload"]);
2182
+ const removedShim = removeAgentMockingbirdShim(paths);
2183
+ const removedOpencodeShim = removeOpencodeShim(paths);
2184
+
2185
+ if (args.purgeData) {
2186
+ if (fs.existsSync(paths.rootDir)) {
2187
+ fs.rmSync(paths.rootDir, { recursive: true, force: true });
2188
+ }
2189
+ } else {
2190
+ const preserve = [paths.dataDir, paths.workspaceDir, paths.logsDir];
2191
+ for (const target of preserve) {
2192
+ ensureDir(target);
2193
+ }
2194
+ const removePaths = [paths.etcDir, paths.npmPrefix];
2195
+ for (const target of removePaths) {
2196
+ if (fs.existsSync(target)) {
2197
+ fs.rmSync(target, { recursive: true, force: true });
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ return {
2203
+ mode: "uninstall",
2204
+ rootDir: paths.rootDir,
2205
+ unitsRemoved: [UNIT_OPENCODE, UNIT_AGENT_MOCKINGBIRD],
2206
+ removedShim,
2207
+ removedOpencodeShim,
2208
+ removed: true,
2209
+ purgeData: Boolean(args.purgeData),
2210
+ };
2211
+ }
2212
+
2213
+ function printResult(result, asJson) {
2214
+ if (asJson) {
2215
+ console.log(JSON.stringify(result, null, 2));
2216
+ return;
2217
+ }
2218
+
2219
+ if (result.mode === "install" || result.mode === "update") {
2220
+ const localBinDir = path.join(os.homedir(), ".local", "bin");
2221
+ console.log(`${result.mode} complete`);
2222
+ console.log(`root: ${result.rootDir}`);
2223
+ console.log(`registry: ${result.registryUrl}`);
2224
+ console.log(`agent-mockingbird: ${result.agentMockingbirdVersion ?? "unknown"}`);
2225
+ console.log(`runtime: ${result.runtimeMode ?? "unknown"}`);
2226
+ console.log(`opencode: ${result.opencodeVersion ?? "unknown"}`);
2227
+ console.log(`cli: ${result.shimPath ?? "unavailable"}`);
2228
+ if (result.opencodeShimPath) {
2229
+ console.log(`opencode-cli: ${result.opencodeShimPath}`);
2230
+ }
2231
+ if (result.pathSetup) {
2232
+ if (result.pathSetup.inPath) {
2233
+ console.log(`path: ${localBinDir} already in PATH`);
2234
+ } else if (result.pathSetup.updatedFiles?.length > 0) {
2235
+ console.log(`path: added ${localBinDir} to ${result.pathSetup.updatedFiles.join(", ")}`);
2236
+ } else {
2237
+ console.log(`path: add ${localBinDir} to PATH, then restart your shell`);
2238
+ }
2239
+ }
2240
+ for (const [name, assetResult] of Object.entries(result.runtimeAssets ?? {})) {
2241
+ if (!assetResult || typeof assetResult !== "object" || !("target" in assetResult)) {
2242
+ continue;
2243
+ }
2244
+ console.log(`runtime-assets:${name}: source ${assetResult.source}`);
2245
+ console.log(`runtime-assets:${name}: target ${assetResult.target}`);
2246
+ console.log(
2247
+ `runtime-assets:${name}: copied=${assetResult.copied}, overwritten=${assetResult.overwritten}, unchanged=${assetResult.unchanged}, keptLocal=${assetResult.keptLocal}, conflicts=${assetResult.conflicts}`,
2248
+ );
2249
+ if (assetResult.backupsCreated > 0) {
2250
+ console.log(`runtime-assets:${name}: backups created=${assetResult.backupsCreated}`);
2251
+ }
2252
+ }
2253
+ if (result.defaultSkillSync?.attempted) {
2254
+ if (result.defaultSkillSync.updated) {
2255
+ console.log(`skills: enabled defaults (${result.defaultSkillSync.skills.join(", ")})`);
2256
+ } else {
2257
+ console.log(`skills: ${result.defaultSkillSync.reason}`);
2258
+ }
2259
+ }
2260
+ console.log(`health: ${result.health.ok ? "ok" : `failed (${result.health.status})`}`);
2261
+ if (result.linger.warning) {
2262
+ console.log(`linger: ${result.linger.warning}`);
2263
+ } else if (result.linger.changed) {
2264
+ console.log("linger: enabled");
2265
+ }
2266
+ if (result.verify) {
2267
+ console.log(`verify: agent-mockingbird.service=${result.verify.agentMockingbirdServiceOk ? "ok" : "failed"}`);
2268
+ console.log(`verify: opencode.service=${result.verify.opencodeServiceOk ? "ok" : "failed"}`);
2269
+ console.log(`verify: linger=${result.verify.lingerOk ? "yes" : "no"}`);
2270
+ console.log(`verify: frontend-assets=${result.verify.frontendAssetsOk ? "ok" : "failed"}`);
2271
+ if (result.verify.frontendAssets?.cssUrl) {
2272
+ console.log(`verify: frontend-css=${result.verify.frontendAssets.cssUrl}`);
2273
+ }
2274
+ if (
2275
+ !result.verify.agentMockingbirdServiceOk ||
2276
+ !result.verify.opencodeServiceOk ||
2277
+ !result.verify.lingerOk ||
2278
+ !result.verify.frontendAssetsOk
2279
+ ) {
2280
+ console.log("verify-details:");
2281
+ console.log(result.verify.commandOutput.agentMockingbirdStatus || "(no output)");
2282
+ console.log(result.verify.commandOutput.opencodeStatus || "(no output)");
2283
+ console.log(result.verify.commandOutput.linger || "(no output)");
2284
+ if (result.verify.frontendAssets?.error) {
2285
+ console.log(result.verify.frontendAssets.error);
2286
+ }
2287
+ }
2288
+ }
2289
+ if (result.mode === "install" && result.onboarding) {
2290
+ if (result.onboarding.status === "completed") {
2291
+ console.log(`onboarding: completed (${result.onboarding.flow})`);
2292
+ if (result.onboarding.authAttempts > 0) {
2293
+ console.log(`onboarding: provider auth attempts=${result.onboarding.authAttempts} success=${result.onboarding.authSuccess ? "yes" : "no"}`);
2294
+ if (result.onboarding.authRefresh) {
2295
+ console.log(`onboarding: auth refresh=${result.onboarding.authRefresh.ok ? "ok" : "skipped/failed"}`);
2296
+ }
2297
+ }
2298
+ if (result.onboarding.selectedModel) {
2299
+ console.log(`onboarding: default model=${result.onboarding.selectedModel}`);
2300
+ }
2301
+ if (result.onboarding.memoryEmbedding) {
2302
+ if (result.onboarding.memoryEmbedding.configured === false) {
2303
+ console.log(`onboarding: memory embeddings=skipped (${result.onboarding.memoryEmbedding.reason ?? "not-configured"})`);
2304
+ } else {
2305
+ console.log(`onboarding: memory enabled=${result.onboarding.memoryEmbedding.enabled ? "yes" : "no"}`);
2306
+ if (result.onboarding.memoryEmbedding.ollamaBaseUrl) {
2307
+ console.log(`onboarding: memory ollama=${result.onboarding.memoryEmbedding.ollamaBaseUrl}`);
2308
+ }
2309
+ if (result.onboarding.memoryEmbedding.embedModel) {
2310
+ console.log(`onboarding: memory embedModel=${result.onboarding.memoryEmbedding.embedModel}`);
2311
+ }
2312
+ }
2313
+ }
2314
+ } else if (result.onboarding.status === "skipped") {
2315
+ console.log(`onboarding: skipped (${result.onboarding.reason})`);
2316
+ } else if (result.onboarding.status === "error") {
2317
+ console.log(`onboarding: failed (${result.onboarding.message})`);
2318
+ if (Array.isArray(result.onboarding.diagnostics)) {
2319
+ for (const line of result.onboarding.diagnostics) {
2320
+ console.log(`onboarding: ${line}`);
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2325
+ return;
2326
+ }
2327
+
2328
+ if (result.mode === "update-dry-run") {
2329
+ console.log("update dry-run");
2330
+ console.log(`root: ${result.rootDir}`);
2331
+ console.log(`registry: ${result.registryUrl}`);
2332
+ console.log(`target: ${result.target}`);
2333
+ console.log(`precheck: npm=${result.precheck.npm ? "ok" : "missing"}, systemd-user=${result.precheck.systemdUser ? "ok" : "missing"}, bun=${result.precheck.bunPresent ? "present" : "will-install"}`);
2334
+ console.log("planned actions:");
2335
+ for (const action of result.actions) {
2336
+ console.log(`- ${action}`);
2337
+ }
2338
+ console.log("not performed:");
2339
+ for (const nonAction of result.nonActions) {
2340
+ console.log(`- ${nonAction}`);
2341
+ }
2342
+ return;
2343
+ }
2344
+
2345
+ if (result.mode === "status") {
2346
+ console.log("status");
2347
+ console.log(`root: ${result.rootDir}`);
2348
+ console.log(`agent-mockingbird: ${result.agentMockingbirdVersion ?? "not installed"}`);
2349
+ console.log(`runtime: ${result.runtimeMode ?? "unknown"}`);
2350
+ console.log(`opencode: ${result.opencodeVersion ?? "not installed"}`);
2351
+ console.log(`units: ${UNIT_OPENCODE}=${result.unitStates[UNIT_OPENCODE]}, ${UNIT_AGENT_MOCKINGBIRD}=${result.unitStates[UNIT_AGENT_MOCKINGBIRD]}`);
2352
+ console.log(`health: ${result.health.ok ? "ok" : `failed (${result.health.status})`}`);
2353
+ return;
2354
+ }
2355
+
2356
+ if (result.mode === "restart" || result.mode === "start" || result.mode === "stop") {
2357
+ console.log(`${result.mode} complete`);
2358
+ console.log(`root: ${result.rootDir}`);
2359
+ console.log(`agent-mockingbird: ${result.agentMockingbirdVersion ?? "not installed"}`);
2360
+ console.log(`runtime: ${result.runtimeMode ?? "unknown"}`);
2361
+ console.log(`opencode: ${result.opencodeVersion ?? "not installed"}`);
2362
+ console.log(`units: ${UNIT_OPENCODE}=${result.unitStates[UNIT_OPENCODE]}, ${UNIT_AGENT_MOCKINGBIRD}=${result.unitStates[UNIT_AGENT_MOCKINGBIRD]}`);
2363
+ console.log(`health: ${result.health.ok ? "ok" : `failed (${result.health.status})`}`);
2364
+ return;
2365
+ }
2366
+
2367
+ if (result.mode === "uninstall") {
2368
+ console.log(`uninstall complete: ${result.purgeData ? `removed ${result.rootDir}` : `removed services/runtime, kept data in ${result.rootDir}`}`);
2369
+ console.log(`cli shim removed: ${result.removedShim ? "yes" : "no"}`);
2370
+ console.log(`opencode shim removed: ${result.removedOpencodeShim ? "yes" : "no"}`);
2371
+ return;
2372
+ }
2373
+
2374
+ if (result.mode === "onboard") {
2375
+ if (result.onboarding?.status === "completed") {
2376
+ console.log("onboard complete");
2377
+ if (result.onboarding.authAttempts > 0) {
2378
+ console.log(`provider auth attempts: ${result.onboarding.authAttempts}`);
2379
+ if (result.onboarding.authRefresh) {
2380
+ console.log(`provider auth refresh: ${result.onboarding.authRefresh.ok ? "ok" : "skipped/failed"}`);
2381
+ }
2382
+ }
2383
+ if (result.onboarding.selectedModel) {
2384
+ console.log(`default model: ${result.onboarding.selectedModel}`);
2385
+ }
2386
+ if (result.onboarding.memoryEmbedding) {
2387
+ if (result.onboarding.memoryEmbedding.configured === false) {
2388
+ console.log(`memory embeddings: skipped (${result.onboarding.memoryEmbedding.reason ?? "not-configured"})`);
2389
+ } else {
2390
+ console.log(`memory enabled: ${result.onboarding.memoryEmbedding.enabled ? "yes" : "no"}`);
2391
+ if (result.onboarding.memoryEmbedding.ollamaBaseUrl) {
2392
+ console.log(`memory ollama: ${result.onboarding.memoryEmbedding.ollamaBaseUrl}`);
2393
+ }
2394
+ if (result.onboarding.memoryEmbedding.embedModel) {
2395
+ console.log(`memory embedModel: ${result.onboarding.memoryEmbedding.embedModel}`);
2396
+ }
2397
+ }
2398
+ }
2399
+ if (result.onboarding.openclawMigration) {
2400
+ const migration = result.onboarding.openclawMigration;
2401
+ if (migration.attempted && !migration.skipped && migration.migration?.summary) {
2402
+ console.log("openclaw migration: completed");
2403
+ const summary = migration.migration.summary;
2404
+ console.log(
2405
+ `openclaw summary: discovered=${summary.discovered ?? 0}, copied=${summary.copied ?? 0}, merged=${summary.merged ?? 0}, skippedExisting=${summary.skippedExisting ?? 0}, skippedIdentical=${summary.skippedIdentical ?? 0}, skippedProtected=${summary.skippedProtected ?? 0}, failed=${summary.failed ?? 0}`,
2406
+ );
2407
+ if (migration.memorySync?.attempted) {
2408
+ console.log(`openclaw memory sync: ${migration.memorySync.completed ? "completed" : "failed"}`);
2409
+ }
2410
+ } else if (migration.error) {
2411
+ console.log(`openclaw migration: failed (${migration.error})`);
2412
+ } else {
2413
+ console.log(`openclaw migration: skipped (${migration.reason ?? "not-requested"})`);
2414
+ }
2415
+ }
2416
+ return;
2417
+ }
2418
+ if (result.onboarding?.status === "skipped") {
2419
+ console.log(`onboard skipped: ${result.onboarding.reason}`);
2420
+ return;
2421
+ }
2422
+ if (result.onboarding?.status === "error") {
2423
+ console.log(`onboard failed: ${result.onboarding.message}`);
2424
+ if (Array.isArray(result.onboarding.diagnostics)) {
2425
+ for (const line of result.onboarding.diagnostics) {
2426
+ console.log(line);
2427
+ }
2428
+ }
2429
+ return;
2430
+ }
2431
+ console.log("onboard complete");
2432
+ return;
2433
+ }
2434
+ }
2435
+
2436
+ function evaluateResult(result) {
2437
+ const isActive =
2438
+ result?.unitStates?.[UNIT_OPENCODE] === "active" && result?.unitStates?.[UNIT_AGENT_MOCKINGBIRD] === "active";
2439
+ if (result.mode === "install" || result.mode === "update") {
2440
+ if (
2441
+ !result.health?.ok ||
2442
+ !result.verify?.agentMockingbirdServiceOk ||
2443
+ !result.verify?.opencodeServiceOk ||
2444
+ !result.verify?.frontendAssetsOk
2445
+ ) {
2446
+ return 2;
2447
+ }
2448
+ return 0;
2449
+ }
2450
+ if (result.mode === "status" || result.mode === "restart" || result.mode === "start") {
2451
+ return isActive && result.health?.ok ? 0 : 2;
2452
+ }
2453
+ if (result.mode === "update-dry-run") {
2454
+ return result.precheck.npm && result.precheck.systemdUser ? 0 : 2;
2455
+ }
2456
+ if (result.mode === "stop") {
2457
+ const stopped =
2458
+ result?.unitStates?.[UNIT_OPENCODE] !== "active" && result?.unitStates?.[UNIT_AGENT_MOCKINGBIRD] !== "active";
2459
+ return stopped ? 0 : 2;
2460
+ }
2461
+ if (result.mode === "onboard") {
2462
+ return result.onboarding?.status === "error" ? 2 : 0;
2463
+ }
2464
+ return 0;
2465
+ }
2466
+
2467
+ async function main() {
2468
+ const args = parseArgs(process.argv.slice(2));
2469
+ if (!args.command || args.command === "help") {
2470
+ printHelp();
2471
+ if (!args.command) {
2472
+ console.log("\nHint: run `agent-mockingbird status` to check service health.");
2473
+ }
2474
+ return;
2475
+ }
2476
+
2477
+ let result;
2478
+ if (args.command === "install") {
2479
+ if (args.dryRun) {
2480
+ throw new Error("--dry-run is supported for `agent-mockingbird update` only.");
2481
+ }
2482
+ result = await installOrUpdate(args, "install");
2483
+ } else if (args.command === "update") {
2484
+ if (args.dryRun) {
2485
+ result = buildUpdateDryRun({
2486
+ args,
2487
+ paths: pathsFor({ rootDir: args.rootDir, scope: args.scope, userUnitDir: USER_UNIT_DIR }),
2488
+ });
2489
+ } else {
2490
+ result = await installOrUpdate(args, "update");
2491
+ }
2492
+ } else if (args.command === "onboard") {
2493
+ if (args.dryRun) {
2494
+ throw new Error("--dry-run is not applicable to `agent-mockingbird onboard`.");
2495
+ }
2496
+ result = await runOnboardingCommand(args);
2497
+ } else if (args.command === "status") {
2498
+ result = await status(args);
2499
+ } else if (args.command === "restart") {
2500
+ result = await manageService(args, "restart");
2501
+ } else if (args.command === "start") {
2502
+ result = await manageService(args, "start");
2503
+ } else if (args.command === "stop") {
2504
+ result = await manageService(args, "stop");
2505
+ } else if (args.command === "uninstall") {
2506
+ result = await uninstall(args);
2507
+ } else if (args.command === "import-openclaw-legacy") {
2508
+ throw new Error("`agent-mockingbird import openclaw ...` is deprecated. Use `agent-mockingbird onboard` and pick the OpenClaw flow.");
2509
+ } else {
2510
+ throw new Error(`Unknown command: ${args.command}`);
2511
+ }
2512
+
2513
+ printResult(result, args.json);
2514
+ process.exitCode = evaluateResult(result);
2515
+ }
2516
+
2517
+ if (import.meta.main) {
2518
+ await main().catch(error => {
2519
+ console.error(error instanceof Error ? error.message : String(error));
2520
+ process.exit(1);
2521
+ });
2522
+ }