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,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { testing } from "./agent-mockingbird.mjs";
|
|
7
|
+
|
|
8
|
+
describe("agent-mockingbird CLI onboarding diagnostics", () => {
|
|
9
|
+
test("builds actionable diagnostics when runtime model discovery is empty", () => {
|
|
10
|
+
const diagnostics = testing.buildEmptyModelDiscoveryDiagnostics({
|
|
11
|
+
workspaceDir: "/var/home/agent-mockingbird/.agent-mockingbird/workspace",
|
|
12
|
+
currentModel: "opencode/big-pickle",
|
|
13
|
+
authAttempts: 1,
|
|
14
|
+
authSuccess: true,
|
|
15
|
+
authRefresh: {
|
|
16
|
+
ok: true,
|
|
17
|
+
message: "opencode.service restarted to refresh provider credentials.",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(diagnostics[0]).toBe("No runtime models were discovered after provider setup.");
|
|
22
|
+
expect(diagnostics).toContain("Current runtime default: opencode/big-pickle");
|
|
23
|
+
expect(diagnostics).toContain("Provider auth attempts: 1 (at least one succeeded)");
|
|
24
|
+
expect(diagnostics).toContain("OpenCode auth refresh: opencode.service restarted to refresh provider credentials.");
|
|
25
|
+
expect(diagnostics).toContain(
|
|
26
|
+
"- curl -sS http://127.0.0.1:3001/api/opencode/models",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("agent-mockingbird CLI opencode version resolution", () => {
|
|
32
|
+
test("prefers explicit env version", () => {
|
|
33
|
+
const version = testing.readOpenCodePackageVersion({
|
|
34
|
+
env: { AGENT_MOCKINGBIRD_OPENCODE_VERSION: "1.2.99" },
|
|
35
|
+
argv: ["bun", "/tmp/fake-bin/agent-mockingbird"],
|
|
36
|
+
moduleDir: "/tmp/fake-module",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(version).toBe("1.2.99");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("resolves version from installed package root", () => {
|
|
43
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-cli-test-"));
|
|
44
|
+
const appDir = path.join(tempRoot, "npm", "lib", "node_modules", "@waffleophagus", "agent-mockingbird");
|
|
45
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(appDir, "opencode.lock.json"),
|
|
48
|
+
JSON.stringify({ packageVersion: "1.2.27" }),
|
|
49
|
+
"utf8",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const version = testing.readOpenCodePackageVersion({
|
|
54
|
+
paths: {
|
|
55
|
+
agentMockingbirdAppDirGlobal: appDir,
|
|
56
|
+
agentMockingbirdAppDirLocal: path.join(tempRoot, "missing-local"),
|
|
57
|
+
},
|
|
58
|
+
env: {},
|
|
59
|
+
argv: ["bun", "/tmp/fake-bin/agent-mockingbird"],
|
|
60
|
+
moduleDir: "/tmp/fake-module",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(version).toBe("1.2.27");
|
|
64
|
+
} finally {
|
|
65
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const STATE_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
function ensureDir(dirPath) {
|
|
8
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeRelativePath(filePath) {
|
|
12
|
+
return filePath.split(path.sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listRelativeFiles(rootDir, currentDir = rootDir) {
|
|
16
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
17
|
+
const files = [];
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
files.push(...listRelativeFiles(rootDir, absolutePath));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!entry.isFile()) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
files.push(normalizeRelativePath(path.relative(rootDir, absolutePath)));
|
|
28
|
+
}
|
|
29
|
+
files.sort((left, right) => left.localeCompare(right));
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hashFile(filePath) {
|
|
34
|
+
const data = fs.readFileSync(filePath);
|
|
35
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeReadState(stateFilePath) {
|
|
39
|
+
if (!fs.existsSync(stateFilePath)) {
|
|
40
|
+
return { version: STATE_VERSION, files: {} };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(fs.readFileSync(stateFilePath, "utf8"));
|
|
44
|
+
const files = typeof parsed?.files === "object" && parsed.files ? parsed.files : {};
|
|
45
|
+
return {
|
|
46
|
+
version: Number.isInteger(parsed?.version) ? parsed.version : STATE_VERSION,
|
|
47
|
+
files,
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return { version: STATE_VERSION, files: {} };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeState(stateFilePath, state) {
|
|
55
|
+
ensureDir(path.dirname(stateFilePath));
|
|
56
|
+
fs.writeFileSync(stateFilePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function timestampForBackup(now = new Date()) {
|
|
60
|
+
const year = String(now.getUTCFullYear());
|
|
61
|
+
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
62
|
+
const day = String(now.getUTCDate()).padStart(2, "0");
|
|
63
|
+
const hours = String(now.getUTCHours()).padStart(2, "0");
|
|
64
|
+
const minutes = String(now.getUTCMinutes()).padStart(2, "0");
|
|
65
|
+
const seconds = String(now.getUTCSeconds()).padStart(2, "0");
|
|
66
|
+
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeBackupPath(targetFilePath) {
|
|
70
|
+
const stamp = timestampForBackup();
|
|
71
|
+
let candidate = `${targetFilePath}.backup-${stamp}`;
|
|
72
|
+
let suffix = 1;
|
|
73
|
+
while (fs.existsSync(candidate)) {
|
|
74
|
+
candidate = `${targetFilePath}.backup-${stamp}-${suffix}`;
|
|
75
|
+
suffix += 1;
|
|
76
|
+
}
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readDecision(value) {
|
|
81
|
+
if (value === "keep-local" || value === "use-packaged") {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return "use-packaged";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function syncRuntimeWorkspaceAssets(input) {
|
|
88
|
+
const mode = input?.mode === "update" ? "update" : "install";
|
|
89
|
+
const sourceWorkspaceDir = path.resolve(String(input?.sourceWorkspaceDir ?? ""));
|
|
90
|
+
const targetWorkspaceDir = path.resolve(String(input?.targetWorkspaceDir ?? ""));
|
|
91
|
+
const stateFilePath = path.resolve(String(input?.stateFilePath ?? ""));
|
|
92
|
+
|
|
93
|
+
if (!sourceWorkspaceDir || !fs.existsSync(sourceWorkspaceDir)) {
|
|
94
|
+
throw new Error(`runtime asset source directory missing: ${sourceWorkspaceDir}`);
|
|
95
|
+
}
|
|
96
|
+
if (!targetWorkspaceDir) {
|
|
97
|
+
throw new Error("runtime asset target directory is required");
|
|
98
|
+
}
|
|
99
|
+
if (!stateFilePath) {
|
|
100
|
+
throw new Error("runtime asset state file path is required");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const interactive = Boolean(input?.interactive);
|
|
104
|
+
const logger = typeof input?.logger === "function" ? input.logger : null;
|
|
105
|
+
const onConflict = typeof input?.onConflict === "function" ? input.onConflict : null;
|
|
106
|
+
|
|
107
|
+
ensureDir(targetWorkspaceDir);
|
|
108
|
+
const previousState = safeReadState(stateFilePath);
|
|
109
|
+
const relativeFiles = listRelativeFiles(sourceWorkspaceDir);
|
|
110
|
+
const nowIso = new Date().toISOString();
|
|
111
|
+
|
|
112
|
+
const summary = {
|
|
113
|
+
mode,
|
|
114
|
+
source: sourceWorkspaceDir,
|
|
115
|
+
target: targetWorkspaceDir,
|
|
116
|
+
stateFile: stateFilePath,
|
|
117
|
+
scannedFiles: relativeFiles.length,
|
|
118
|
+
copied: 0,
|
|
119
|
+
overwritten: 0,
|
|
120
|
+
unchanged: 0,
|
|
121
|
+
keptLocal: 0,
|
|
122
|
+
conflicts: 0,
|
|
123
|
+
backupsCreated: 0,
|
|
124
|
+
removed: 0,
|
|
125
|
+
conflictResolutions: [],
|
|
126
|
+
backups: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const nextState = {
|
|
130
|
+
version: STATE_VERSION,
|
|
131
|
+
updatedAt: nowIso,
|
|
132
|
+
files: {},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const relativePath of relativeFiles) {
|
|
136
|
+
const sourceFilePath = path.join(sourceWorkspaceDir, relativePath);
|
|
137
|
+
const targetFilePath = path.join(targetWorkspaceDir, relativePath);
|
|
138
|
+
const sourceHash = hashFile(sourceFilePath);
|
|
139
|
+
const previous = previousState.files?.[relativePath] ?? null;
|
|
140
|
+
|
|
141
|
+
const targetExists = fs.existsSync(targetFilePath);
|
|
142
|
+
const targetHash = targetExists ? hashFile(targetFilePath) : null;
|
|
143
|
+
const previousSourceHash = typeof previous?.sourceHash === "string" ? previous.sourceHash : null;
|
|
144
|
+
const previousAppliedHash = typeof previous?.appliedHash === "string" ? previous.appliedHash : null;
|
|
145
|
+
|
|
146
|
+
const packageChanged = previousSourceHash === null || previousSourceHash !== sourceHash;
|
|
147
|
+
const localChanged = previousAppliedHash === null ? false : targetHash !== previousAppliedHash;
|
|
148
|
+
|
|
149
|
+
let appliedHash = targetHash;
|
|
150
|
+
|
|
151
|
+
if (!targetExists) {
|
|
152
|
+
ensureDir(path.dirname(targetFilePath));
|
|
153
|
+
fs.copyFileSync(sourceFilePath, targetFilePath);
|
|
154
|
+
summary.copied += 1;
|
|
155
|
+
appliedHash = hashFile(targetFilePath);
|
|
156
|
+
if (logger) logger(`runtime-assets: copied ${relativePath}`);
|
|
157
|
+
} else if (mode === "install") {
|
|
158
|
+
fs.copyFileSync(sourceFilePath, targetFilePath);
|
|
159
|
+
summary.overwritten += 1;
|
|
160
|
+
appliedHash = hashFile(targetFilePath);
|
|
161
|
+
if (logger) logger(`runtime-assets: overwritten ${relativePath}`);
|
|
162
|
+
} else if (!packageChanged) {
|
|
163
|
+
summary.unchanged += 1;
|
|
164
|
+
if (logger) logger(`runtime-assets: unchanged ${relativePath}`);
|
|
165
|
+
} else if (!localChanged) {
|
|
166
|
+
fs.copyFileSync(sourceFilePath, targetFilePath);
|
|
167
|
+
summary.overwritten += 1;
|
|
168
|
+
appliedHash = hashFile(targetFilePath);
|
|
169
|
+
if (logger) logger(`runtime-assets: updated ${relativePath}`);
|
|
170
|
+
} else {
|
|
171
|
+
summary.conflicts += 1;
|
|
172
|
+
const conflictInput = {
|
|
173
|
+
relativePath,
|
|
174
|
+
targetFilePath,
|
|
175
|
+
sourceFilePath,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const decision =
|
|
179
|
+
interactive && onConflict ? readDecision(await onConflict(conflictInput)) : "use-packaged";
|
|
180
|
+
|
|
181
|
+
if (decision === "keep-local") {
|
|
182
|
+
summary.keptLocal += 1;
|
|
183
|
+
summary.conflictResolutions.push({ path: relativePath, decision: "keep-local" });
|
|
184
|
+
if (logger) logger(`runtime-assets: kept local ${relativePath}`);
|
|
185
|
+
} else {
|
|
186
|
+
if (!interactive) {
|
|
187
|
+
const backupPath = makeBackupPath(targetFilePath);
|
|
188
|
+
fs.copyFileSync(targetFilePath, backupPath);
|
|
189
|
+
summary.backupsCreated += 1;
|
|
190
|
+
summary.backups.push({ path: relativePath, backupPath });
|
|
191
|
+
if (logger) logger(`runtime-assets: backup ${relativePath} -> ${backupPath}`);
|
|
192
|
+
}
|
|
193
|
+
fs.copyFileSync(sourceFilePath, targetFilePath);
|
|
194
|
+
summary.overwritten += 1;
|
|
195
|
+
appliedHash = hashFile(targetFilePath);
|
|
196
|
+
summary.conflictResolutions.push({ path: relativePath, decision: "use-packaged" });
|
|
197
|
+
if (logger) logger(`runtime-assets: replaced ${relativePath}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
nextState.files[relativePath] = {
|
|
202
|
+
sourceHash,
|
|
203
|
+
appliedHash,
|
|
204
|
+
updatedAt: nowIso,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const removedPaths = Object.keys(previousState.files ?? {})
|
|
209
|
+
.filter(relativePath => !relativeFiles.includes(relativePath))
|
|
210
|
+
.sort((left, right) => left.localeCompare(right));
|
|
211
|
+
|
|
212
|
+
for (const relativePath of removedPaths) {
|
|
213
|
+
const previous = previousState.files?.[relativePath] ?? null;
|
|
214
|
+
const targetFilePath = path.join(targetWorkspaceDir, relativePath);
|
|
215
|
+
if (!fs.existsSync(targetFilePath)) {
|
|
216
|
+
if (logger) logger(`runtime-assets: already absent ${relativePath}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const targetHash = hashFile(targetFilePath);
|
|
221
|
+
const previousAppliedHash = typeof previous?.appliedHash === "string" ? previous.appliedHash : null;
|
|
222
|
+
const localChanged = previousAppliedHash === null ? true : targetHash !== previousAppliedHash;
|
|
223
|
+
|
|
224
|
+
if (!localChanged || mode === "install") {
|
|
225
|
+
fs.rmSync(targetFilePath, { force: true });
|
|
226
|
+
summary.removed += 1;
|
|
227
|
+
if (logger) logger(`runtime-assets: removed ${relativePath}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const conflictInput = {
|
|
232
|
+
relativePath,
|
|
233
|
+
targetFilePath,
|
|
234
|
+
sourceFilePath: null,
|
|
235
|
+
removed: true,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const decision =
|
|
239
|
+
interactive && onConflict ? readDecision(await onConflict(conflictInput)) : "use-packaged";
|
|
240
|
+
|
|
241
|
+
if (decision === "keep-local") {
|
|
242
|
+
summary.keptLocal += 1;
|
|
243
|
+
summary.conflictResolutions.push({ path: relativePath, decision: "keep-local" });
|
|
244
|
+
if (logger) logger(`runtime-assets: kept removed local file ${relativePath}`);
|
|
245
|
+
nextState.files[relativePath] = {
|
|
246
|
+
sourceHash: null,
|
|
247
|
+
appliedHash: targetHash,
|
|
248
|
+
updatedAt: nowIso,
|
|
249
|
+
};
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!interactive) {
|
|
254
|
+
const backupPath = makeBackupPath(targetFilePath);
|
|
255
|
+
fs.copyFileSync(targetFilePath, backupPath);
|
|
256
|
+
summary.backupsCreated += 1;
|
|
257
|
+
summary.backups.push({ path: relativePath, backupPath });
|
|
258
|
+
if (logger) logger(`runtime-assets: backup ${relativePath} -> ${backupPath}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fs.rmSync(targetFilePath, { force: true });
|
|
262
|
+
summary.removed += 1;
|
|
263
|
+
summary.conflictResolutions.push({ path: relativePath, decision: "use-packaged" });
|
|
264
|
+
if (logger) logger(`runtime-assets: removed stale ${relativePath}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
writeState(stateFilePath, nextState);
|
|
268
|
+
return summary;
|
|
269
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { syncRuntimeWorkspaceAssets } from "./runtime-assets.mjs";
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const dir of tempDirs.splice(0)) {
|
|
12
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function makeTempDir() {
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-runtime-assets-"));
|
|
18
|
+
tempDirs.push(dir);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("syncRuntimeWorkspaceAssets", () => {
|
|
23
|
+
test("removes previously managed files that no longer exist in the source bundle", async () => {
|
|
24
|
+
const sourceDir = makeTempDir();
|
|
25
|
+
const targetDir = makeTempDir();
|
|
26
|
+
const stateFilePath = path.join(makeTempDir(), "runtime-assets-state.json");
|
|
27
|
+
|
|
28
|
+
fs.mkdirSync(path.join(sourceDir, "plugins"), { recursive: true });
|
|
29
|
+
fs.mkdirSync(path.join(targetDir, "plugins"), { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(sourceDir, "plugins", "memory_search.ts"), "export default 1\n");
|
|
31
|
+
|
|
32
|
+
await syncRuntimeWorkspaceAssets({
|
|
33
|
+
sourceWorkspaceDir: sourceDir,
|
|
34
|
+
targetWorkspaceDir: targetDir,
|
|
35
|
+
stateFilePath,
|
|
36
|
+
mode: "install",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(fs.existsSync(path.join(targetDir, "plugins", "memory_search.ts"))).toBe(true);
|
|
40
|
+
|
|
41
|
+
fs.rmSync(path.join(sourceDir, "plugins", "memory_search.ts"));
|
|
42
|
+
const result = await syncRuntimeWorkspaceAssets({
|
|
43
|
+
sourceWorkspaceDir: sourceDir,
|
|
44
|
+
targetWorkspaceDir: targetDir,
|
|
45
|
+
stateFilePath,
|
|
46
|
+
mode: "update",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.removed).toBe(1);
|
|
50
|
+
expect(fs.existsSync(path.join(targetDir, "plugins", "memory_search.ts"))).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
function workspaceFingerprint(workspaceDir) {
|
|
7
|
+
return createHash("sha256").update(path.resolve(workspaceDir)).digest("hex").slice(0, 16);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveManagedOpencodeConfigDir({
|
|
11
|
+
rootDir,
|
|
12
|
+
dataDir = path.join(rootDir, "data"),
|
|
13
|
+
workspaceDir = path.join(rootDir, "workspace"),
|
|
14
|
+
explicitConfigDir = process.env.OPENCODE_CONFIG_DIR,
|
|
15
|
+
}) {
|
|
16
|
+
if (typeof explicitConfigDir === "string" && explicitConfigDir.trim()) {
|
|
17
|
+
return path.resolve(explicitConfigDir.trim());
|
|
18
|
+
}
|
|
19
|
+
return path.join(dataDir, "opencode-config", workspaceFingerprint(workspaceDir));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function opencodeEnvironment(paths, baseEnv = process.env) {
|
|
23
|
+
return {
|
|
24
|
+
...baseEnv,
|
|
25
|
+
OPENCODE_CONFIG_DIR: paths.opencodeConfigDir,
|
|
26
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function pathsFor({ rootDir, scope, userUnitDir }) {
|
|
31
|
+
const normalizedScope = scope.replace(/^@/, "");
|
|
32
|
+
const npmPrefix = path.join(rootDir, "npm");
|
|
33
|
+
const workspaceDir = path.join(rootDir, "workspace");
|
|
34
|
+
const dataDir = path.join(rootDir, "data");
|
|
35
|
+
const localBinDir = path.join(process.env.HOME || "", ".local", "bin");
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
rootDir,
|
|
39
|
+
npmPrefix,
|
|
40
|
+
localBinDir,
|
|
41
|
+
agentMockingbirdShimPath: path.join(localBinDir, "agent-mockingbird"),
|
|
42
|
+
opencodeShimPath: path.join(localBinDir, "opencode"),
|
|
43
|
+
dataDir,
|
|
44
|
+
workspaceDir,
|
|
45
|
+
opencodeConfigDir: resolveManagedOpencodeConfigDir({ rootDir, dataDir, workspaceDir }),
|
|
46
|
+
logsDir: path.join(rootDir, "logs"),
|
|
47
|
+
etcDir: path.join(rootDir, "etc"),
|
|
48
|
+
npmrcPath: path.join(rootDir, "etc", "npmrc"),
|
|
49
|
+
agentMockingbirdAppDirGlobal: path.join(npmPrefix, "lib", "node_modules", `@${normalizedScope}`, "agent-mockingbird"),
|
|
50
|
+
agentMockingbirdAppDirLocal: path.join(npmPrefix, "node_modules", `@${normalizedScope}`, "agent-mockingbird"),
|
|
51
|
+
agentMockingbirdBinGlobal: path.join(npmPrefix, "bin", "agent-mockingbird"),
|
|
52
|
+
agentMockingbirdBinLocal: path.join(npmPrefix, "node_modules", ".bin", "agent-mockingbird"),
|
|
53
|
+
opencodeBinGlobal: path.join(npmPrefix, "bin", "opencode"),
|
|
54
|
+
opencodeBinLocal: path.join(npmPrefix, "node_modules", ".bin", "opencode"),
|
|
55
|
+
bunBinManagedGlobal: path.join(npmPrefix, "bin", "bun"),
|
|
56
|
+
bunBinManagedLocal: path.join(npmPrefix, "node_modules", ".bin", "bun"),
|
|
57
|
+
bunBinTools: path.join(rootDir, "tools", "bun", "bin", "bun"),
|
|
58
|
+
opencodeUnitPath: path.join(userUnitDir, "opencode.service"),
|
|
59
|
+
agentMockingbirdUnitPath: path.join(userUnitDir, "agent-mockingbird.service"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function prepareRuntimeAssetSources(agentMockingbirdAppDir) {
|
|
64
|
+
const workspaceSourceDir = path.join(agentMockingbirdAppDir, "runtime-assets", "workspace");
|
|
65
|
+
const opencodeConfigSourceDir = path.join(agentMockingbirdAppDir, "runtime-assets", "opencode-config");
|
|
66
|
+
if (!fs.existsSync(workspaceSourceDir) || !fs.existsSync(opencodeConfigSourceDir)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`runtime assets missing in package: expected ${workspaceSourceDir} and ${opencodeConfigSourceDir}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
workspaceSourceDir,
|
|
73
|
+
opencodeConfigSourceDir,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createStandaloneBuildOptions, STANDALONE_ENTRYPOINTS_RELATIVE } from "./standaloneBuild";
|
|
5
|
+
|
|
6
|
+
test("standalone build config uses the main server entrypoint", () => {
|
|
7
|
+
const repoRoot = path.resolve(import.meta.dir, "../../../..");
|
|
8
|
+
const outfile = path.join(repoRoot, "dist", "agent-mockingbird");
|
|
9
|
+
|
|
10
|
+
const options = createStandaloneBuildOptions(repoRoot, outfile);
|
|
11
|
+
expect(options.compile).toEqual({ outfile });
|
|
12
|
+
expect(options.sourcemap).toBe("linked");
|
|
13
|
+
expect(options.minify).toBe(true);
|
|
14
|
+
|
|
15
|
+
const entrypoints = options.entrypoints as string[];
|
|
16
|
+
for (const relativeEntrypoint of STANDALONE_ENTRYPOINTS_RELATIVE) {
|
|
17
|
+
expect(entrypoints).toContain(path.join(repoRoot, relativeEntrypoint));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const STANDALONE_ENTRYPOINTS_RELATIVE = ["apps/server/src/index.ts"] as const;
|
|
4
|
+
|
|
5
|
+
function resolveStandaloneEntrypoints(repoRoot: string): string[] {
|
|
6
|
+
return STANDALONE_ENTRYPOINTS_RELATIVE.map(entrypoint => path.join(repoRoot, entrypoint));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createStandaloneBuildOptions(repoRoot: string, outfile: string): Bun.BuildConfig {
|
|
10
|
+
return {
|
|
11
|
+
root: repoRoot,
|
|
12
|
+
entrypoints: resolveStandaloneEntrypoints(repoRoot),
|
|
13
|
+
compile: {
|
|
14
|
+
outfile,
|
|
15
|
+
},
|
|
16
|
+
minify: true,
|
|
17
|
+
sourcemap: "linked",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
function allocatePort(): Promise<number> {
|
|
8
|
+
return new Promise((resolvePort, rejectPort) => {
|
|
9
|
+
const server = net.createServer();
|
|
10
|
+
server.once("error", rejectPort);
|
|
11
|
+
server.listen(0, "127.0.0.1", () => {
|
|
12
|
+
const address = server.address();
|
|
13
|
+
if (!address || typeof address === "string") {
|
|
14
|
+
server.close();
|
|
15
|
+
rejectPort(new Error("Failed to allocate port"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const { port } = address;
|
|
19
|
+
server.close(error => {
|
|
20
|
+
if (error) {
|
|
21
|
+
rejectPort(error);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolvePort(port);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function waitForJson(url: string, timeoutMs: number) {
|
|
31
|
+
const deadline = Date.now() + timeoutMs;
|
|
32
|
+
let lastError: unknown = null;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
lastError = new Error(`Unexpected status ${response.status}`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
lastError = error;
|
|
42
|
+
}
|
|
43
|
+
await Bun.sleep(250);
|
|
44
|
+
}
|
|
45
|
+
throw lastError instanceof Error ? lastError : new Error(`Timed out waiting for ${url}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function waitForInstance(baseUrl: string, jobId: string, timeoutMs: number) {
|
|
49
|
+
const deadline = Date.now() + timeoutMs;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
const response = await fetch(`${baseUrl}/api/mockingbird/cron/instances?jobId=${encodeURIComponent(jobId)}&limit=1`);
|
|
52
|
+
const payload = (await response.json()) as {
|
|
53
|
+
instances?: Array<{ state?: string; error?: { message?: string } | null }>;
|
|
54
|
+
};
|
|
55
|
+
const instance = payload.instances?.[0];
|
|
56
|
+
if (instance?.state === "completed" || instance?.state === "failed" || instance?.state === "dead") {
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
await Bun.sleep(250);
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Timed out waiting for cron instance ${jobId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test(
|
|
65
|
+
"compiled binary runs module-backed background cron jobs without worker ModuleNotFound",
|
|
66
|
+
async () => {
|
|
67
|
+
const repoRoot = path.resolve(import.meta.dir, "../../../..");
|
|
68
|
+
const build = Bun.spawnSync({
|
|
69
|
+
cmd: ["bun", "run", "build:bin"],
|
|
70
|
+
cwd: repoRoot,
|
|
71
|
+
stdout: "pipe",
|
|
72
|
+
stderr: "pipe",
|
|
73
|
+
env: process.env,
|
|
74
|
+
});
|
|
75
|
+
expect(build.exitCode).toBe(0);
|
|
76
|
+
|
|
77
|
+
const sidecarPort = await allocatePort();
|
|
78
|
+
const apiPort = await allocatePort();
|
|
79
|
+
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-bin-cron-"));
|
|
80
|
+
const workspaceDir = path.join(tempRoot, "workspace");
|
|
81
|
+
const configPath = path.join(tempRoot, "config.json");
|
|
82
|
+
const dbPath = path.join(tempRoot, "agent-mockingbird.db");
|
|
83
|
+
mkdirSync(path.join(workspaceDir, "cron"), { recursive: true });
|
|
84
|
+
|
|
85
|
+
const config = JSON.parse(readFileSync(path.join(repoRoot, "agent-mockingbird.config.example.json"), "utf8")) as {
|
|
86
|
+
workspace: { pinnedDirectory: string };
|
|
87
|
+
runtime: {
|
|
88
|
+
opencode: { baseUrl: string; directory: string };
|
|
89
|
+
memory: { enabled: boolean; workspaceDir: string };
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
config.workspace.pinnedDirectory = workspaceDir;
|
|
93
|
+
config.runtime.opencode.baseUrl = `http://127.0.0.1:${sidecarPort}`;
|
|
94
|
+
config.runtime.opencode.directory = workspaceDir;
|
|
95
|
+
config.runtime.memory.enabled = false;
|
|
96
|
+
config.runtime.memory.workspaceDir = workspaceDir;
|
|
97
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
98
|
+
writeFileSync(
|
|
99
|
+
path.join(workspaceDir, "cron", "compiled-background.ts"),
|
|
100
|
+
[
|
|
101
|
+
"export default async function run(ctx) {",
|
|
102
|
+
" return { status: 'ok', summary: `compiled check ${ctx.payload.symbol}` };",
|
|
103
|
+
"}",
|
|
104
|
+
].join("\n"),
|
|
105
|
+
"utf8",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
let sessionCount = 0;
|
|
109
|
+
const sidecar = Bun.serve({
|
|
110
|
+
hostname: "127.0.0.1",
|
|
111
|
+
port: sidecarPort,
|
|
112
|
+
fetch(req) {
|
|
113
|
+
const url = new URL(req.url);
|
|
114
|
+
if (url.pathname === "/config" && req.method === "GET") {
|
|
115
|
+
return Response.json({ data: {} });
|
|
116
|
+
}
|
|
117
|
+
if (url.pathname === "/config" && req.method === "PUT") {
|
|
118
|
+
return Response.json({ data: {} });
|
|
119
|
+
}
|
|
120
|
+
if (url.pathname === "/session" && req.method === "POST") {
|
|
121
|
+
sessionCount += 1;
|
|
122
|
+
return Response.json({
|
|
123
|
+
data: {
|
|
124
|
+
id: `sess-${sessionCount}`,
|
|
125
|
+
title: sessionCount === 1 ? "main" : "Cron background",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return new Response("Not found", { status: 404 });
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const binary = Bun.spawn({
|
|
134
|
+
cmd: [path.join(repoRoot, "dist", "agent-mockingbird")],
|
|
135
|
+
cwd: repoRoot,
|
|
136
|
+
stdout: "pipe",
|
|
137
|
+
stderr: "pipe",
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
NODE_ENV: "production",
|
|
141
|
+
PORT: String(apiPort),
|
|
142
|
+
AGENT_MOCKINGBIRD_DB_PATH: dbPath,
|
|
143
|
+
AGENT_MOCKINGBIRD_CONFIG_PATH: configPath,
|
|
144
|
+
AGENT_MOCKINGBIRD_MEMORY_ENABLED: "false",
|
|
145
|
+
AGENT_MOCKINGBIRD_MEMORY_EMBED_PROVIDER: "none",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await waitForJson(`http://127.0.0.1:${apiPort}/api/mockingbird/cron/health`, 15_000);
|
|
151
|
+
|
|
152
|
+
const createResponse = await fetch(`http://127.0.0.1:${apiPort}/api/mockingbird/cron/jobs`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "Content-Type": "application/json" },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
id: "compiled-background-check",
|
|
157
|
+
name: "Compiled Background Check",
|
|
158
|
+
enabled: true,
|
|
159
|
+
scheduleKind: "every",
|
|
160
|
+
everyMs: 60_000,
|
|
161
|
+
runMode: "background",
|
|
162
|
+
conditionModulePath: "cron/compiled-background.ts",
|
|
163
|
+
conditionDescription: "Compiled binary module smoke test",
|
|
164
|
+
payload: { symbol: "AAPL" },
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
expect(createResponse.status).toBe(201);
|
|
168
|
+
|
|
169
|
+
const runResponse = await fetch(`http://127.0.0.1:${apiPort}/api/mockingbird/cron/jobs/compiled-background-check/run`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
});
|
|
172
|
+
expect(runResponse.status).toBe(202);
|
|
173
|
+
|
|
174
|
+
const instance = await waitForInstance(`http://127.0.0.1:${apiPort}`, "compiled-background-check", 15_000);
|
|
175
|
+
if (instance?.state !== "completed") {
|
|
176
|
+
throw new Error(`Cron instance failed: ${JSON.stringify(instance)}`);
|
|
177
|
+
}
|
|
178
|
+
expect(instance.error ?? null).toBeNull();
|
|
179
|
+
} finally {
|
|
180
|
+
binary.kill();
|
|
181
|
+
await binary.exited;
|
|
182
|
+
sidecar.stop(true);
|
|
183
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
60_000,
|
|
187
|
+
);
|