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.
- package/.agents/skills/btca-cli/SKILL.md +64 -0
- package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
- package/.env.example +36 -0
- package/.githooks/pre-commit +33 -0
- package/.github/workflows/ci.yml +309 -0
- package/.opencode/bun.lock +18 -0
- package/.opencode/package.json +5 -0
- package/.opencode/tools/agent_type_manager.ts +100 -0
- package/.opencode/tools/config_manager.ts +87 -0
- package/.opencode/tools/cron_manager.ts +145 -0
- package/.opencode/tools/memory_get.ts +43 -0
- package/.opencode/tools/memory_remember.ts +53 -0
- package/.opencode/tools/memory_search.ts +48 -0
- package/AGENTS.md +126 -0
- package/MEMORY.md +2 -0
- package/README.md +451 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/agent-mockingbird.config.example.json +135 -0
- package/apps/server/package.json +32 -0
- package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
- package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
- package/apps/server/src/backend/agents/openclawImport.ts +797 -0
- package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
- package/apps/server/src/backend/agents/service.ts +10 -0
- package/apps/server/src/backend/config/example-config.test.ts +20 -0
- package/apps/server/src/backend/config/orchestration.ts +243 -0
- package/apps/server/src/backend/config/policy.ts +158 -0
- package/apps/server/src/backend/config/schema.test.ts +15 -0
- package/apps/server/src/backend/config/schema.ts +391 -0
- package/apps/server/src/backend/config/semantic.test.ts +34 -0
- package/apps/server/src/backend/config/semantic.ts +149 -0
- package/apps/server/src/backend/config/service.test.ts +75 -0
- package/apps/server/src/backend/config/service.ts +207 -0
- package/apps/server/src/backend/config/smoke.ts +77 -0
- package/apps/server/src/backend/config/store.test.ts +123 -0
- package/apps/server/src/backend/config/store.ts +581 -0
- package/apps/server/src/backend/config/testFixtures.ts +5 -0
- package/apps/server/src/backend/config/types.ts +56 -0
- package/apps/server/src/backend/contracts/events.ts +320 -0
- package/apps/server/src/backend/contracts/runtime.ts +111 -0
- package/apps/server/src/backend/cron/executor.ts +435 -0
- package/apps/server/src/backend/cron/repository.ts +170 -0
- package/apps/server/src/backend/cron/service.ts +660 -0
- package/apps/server/src/backend/cron/storage.ts +92 -0
- package/apps/server/src/backend/cron/types.ts +138 -0
- package/apps/server/src/backend/cron/utils.ts +351 -0
- package/apps/server/src/backend/db/client.ts +20 -0
- package/apps/server/src/backend/db/migrate.ts +40 -0
- package/apps/server/src/backend/db/repository.ts +1762 -0
- package/apps/server/src/backend/db/schema.ts +113 -0
- package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
- package/apps/server/src/backend/db/wipe.ts +13 -0
- package/apps/server/src/backend/defaults.ts +32 -0
- package/apps/server/src/backend/env.ts +48 -0
- package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
- package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
- package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
- package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
- package/apps/server/src/backend/heartbeat/service.ts +176 -0
- package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
- package/apps/server/src/backend/heartbeat/state.ts +167 -0
- package/apps/server/src/backend/heartbeat/types.ts +54 -0
- package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
- package/apps/server/src/backend/http/boundedQueue.ts +92 -0
- package/apps/server/src/backend/http/parsers.ts +40 -0
- package/apps/server/src/backend/http/router.ts +61 -0
- package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
- package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
- package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
- package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
- package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
- package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
- package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
- package/apps/server/src/backend/http/routes/index.ts +101 -0
- package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
- package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
- package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
- package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
- package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
- package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
- package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
- package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
- package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
- package/apps/server/src/backend/http/schemas.ts +64 -0
- package/apps/server/src/backend/http/sse.ts +144 -0
- package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
- package/apps/server/src/backend/logging/logger.ts +64 -0
- package/apps/server/src/backend/mcp/service.ts +326 -0
- package/apps/server/src/backend/memory/cli.ts +170 -0
- package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
- package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
- package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
- package/apps/server/src/backend/memory/qmdPort.ts +61 -0
- package/apps/server/src/backend/memory/records.test.ts +66 -0
- package/apps/server/src/backend/memory/records.ts +229 -0
- package/apps/server/src/backend/memory/service.ts +2012 -0
- package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
- package/apps/server/src/backend/memory/types.ts +104 -0
- package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
- package/apps/server/src/backend/opencode/client.ts +98 -0
- package/apps/server/src/backend/opencode/models.ts +41 -0
- package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
- package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
- package/apps/server/src/backend/paths.ts +57 -0
- package/apps/server/src/backend/prompts/service.ts +100 -0
- package/apps/server/src/backend/queue/queue.test.ts +189 -0
- package/apps/server/src/backend/queue/service.ts +177 -0
- package/apps/server/src/backend/queue/types.ts +39 -0
- package/apps/server/src/backend/run/service.ts +576 -0
- package/apps/server/src/backend/run/storage.ts +47 -0
- package/apps/server/src/backend/run/types.ts +44 -0
- package/apps/server/src/backend/runtime/errors.ts +61 -0
- package/apps/server/src/backend/runtime/index.ts +72 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
- package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
- package/apps/server/src/backend/skills/service.ts +442 -0
- package/apps/server/src/backend/workspace/resolve.ts +27 -0
- package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
- package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
- package/apps/server/src/cli/runtime-assets.mjs +269 -0
- package/apps/server/src/cli/runtime-assets.test.ts +52 -0
- package/apps/server/src/cli/runtime-layout.mjs +75 -0
- package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
- package/apps/server/src/cli/standaloneBuild.ts +19 -0
- package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
- package/apps/server/src/index.ts +178 -0
- package/apps/server/tsconfig.json +12 -0
- package/backlog.md +5 -0
- package/bin/agent-mockingbird +2522 -0
- package/bin/runtime-layout.mjs +75 -0
- package/build-bin.ts +34 -0
- package/build-cli.mjs +37 -0
- package/build.ts +40 -0
- package/bun-env.d.ts +11 -0
- package/bun.lock +888 -0
- package/bunfig.toml +2 -0
- package/components.json +21 -0
- package/config.json +130 -0
- package/deploy/RELEASE_INSTALL.md +112 -0
- package/deploy/docker-compose.yml +42 -0
- package/deploy/systemd/README.md +46 -0
- package/deploy/systemd/agent-mockingbird.service +28 -0
- package/deploy/systemd/opencode.service +25 -0
- package/docs/legacy-config-ui-reference.md +51 -0
- package/docs/memory-e2e-trace-2026-03-04.md +63 -0
- package/docs/memory-ops.md +96 -0
- package/docs/memory-runtime-contract.md +42 -0
- package/docs/memory-tuning-remote-2026-03-04.md +59 -0
- package/docs/opencode-rebase-workflow-plan.md +614 -0
- package/docs/opencode-startup-sync-plan.md +94 -0
- package/docs/vendor-opencode.md +41 -0
- package/drizzle/0000_famous_turbo.sql +49 -0
- package/drizzle/0001_cron_memory_aux.sql +160 -0
- package/drizzle/0002_runtime_session_bindings.sql +28 -0
- package/drizzle/0003_background_runs.sql +27 -0
- package/drizzle/0004_memory_open_write.sql +63 -0
- package/drizzle/0005_signal_channel.sql +47 -0
- package/drizzle/0006_usage_event_dimensions.sql +7 -0
- package/drizzle/meta/0000_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +55 -0
- package/drizzle.config.ts +14 -0
- package/eslint.config.mjs +77 -0
- package/knip.json +18 -0
- package/memory/2026-03-04.md +4 -0
- package/opencode.lock.json +16 -0
- package/package.json +67 -0
- package/packages/agent-mockingbird-installer/README.md +31 -0
- package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
- package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
- package/packages/agent-mockingbird-installer/package.json +23 -0
- package/packages/contracts/package.json +19 -0
- package/packages/contracts/src/agentTypes.ts +122 -0
- package/packages/contracts/src/cron.ts +146 -0
- package/packages/contracts/src/dashboard.ts +378 -0
- package/packages/contracts/src/index.ts +3 -0
- package/packages/contracts/tsconfig.json +4 -0
- package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
- package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
- package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
- package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
- package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
- package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
- package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
- package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
- package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
- package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
- package/runtime-assets/opencode-config/opencode.jsonc +25 -0
- package/runtime-assets/opencode-config/package.json +5 -0
- package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
- package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
- package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
- package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
- package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
- package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
- package/runtime-assets/workspace/AGENTS.md +56 -0
- package/runtime-assets/workspace/MEMORY.md +4 -0
- package/scripts/build-release-bundle.sh +66 -0
- package/scripts/check-ship.ts +383 -0
- package/scripts/dev-opencode.sh +17 -0
- package/scripts/dev-stack-opencode.sh +15 -0
- package/scripts/dev-stack.sh +61 -0
- package/scripts/install-systemd.sh +87 -0
- package/scripts/memory-e2e.sh +76 -0
- package/scripts/memory-trace-e2e.sh +141 -0
- package/scripts/migrate-opencode-env.ts +108 -0
- package/scripts/onboard/bootstrap.sh +32 -0
- package/scripts/opencode-swap.ts +78 -0
- package/scripts/opencode-sync.ts +715 -0
- package/scripts/runtime-assets-sync.mjs +83 -0
- package/scripts/setup-git-hooks.ts +39 -0
- package/tsconfig.json +45 -0
- package/tui.json +98 -0
- package/turbo.json +36 -0
- package/vendor/OPENCODE_VENDOR.md +13 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { CronService } from "../../cron/service";
|
|
2
|
+
import { cronJobCreateSchema, cronJobPatchSchema, cronManageSchema } from "../schemas";
|
|
3
|
+
|
|
4
|
+
export function createCronRoutes(cronService: CronService) {
|
|
5
|
+
return {
|
|
6
|
+
"/api/mockingbird/cron/health": {
|
|
7
|
+
GET: async () => {
|
|
8
|
+
try {
|
|
9
|
+
return Response.json({ health: await cronService.getHealth() });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : "Failed to load cron health";
|
|
12
|
+
return Response.json({ error: message }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"/api/mockingbird/cron/jobs": {
|
|
18
|
+
GET: async () => {
|
|
19
|
+
try {
|
|
20
|
+
return Response.json({ jobs: await cronService.listJobs() });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : "Failed to load cron jobs";
|
|
23
|
+
return Response.json({ error: message }, { status: 500 });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
POST: async (req: Request) => {
|
|
27
|
+
const parsed = cronJobCreateSchema.safeParse(await req.json());
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
return Response.json(
|
|
30
|
+
{ error: parsed.error.issues[0]?.message ?? "Invalid cron job payload" },
|
|
31
|
+
{ status: 400 },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const job = await cronService.createJob(parsed.data);
|
|
36
|
+
return Response.json({ job }, { status: 201 });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : "Failed to create cron job";
|
|
39
|
+
return Response.json({ error: message }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"/api/mockingbird/cron/jobs/:id": {
|
|
45
|
+
GET: async (req: Request & { params: { id: string } }) => {
|
|
46
|
+
const job = await cronService.getJob(req.params.id);
|
|
47
|
+
if (!job) {
|
|
48
|
+
return Response.json({ error: "Unknown cron job" }, { status: 404 });
|
|
49
|
+
}
|
|
50
|
+
return Response.json({ job });
|
|
51
|
+
},
|
|
52
|
+
PATCH: async (req: Request & { params: { id: string } }) => {
|
|
53
|
+
const parsed = cronJobPatchSchema.safeParse(await req.json());
|
|
54
|
+
if (!parsed.success) {
|
|
55
|
+
return Response.json(
|
|
56
|
+
{ error: parsed.error.issues[0]?.message ?? "Invalid cron job patch payload" },
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const job = await cronService.updateJob(req.params.id, parsed.data);
|
|
62
|
+
return Response.json({ job });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : "Failed to update cron job";
|
|
65
|
+
const status = message.startsWith("Unknown cron job:") ? 404 : 400;
|
|
66
|
+
return Response.json({ error: message }, { status });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
DELETE: async (req: Request & { params: { id: string } }) => {
|
|
70
|
+
const result = await cronService.deleteJob(req.params.id);
|
|
71
|
+
if (!result.removed) {
|
|
72
|
+
return Response.json({ error: "Unknown cron job" }, { status: 404 });
|
|
73
|
+
}
|
|
74
|
+
return Response.json(result);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"/api/mockingbird/cron/jobs/:id/run": {
|
|
79
|
+
POST: async (req: Request & { params: { id: string } }) => {
|
|
80
|
+
try {
|
|
81
|
+
const run = await cronService.runJobNow(req.params.id);
|
|
82
|
+
return Response.json(run, { status: run.queued ? 202 : 409 });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : "Failed to queue cron run";
|
|
85
|
+
const status = message.startsWith("Unknown cron job:") ? 404 : 400;
|
|
86
|
+
return Response.json({ error: message }, { status });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
"/api/mockingbird/cron/instances": {
|
|
92
|
+
GET: async (req: Request) => {
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(req.url);
|
|
95
|
+
const jobId = url.searchParams.get("jobId")?.trim() || undefined;
|
|
96
|
+
const limitRaw = url.searchParams.get("limit");
|
|
97
|
+
const limit = limitRaw ? Number(limitRaw) : undefined;
|
|
98
|
+
const instances = await cronService.listInstances({
|
|
99
|
+
jobId,
|
|
100
|
+
limit: typeof limit === "number" && Number.isFinite(limit) ? limit : undefined,
|
|
101
|
+
});
|
|
102
|
+
return Response.json({ instances });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : "Failed to load cron instances";
|
|
105
|
+
return Response.json({ error: message }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
"/api/mockingbird/cron/instances/:id/steps": {
|
|
111
|
+
GET: async (req: Request & { params: { id: string } }) => {
|
|
112
|
+
try {
|
|
113
|
+
const steps = await cronService.listSteps(req.params.id);
|
|
114
|
+
return Response.json({ steps });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : "Failed to load cron steps";
|
|
117
|
+
return Response.json({ error: message }, { status: 400 });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
"/api/mockingbird/cron/manage": {
|
|
123
|
+
POST: async (req: Request) => {
|
|
124
|
+
const parsed = cronManageSchema.safeParse(await req.json());
|
|
125
|
+
if (!parsed.success) {
|
|
126
|
+
return Response.json(
|
|
127
|
+
{ error: parsed.error.issues[0]?.message ?? "Invalid cron manage request" },
|
|
128
|
+
{ status: 400 },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const command = parsed.data;
|
|
133
|
+
switch (command.action) {
|
|
134
|
+
case "list_jobs":
|
|
135
|
+
return Response.json({ ok: true, action: command.action, jobs: await cronService.listJobs() });
|
|
136
|
+
case "health":
|
|
137
|
+
return Response.json({ ok: true, action: command.action, health: await cronService.getHealth() });
|
|
138
|
+
case "get_job": {
|
|
139
|
+
const job = await cronService.getJob(command.jobId);
|
|
140
|
+
if (!job) {
|
|
141
|
+
return Response.json({ error: "Unknown cron job" }, { status: 404 });
|
|
142
|
+
}
|
|
143
|
+
return Response.json({ ok: true, action: command.action, job });
|
|
144
|
+
}
|
|
145
|
+
case "create_job":
|
|
146
|
+
return Response.json(
|
|
147
|
+
{ ok: true, action: command.action, job: await cronService.createJob(command.job) },
|
|
148
|
+
{ status: 201 },
|
|
149
|
+
);
|
|
150
|
+
case "upsert_job": {
|
|
151
|
+
const upserted = await cronService.upsertJob(command.job);
|
|
152
|
+
return Response.json({
|
|
153
|
+
ok: true,
|
|
154
|
+
action: command.action,
|
|
155
|
+
created: upserted.created,
|
|
156
|
+
job: upserted.job,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
case "update_job":
|
|
160
|
+
return Response.json({
|
|
161
|
+
ok: true,
|
|
162
|
+
action: command.action,
|
|
163
|
+
job: await cronService.updateJob(command.jobId, command.patch),
|
|
164
|
+
});
|
|
165
|
+
case "enable_job":
|
|
166
|
+
return Response.json({
|
|
167
|
+
ok: true,
|
|
168
|
+
action: command.action,
|
|
169
|
+
job: await cronService.updateJob(command.jobId, { enabled: true }),
|
|
170
|
+
});
|
|
171
|
+
case "disable_job":
|
|
172
|
+
return Response.json({
|
|
173
|
+
ok: true,
|
|
174
|
+
action: command.action,
|
|
175
|
+
job: await cronService.updateJob(command.jobId, { enabled: false }),
|
|
176
|
+
});
|
|
177
|
+
case "describe_contract":
|
|
178
|
+
return Response.json({
|
|
179
|
+
ok: true,
|
|
180
|
+
action: command.action,
|
|
181
|
+
contract: cronService.describeContract(),
|
|
182
|
+
});
|
|
183
|
+
case "delete_job":
|
|
184
|
+
return Response.json({
|
|
185
|
+
ok: true,
|
|
186
|
+
action: command.action,
|
|
187
|
+
...(await cronService.deleteJob(command.jobId)),
|
|
188
|
+
});
|
|
189
|
+
case "run_job_now":
|
|
190
|
+
return Response.json({
|
|
191
|
+
ok: true,
|
|
192
|
+
action: command.action,
|
|
193
|
+
...(await cronService.runJobNow(command.jobId)),
|
|
194
|
+
});
|
|
195
|
+
case "list_instances":
|
|
196
|
+
return Response.json({
|
|
197
|
+
ok: true,
|
|
198
|
+
action: command.action,
|
|
199
|
+
instances: await cronService.listInstances({
|
|
200
|
+
jobId: command.jobId,
|
|
201
|
+
limit: command.limit,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
case "list_steps":
|
|
205
|
+
return Response.json({
|
|
206
|
+
ok: true,
|
|
207
|
+
action: command.action,
|
|
208
|
+
steps: await cronService.listSteps(command.instanceId),
|
|
209
|
+
});
|
|
210
|
+
default:
|
|
211
|
+
return Response.json({ error: "Unsupported cron action" }, { status: 400 });
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : "Cron manage request failed";
|
|
215
|
+
const status = message.startsWith("Unknown cron job:") ? 404 : 400;
|
|
216
|
+
return Response.json({ error: message }, { status });
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { buildWorkspaceBootstrapPromptContext } from "../../agents/bootstrapContext";
|
|
2
|
+
import { getOpencodeAgentStorageInfo } from "../../agents/opencodeConfig";
|
|
3
|
+
import { applyConfigPatch, ConfigApplyError, getConfigSnapshot } from "../../config/service";
|
|
4
|
+
import type { RuntimeEngine } from "../../contracts/runtime";
|
|
5
|
+
import {
|
|
6
|
+
createSession,
|
|
7
|
+
getDashboardBootstrap,
|
|
8
|
+
getSessionById,
|
|
9
|
+
listMessagesForSession,
|
|
10
|
+
listSessions,
|
|
11
|
+
setSessionModel,
|
|
12
|
+
} from "../../db/repository";
|
|
13
|
+
import { listOpencodeModelOptions } from "../../opencode/models";
|
|
14
|
+
import { getRuntimeStartupInfo } from "../../runtime";
|
|
15
|
+
import { resolveWorkspaceAlignment } from "../../workspace/resolve";
|
|
16
|
+
|
|
17
|
+
function parseModelSelection(model: string, defaultProviderId: string) {
|
|
18
|
+
const trimmed = model.trim();
|
|
19
|
+
if (!trimmed) return null;
|
|
20
|
+
if (!trimmed.includes("/")) {
|
|
21
|
+
const providerId = defaultProviderId.trim();
|
|
22
|
+
if (!providerId) return null;
|
|
23
|
+
return { providerId, modelId: trimmed };
|
|
24
|
+
}
|
|
25
|
+
const [providerPart = "", ...rest] = trimmed.split("/");
|
|
26
|
+
const providerId = providerPart.trim();
|
|
27
|
+
const modelId = rest.join("/").trim();
|
|
28
|
+
if (!providerId || !modelId) return null;
|
|
29
|
+
return { providerId, modelId };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toQualifiedModel(providerId: string, modelId: string) {
|
|
33
|
+
const provider = providerId.trim();
|
|
34
|
+
const model = modelId.trim();
|
|
35
|
+
if (!provider || !model) return "";
|
|
36
|
+
return `${provider}/${model}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toConfigErrorResponse(error: unknown) {
|
|
40
|
+
if (error instanceof ConfigApplyError) {
|
|
41
|
+
if (error.stage === "conflict") {
|
|
42
|
+
return {
|
|
43
|
+
status: 409,
|
|
44
|
+
body: { error: error.message, stage: error.stage },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (error.stage === "request" || error.stage === "schema") {
|
|
48
|
+
return {
|
|
49
|
+
status: 400,
|
|
50
|
+
body: { error: error.message, stage: error.stage, details: error.details },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (error.stage === "semantic" || error.stage === "smoke" || error.stage === "policy") {
|
|
54
|
+
return {
|
|
55
|
+
status: 422,
|
|
56
|
+
body: { error: error.message, stage: error.stage, details: error.details },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: 500,
|
|
61
|
+
body: { error: error.message, stage: error.stage },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
status: 500,
|
|
67
|
+
body: {
|
|
68
|
+
error: error instanceof Error ? error.message : "Failed to update runtime model defaults",
|
|
69
|
+
stage: "unknown",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createDashboardRoutes(runtime: RuntimeEngine) {
|
|
75
|
+
return {
|
|
76
|
+
"/api/health": {
|
|
77
|
+
GET: () =>
|
|
78
|
+
Response.json({
|
|
79
|
+
status: "ok",
|
|
80
|
+
now: new Date().toISOString(),
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
"/api/dashboard/bootstrap": {
|
|
85
|
+
GET: () => Response.json(getDashboardBootstrap()),
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
"/api/runtime/health": {
|
|
89
|
+
GET: async (req: Request) => {
|
|
90
|
+
if (!runtime.checkHealth) {
|
|
91
|
+
return Response.json({ error: "Runtime health checks are not supported by this runtime" }, { status: 501 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const force = (() => {
|
|
95
|
+
const value = new URL(req.url).searchParams.get("force");
|
|
96
|
+
if (!value) return false;
|
|
97
|
+
const normalized = value.trim().toLowerCase();
|
|
98
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const health = await runtime.checkHealth({ force });
|
|
103
|
+
return Response.json({ health }, { status: health.ok ? 200 : 503 });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : "Runtime health check failed";
|
|
106
|
+
return Response.json({ error: message }, { status: 503 });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
"/api/runtime/info": {
|
|
112
|
+
GET: () => {
|
|
113
|
+
const snapshot = getConfigSnapshot();
|
|
114
|
+
const runtimeInfo = getRuntimeStartupInfo();
|
|
115
|
+
const storage = getOpencodeAgentStorageInfo();
|
|
116
|
+
const bootstrap = buildWorkspaceBootstrapPromptContext();
|
|
117
|
+
const workspaceAlignment = resolveWorkspaceAlignment(snapshot.config);
|
|
118
|
+
return Response.json({
|
|
119
|
+
configAuthority: {
|
|
120
|
+
source: "agent-mockingbird-config-json",
|
|
121
|
+
path: snapshot.path,
|
|
122
|
+
hash: snapshot.hash,
|
|
123
|
+
},
|
|
124
|
+
opencode: {
|
|
125
|
+
...runtimeInfo.opencode,
|
|
126
|
+
workspaceDirectory: storage.workspaceDirectory,
|
|
127
|
+
configDirectory: storage.configDirectory,
|
|
128
|
+
effectiveConfigPath: storage.configFilePath,
|
|
129
|
+
persistenceMode: storage.persistenceMode,
|
|
130
|
+
projection: {
|
|
131
|
+
source: "agent-mockingbird-config-json",
|
|
132
|
+
syncs: ["small_model", "skills.paths", "mcp", "agent"],
|
|
133
|
+
},
|
|
134
|
+
identity: bootstrap.identity,
|
|
135
|
+
bootstrap: {
|
|
136
|
+
mode: bootstrap.mode,
|
|
137
|
+
files: bootstrap.files.map(file => ({
|
|
138
|
+
name: file.name,
|
|
139
|
+
path: file.path,
|
|
140
|
+
missing: file.missing,
|
|
141
|
+
truncated: file.truncated,
|
|
142
|
+
originalLength: file.originalLength,
|
|
143
|
+
injectedLength: file.content.length,
|
|
144
|
+
})),
|
|
145
|
+
},
|
|
146
|
+
workspace: {
|
|
147
|
+
aligned: workspaceAlignment.aligned,
|
|
148
|
+
opencodeDirectoryExplicit: workspaceAlignment.opencodeDirectoryExplicit,
|
|
149
|
+
opencodeWorkspaceDir: workspaceAlignment.opencodeWorkspaceDir,
|
|
150
|
+
opencodeConfigDir: storage.configDirectory,
|
|
151
|
+
memoryWorkspaceDir: workspaceAlignment.memoryWorkspaceDir,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
"/api/sessions": {
|
|
159
|
+
GET: () => Response.json({ sessions: listSessions() }),
|
|
160
|
+
POST: async (req: Request) => {
|
|
161
|
+
const body = (await req.json()) as { title?: string; model?: string } | null;
|
|
162
|
+
const session = createSession({
|
|
163
|
+
title: body?.title,
|
|
164
|
+
model: body?.model,
|
|
165
|
+
});
|
|
166
|
+
return Response.json({ session }, { status: 201 });
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"/api/sessions/:id/messages": (req: Request & { params: { id: string } }) => {
|
|
171
|
+
const sessionId = req.params.id;
|
|
172
|
+
const session = getSessionById(sessionId);
|
|
173
|
+
if (!session) {
|
|
174
|
+
return Response.json({ error: "Unknown session" }, { status: 404 });
|
|
175
|
+
}
|
|
176
|
+
return (async () => {
|
|
177
|
+
if (runtime.syncSessionMessages) {
|
|
178
|
+
try {
|
|
179
|
+
await runtime.syncSessionMessages(sessionId);
|
|
180
|
+
} catch {
|
|
181
|
+
// degrade gracefully; return best-effort local transcript
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return Response.json({
|
|
185
|
+
sessionId,
|
|
186
|
+
messages: listMessagesForSession(sessionId),
|
|
187
|
+
});
|
|
188
|
+
})();
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
"/api/sessions/:id/model": {
|
|
192
|
+
PUT: async (req: Request & { params: { id: string } }) => {
|
|
193
|
+
const sessionId = req.params.id;
|
|
194
|
+
const body = (await req.json()) as { model?: string };
|
|
195
|
+
const model = body.model?.trim();
|
|
196
|
+
if (!model) {
|
|
197
|
+
return Response.json({ error: "model is required" }, { status: 400 });
|
|
198
|
+
}
|
|
199
|
+
if (!getSessionById(sessionId)) {
|
|
200
|
+
return Response.json({ error: "Unknown session" }, { status: 404 });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const session = setSessionModel(sessionId, model);
|
|
204
|
+
if (!session) {
|
|
205
|
+
return Response.json({ error: "Unknown session" }, { status: 404 });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const snapshot = getConfigSnapshot();
|
|
209
|
+
const currentRuntimeDefaultModel = toQualifiedModel(
|
|
210
|
+
snapshot.config.runtime.opencode.providerId,
|
|
211
|
+
snapshot.config.runtime.opencode.modelId,
|
|
212
|
+
);
|
|
213
|
+
const parsed = parseModelSelection(model, snapshot.config.runtime.opencode.providerId);
|
|
214
|
+
if (!parsed) {
|
|
215
|
+
return Response.json({
|
|
216
|
+
session,
|
|
217
|
+
runtimeDefaultModel: currentRuntimeDefaultModel,
|
|
218
|
+
sessionMatchesRuntimeDefault: session.model === currentRuntimeDefaultModel,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const configResult = await applyConfigPatch({
|
|
224
|
+
expectedHash: snapshot.hash,
|
|
225
|
+
runSmokeTest: false,
|
|
226
|
+
patch: {
|
|
227
|
+
runtime: {
|
|
228
|
+
opencode: {
|
|
229
|
+
providerId: parsed.providerId,
|
|
230
|
+
modelId: parsed.modelId,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const runtimeDefaultModel = toQualifiedModel(parsed.providerId, parsed.modelId);
|
|
237
|
+
return Response.json({
|
|
238
|
+
session,
|
|
239
|
+
configHash: configResult.snapshot.hash,
|
|
240
|
+
runtimeDefaultModel,
|
|
241
|
+
sessionMatchesRuntimeDefault: session.model === runtimeDefaultModel,
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const details = toConfigErrorResponse(error);
|
|
245
|
+
return Response.json({
|
|
246
|
+
session,
|
|
247
|
+
configError: details.body.error,
|
|
248
|
+
configStage: details.body.stage,
|
|
249
|
+
runtimeDefaultModel: currentRuntimeDefaultModel,
|
|
250
|
+
sessionMatchesRuntimeDefault: session.model === currentRuntimeDefaultModel,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
"/api/runtime/default-model": {
|
|
257
|
+
PUT: async (req: Request) => {
|
|
258
|
+
const body = (await req.json()) as { model?: string };
|
|
259
|
+
const model = body.model?.trim();
|
|
260
|
+
if (!model) {
|
|
261
|
+
return Response.json({ error: "model is required" }, { status: 400 });
|
|
262
|
+
}
|
|
263
|
+
const snapshot = getConfigSnapshot();
|
|
264
|
+
const parsed = parseModelSelection(model, snapshot.config.runtime.opencode.providerId);
|
|
265
|
+
if (!parsed) {
|
|
266
|
+
return Response.json({ error: "Invalid model format" }, { status: 400 });
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const configResult = await applyConfigPatch({
|
|
270
|
+
expectedHash: snapshot.hash,
|
|
271
|
+
runSmokeTest: false,
|
|
272
|
+
patch: {
|
|
273
|
+
runtime: {
|
|
274
|
+
opencode: {
|
|
275
|
+
providerId: parsed.providerId,
|
|
276
|
+
modelId: parsed.modelId,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
const runtimeDefaultModel = toQualifiedModel(parsed.providerId, parsed.modelId);
|
|
282
|
+
return Response.json({
|
|
283
|
+
runtimeDefaultModel,
|
|
284
|
+
configHash: configResult.snapshot.hash,
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const details = toConfigErrorResponse(error);
|
|
288
|
+
return Response.json(
|
|
289
|
+
{ error: details.body.error, stage: details.body.stage },
|
|
290
|
+
{ status: details.status },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
"/api/opencode/models": {
|
|
297
|
+
GET: async () => {
|
|
298
|
+
try {
|
|
299
|
+
const models = await listOpencodeModelOptions();
|
|
300
|
+
return Response.json({ models });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : "Failed to load models";
|
|
303
|
+
return Response.json({ models: [], error: message }, { status: 502 });
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createHeartbeatRoutes } from "./heartbeatRoutes";
|
|
4
|
+
|
|
5
|
+
describe("heartbeat routes", () => {
|
|
6
|
+
test("GET /api/mockingbird/heartbeat returns session metadata", async () => {
|
|
7
|
+
const routes = createHeartbeatRoutes({
|
|
8
|
+
getStatus: () => ({
|
|
9
|
+
config: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
interval: "30m",
|
|
12
|
+
agentId: "build",
|
|
13
|
+
model: "opencode/big-pickle",
|
|
14
|
+
prompt: "Read HEARTBEAT.md",
|
|
15
|
+
ackMaxChars: 300,
|
|
16
|
+
activeHours: null,
|
|
17
|
+
},
|
|
18
|
+
state: {
|
|
19
|
+
sessionId: "session-heartbeat",
|
|
20
|
+
running: false,
|
|
21
|
+
lastRunAt: "2026-03-20T00:00:00.000Z",
|
|
22
|
+
lastResult: "acknowledged",
|
|
23
|
+
lastResponse: null,
|
|
24
|
+
lastError: null,
|
|
25
|
+
updatedAt: "2026-03-20T00:00:00.000Z",
|
|
26
|
+
},
|
|
27
|
+
sessionTitle: "Heartbeat",
|
|
28
|
+
nextDueAt: "2026-03-20T00:30:00.000Z",
|
|
29
|
+
}),
|
|
30
|
+
} as never);
|
|
31
|
+
|
|
32
|
+
const response = await routes["/api/mockingbird/heartbeat"].GET();
|
|
33
|
+
expect(response.status).toBe(200);
|
|
34
|
+
|
|
35
|
+
const payload = (await response.json()) as {
|
|
36
|
+
heartbeat: { state: { sessionId: string | null }; sessionTitle: string | null };
|
|
37
|
+
};
|
|
38
|
+
expect(payload.heartbeat.state.sessionId).toBe("session-heartbeat");
|
|
39
|
+
expect(payload.heartbeat.sessionTitle).toBe("Heartbeat");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HeartbeatRuntimeService } from "../../heartbeat/runtimeService";
|
|
2
|
+
|
|
3
|
+
export function createHeartbeatRoutes(heartbeatService: HeartbeatRuntimeService) {
|
|
4
|
+
return {
|
|
5
|
+
"/api/mockingbird/heartbeat": {
|
|
6
|
+
GET: async () => {
|
|
7
|
+
try {
|
|
8
|
+
return Response.json({ heartbeat: heartbeatService.getStatus() });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : "Failed to load heartbeat status";
|
|
11
|
+
return Response.json({ error: message }, { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"/api/mockingbird/heartbeat/run": {
|
|
17
|
+
POST: async () => {
|
|
18
|
+
try {
|
|
19
|
+
const heartbeat = await heartbeatService.runNow();
|
|
20
|
+
return Response.json({ heartbeat }, { status: 202 });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : "Failed to run heartbeat";
|
|
23
|
+
return Response.json({ error: message }, { status: 500 });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|