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,58 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
import { sqlite } from "../db/client";
|
|
4
|
+
|
|
5
|
+
interface SqliteVecState {
|
|
6
|
+
available: boolean;
|
|
7
|
+
version: string | null;
|
|
8
|
+
error: string | null;
|
|
9
|
+
loaded: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let state: SqliteVecState = {
|
|
13
|
+
available: false,
|
|
14
|
+
version: null,
|
|
15
|
+
error: null,
|
|
16
|
+
loaded: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function formatError(error: unknown) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function ensureSqliteVecLoaded(db: Database = sqlite): Promise<SqliteVecState> {
|
|
24
|
+
if (state.loaded) return state;
|
|
25
|
+
try {
|
|
26
|
+
const sqliteVec = await import("sqlite-vec");
|
|
27
|
+
const loadablePath =
|
|
28
|
+
typeof sqliteVec.getLoadablePath === "function" ? sqliteVec.getLoadablePath() : "";
|
|
29
|
+
if (!loadablePath) {
|
|
30
|
+
throw new Error("sqlite-vec loadable path is unavailable");
|
|
31
|
+
}
|
|
32
|
+
db.loadExtension(loadablePath);
|
|
33
|
+
const row = db
|
|
34
|
+
.query("SELECT vec_version() as version")
|
|
35
|
+
.get() as { version?: string } | null;
|
|
36
|
+
if (!row?.version || typeof row.version !== "string") {
|
|
37
|
+
throw new Error("vec_version() returned no version");
|
|
38
|
+
}
|
|
39
|
+
state = {
|
|
40
|
+
available: true,
|
|
41
|
+
version: row.version,
|
|
42
|
+
error: null,
|
|
43
|
+
loaded: true,
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
state = {
|
|
47
|
+
available: false,
|
|
48
|
+
version: null,
|
|
49
|
+
error: formatError(error),
|
|
50
|
+
loaded: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getSqliteVecState() {
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type MemoryRecordSource = "user" | "assistant" | "system";
|
|
2
|
+
export type MemoryToolMode = "hybrid" | "inject_only" | "tool_only";
|
|
3
|
+
|
|
4
|
+
export interface MemoryRecordInput {
|
|
5
|
+
source: MemoryRecordSource;
|
|
6
|
+
content: string;
|
|
7
|
+
entities?: string[];
|
|
8
|
+
confidence?: number;
|
|
9
|
+
supersedes?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MemoryRecord extends MemoryRecordInput {
|
|
13
|
+
id: string;
|
|
14
|
+
recordedAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MemoryRememberInput extends MemoryRecordInput {
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
topic?: string;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MemoryChunk {
|
|
24
|
+
id: string;
|
|
25
|
+
path: string;
|
|
26
|
+
startLine: number;
|
|
27
|
+
endLine: number;
|
|
28
|
+
text: string;
|
|
29
|
+
hash: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MemorySearchResult {
|
|
33
|
+
id: string;
|
|
34
|
+
path: string;
|
|
35
|
+
startLine: number;
|
|
36
|
+
endLine: number;
|
|
37
|
+
source: "memory";
|
|
38
|
+
score: number;
|
|
39
|
+
snippet: string;
|
|
40
|
+
citation: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MemoryStatus {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
workspaceDir: string;
|
|
46
|
+
provider: string;
|
|
47
|
+
model: string;
|
|
48
|
+
toolMode: MemoryToolMode;
|
|
49
|
+
vectorBackendConfigured: string;
|
|
50
|
+
vectorBackendActive: string;
|
|
51
|
+
vectorAvailable: boolean;
|
|
52
|
+
vectorDims: number | null;
|
|
53
|
+
vectorIndexedChunks: number;
|
|
54
|
+
vectorLastError: string | null;
|
|
55
|
+
files: number;
|
|
56
|
+
chunks: number;
|
|
57
|
+
records: number;
|
|
58
|
+
cacheEntries: number;
|
|
59
|
+
indexedAt: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface MemoryWriteValidation {
|
|
63
|
+
accepted: boolean;
|
|
64
|
+
reason: string;
|
|
65
|
+
normalizedContent: string;
|
|
66
|
+
normalizedConfidence: number;
|
|
67
|
+
duplicateRecordId?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MemoryRememberResult {
|
|
71
|
+
accepted: boolean;
|
|
72
|
+
reason: string;
|
|
73
|
+
validation: MemoryWriteValidation;
|
|
74
|
+
record?: MemoryRecord;
|
|
75
|
+
path?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MemoryWriteEvent {
|
|
79
|
+
id: string;
|
|
80
|
+
status: "accepted" | "rejected";
|
|
81
|
+
reason: string;
|
|
82
|
+
source: MemoryRecordSource;
|
|
83
|
+
content: string;
|
|
84
|
+
confidence: number;
|
|
85
|
+
sessionId: string | null;
|
|
86
|
+
topic: string | null;
|
|
87
|
+
recordId: string | null;
|
|
88
|
+
path: string | null;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MemoryLintReport {
|
|
93
|
+
ok: boolean;
|
|
94
|
+
totalRecords: number;
|
|
95
|
+
duplicateActiveRecords: Array<{
|
|
96
|
+
content: string;
|
|
97
|
+
count: number;
|
|
98
|
+
recordIds: string[];
|
|
99
|
+
}>;
|
|
100
|
+
danglingSupersedes: Array<{
|
|
101
|
+
recordId: string;
|
|
102
|
+
missingSupersedes: string[];
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { AgentMockingbirdPlugin } from "../../../../../runtime-assets/opencode-config/plugins/agent-mockingbird";
|
|
4
|
+
|
|
5
|
+
const originalFetch = globalThis.fetch;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
globalThis.fetch = originalFetch;
|
|
9
|
+
delete process.env.AGENT_MOCKINGBIRD_MEMORY_API_BASE_URL;
|
|
10
|
+
delete process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL;
|
|
11
|
+
delete process.env.AGENT_MOCKINGBIRD_CRON_API_BASE_URL;
|
|
12
|
+
delete process.env.AGENT_MOCKINGBIRD_PORT;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("AgentMockingbirdPlugin", () => {
|
|
16
|
+
test("registers the expected Agent Mockingbird tool surface", async () => {
|
|
17
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
18
|
+
expect(Object.keys(hooks.tool ?? {}).sort()).toEqual([
|
|
19
|
+
"agent_type_manager",
|
|
20
|
+
"config_manager",
|
|
21
|
+
"cron_manager",
|
|
22
|
+
"memory_get",
|
|
23
|
+
"memory_remember",
|
|
24
|
+
"memory_search",
|
|
25
|
+
"notify_main_thread",
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("memory_search calls the memory API and compacts snippets", async () => {
|
|
30
|
+
process.env.AGENT_MOCKINGBIRD_MEMORY_API_BASE_URL = "http://127.0.0.1:3001";
|
|
31
|
+
|
|
32
|
+
globalThis.fetch = (async (input) => {
|
|
33
|
+
expect(String(input)).toBe("http://127.0.0.1:3001/api/mockingbird/memory/retrieve");
|
|
34
|
+
return new Response(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
results: [
|
|
37
|
+
{
|
|
38
|
+
id: "memory-1",
|
|
39
|
+
score: 0.97,
|
|
40
|
+
citation: "memory/2026-03-14.md#L1",
|
|
41
|
+
path: "memory/2026-03-14.md",
|
|
42
|
+
startLine: 1,
|
|
43
|
+
endLine: 4,
|
|
44
|
+
snippet: "### [memory:memory-1]\nmeta: thing\nStored detail",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}),
|
|
48
|
+
{
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}) as typeof fetch;
|
|
55
|
+
|
|
56
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
57
|
+
const raw = await hooks.tool?.memory_search?.execute(
|
|
58
|
+
{
|
|
59
|
+
query: "stored detail",
|
|
60
|
+
},
|
|
61
|
+
{} as never,
|
|
62
|
+
);
|
|
63
|
+
const payload = JSON.parse(raw ?? "{}") as {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
count: number;
|
|
66
|
+
results: Array<{ id: string; preview: string; snippet: string }>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
expect(payload.ok).toBe(true);
|
|
70
|
+
expect(payload.count).toBe(1);
|
|
71
|
+
expect(payload.results[0]?.id).toBe("memory-1");
|
|
72
|
+
expect(payload.results[0]?.preview).toBe("Stored detail");
|
|
73
|
+
expect(payload.results[0]?.snippet).toBe("Stored detail");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("system transform appends Agent Mockingbird prompt from the runtime API", async () => {
|
|
77
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
78
|
+
|
|
79
|
+
globalThis.fetch = (async (input) => {
|
|
80
|
+
const url = String(input);
|
|
81
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/system-prompt") {
|
|
82
|
+
return new Response(JSON.stringify({ system: "Config policy:\n- Use config_manager." }), {
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/session-scope?sessionId=sess-1") {
|
|
89
|
+
return new Response(JSON.stringify({ localSessionId: "session-123", isMain: false, kind: "other", heartbeat: false }), {
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
96
|
+
}) as typeof fetch;
|
|
97
|
+
|
|
98
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
99
|
+
const output = { system: ["existing"] };
|
|
100
|
+
await hooks["experimental.chat.system.transform"]?.({ sessionID: "sess-1", model: {} as never }, output);
|
|
101
|
+
expect(output.system).toEqual(["existing", "Config policy:\n- Use config_manager."]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("system transform appends main-thread guidance for the rooted conversation", async () => {
|
|
105
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
106
|
+
|
|
107
|
+
globalThis.fetch = (async (input) => {
|
|
108
|
+
const url = String(input);
|
|
109
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/system-prompt") {
|
|
110
|
+
return new Response(JSON.stringify({ system: "Config policy:\n- Use config_manager." }), {
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/session-scope?sessionId=sess-main") {
|
|
117
|
+
return new Response(JSON.stringify({ localSessionId: "main", isMain: true, kind: "main", heartbeat: false }), {
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
124
|
+
}) as typeof fetch;
|
|
125
|
+
|
|
126
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
127
|
+
const output = { system: ["existing"] };
|
|
128
|
+
await hooks["experimental.chat.system.transform"]?.({ sessionID: "sess-main", model: {} as never }, output);
|
|
129
|
+
expect(output.system).toEqual([
|
|
130
|
+
"existing",
|
|
131
|
+
"Config policy:\n- Use config_manager.",
|
|
132
|
+
"Thread policy:\n- This is the main/root conversation thread.\n- Prefer doing work directly in this thread unless delegation materially improves speed or focus.\n- Treat this thread as the primary durable context for the user.",
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("system transform appends cron-thread guidance for cron worker sessions", async () => {
|
|
137
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
138
|
+
|
|
139
|
+
globalThis.fetch = (async (input) => {
|
|
140
|
+
const url = String(input);
|
|
141
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/system-prompt") {
|
|
142
|
+
return new Response(JSON.stringify({ system: "Config policy:\n- Use config_manager." }), {
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/session-scope?sessionId=sess-cron") {
|
|
149
|
+
return new Response(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
localSessionId: "session-cron-1",
|
|
152
|
+
isMain: false,
|
|
153
|
+
kind: "cron",
|
|
154
|
+
heartbeat: false,
|
|
155
|
+
cronJobId: "cron-stock",
|
|
156
|
+
cronJobName: "stock-watch",
|
|
157
|
+
}),
|
|
158
|
+
{
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
166
|
+
}) as typeof fetch;
|
|
167
|
+
|
|
168
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
169
|
+
const output = { system: ["existing"] };
|
|
170
|
+
await hooks["experimental.chat.system.transform"]?.({ sessionID: "sess-cron", model: {} as never }, output);
|
|
171
|
+
expect(output.system).toEqual([
|
|
172
|
+
"existing",
|
|
173
|
+
"Config policy:\n- Use config_manager.",
|
|
174
|
+
"Thread policy:\n- This thread belongs to cron job stock-watch (cron-stock).\n- Keep work focused on this cron job's ongoing context and prior runs.\n- Do not act like this is the main user-facing conversation thread.\n- If user attention or a decision is needed, call notify_main_thread with a concise prompt for main.",
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("system transform appends heartbeat-thread guidance for heartbeat worker sessions", async () => {
|
|
179
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
180
|
+
|
|
181
|
+
globalThis.fetch = (async (input) => {
|
|
182
|
+
const url = String(input);
|
|
183
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/system-prompt") {
|
|
184
|
+
return new Response(JSON.stringify({ system: "Config policy:\n- Use config_manager." }), {
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (url === "http://127.0.0.1:3001/api/mockingbird/runtime/session-scope?sessionId=sess-heartbeat") {
|
|
191
|
+
return new Response(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
localSessionId: "session-heartbeat-1",
|
|
194
|
+
isMain: false,
|
|
195
|
+
kind: "heartbeat",
|
|
196
|
+
heartbeat: true,
|
|
197
|
+
cronJobId: null,
|
|
198
|
+
cronJobName: null,
|
|
199
|
+
}),
|
|
200
|
+
{
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
208
|
+
}) as typeof fetch;
|
|
209
|
+
|
|
210
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
211
|
+
const output = { system: ["existing"] };
|
|
212
|
+
await hooks["experimental.chat.system.transform"]?.({ sessionID: "sess-heartbeat", model: {} as never }, output);
|
|
213
|
+
expect(output.system).toEqual([
|
|
214
|
+
"existing",
|
|
215
|
+
"Config policy:\n- Use config_manager.",
|
|
216
|
+
"Thread policy:\n- This thread belongs to heartbeat.\n- Treat the main/root conversation as the durable source of user context.\n- The standard tool surface remains available in this thread.\n- Use any available tool when it materially helps the heartbeat do useful work.\n- Do not act like this is the main user-facing conversation thread.\n- If user attention or a decision is needed, call notify_main_thread with a concise prompt for main.",
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("compaction hook prefers a replacement prompt from the runtime API", async () => {
|
|
221
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
222
|
+
|
|
223
|
+
globalThis.fetch = (async (input) => {
|
|
224
|
+
expect(String(input)).toBe("http://127.0.0.1:3001/api/mockingbird/runtime/compaction-context?sessionId=sess-1");
|
|
225
|
+
return new Response(
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
prompt: "You are generating a compact factual continuation summary.\n## Decisions",
|
|
228
|
+
context: ["Agent Mockingbird continuation notes:\n- Mention config changes."],
|
|
229
|
+
}),
|
|
230
|
+
{
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}) as typeof fetch;
|
|
237
|
+
|
|
238
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
239
|
+
const output = { context: ["existing"], prompt: undefined as string | undefined };
|
|
240
|
+
await hooks["experimental.session.compacting"]?.({ sessionID: "sess-1" }, output);
|
|
241
|
+
expect(output.prompt).toContain("compact factual continuation summary");
|
|
242
|
+
expect(output.context).toEqual(["existing"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("compaction hook falls back to appended context when no replacement prompt is returned", async () => {
|
|
246
|
+
process.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL = "http://127.0.0.1:3001";
|
|
247
|
+
|
|
248
|
+
globalThis.fetch = (async (input) => {
|
|
249
|
+
expect(String(input)).toBe("http://127.0.0.1:3001/api/mockingbird/runtime/compaction-context?sessionId=sess-1");
|
|
250
|
+
return new Response(JSON.stringify({ context: ["Agent Mockingbird continuation notes:\n- Mention config changes."] }), {
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}) as typeof fetch;
|
|
256
|
+
|
|
257
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
258
|
+
const output = { context: ["existing"], prompt: undefined as string | undefined };
|
|
259
|
+
await hooks["experimental.session.compacting"]?.({ sessionID: "sess-1" }, output);
|
|
260
|
+
expect(output.prompt).toBeUndefined();
|
|
261
|
+
expect(output.context).toEqual(["existing", "Agent Mockingbird continuation notes:\n- Mention config changes."]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("tool definition hook rewrites assistant-facing tool copy", async () => {
|
|
265
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
266
|
+
const questionOutput = {
|
|
267
|
+
description: "original question",
|
|
268
|
+
parameters: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
questions: {
|
|
272
|
+
type: "array",
|
|
273
|
+
description: "original questions",
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await hooks["tool.definition"]?.({ toolID: "question" }, questionOutput);
|
|
280
|
+
|
|
281
|
+
expect(questionOutput.description).toContain("Ask the user a short structured question");
|
|
282
|
+
expect(questionOutput.parameters.properties.questions.description).toContain("structured clarification");
|
|
283
|
+
|
|
284
|
+
const taskOutput = {
|
|
285
|
+
description: "original task",
|
|
286
|
+
parameters: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
description: { type: "string", description: "old" },
|
|
290
|
+
prompt: { type: "string", description: "old" },
|
|
291
|
+
subagent_type: { type: "string", description: "old" },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await hooks["tool.definition"]?.({ toolID: "task" }, taskOutput);
|
|
297
|
+
|
|
298
|
+
expect(taskOutput.description).toContain("Delegate a bounded subtask");
|
|
299
|
+
expect(taskOutput.parameters.properties.description.description).toBe(
|
|
300
|
+
"A short summary of the delegated subtask.",
|
|
301
|
+
);
|
|
302
|
+
expect(taskOutput.parameters.properties.prompt.description).toBe(
|
|
303
|
+
"Exact instructions for the specialized agent to complete.",
|
|
304
|
+
);
|
|
305
|
+
expect(taskOutput.parameters.properties.subagent_type.description).toBe(
|
|
306
|
+
"The specialist agent type that should handle this delegated subtask.",
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("shell env exposes Agent Mockingbird API base URLs", async () => {
|
|
311
|
+
process.env.AGENT_MOCKINGBIRD_PORT = "3001";
|
|
312
|
+
|
|
313
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
314
|
+
const output = { env: {} as Record<string, string> };
|
|
315
|
+
await hooks["shell.env"]?.({ cwd: "/tmp" }, output);
|
|
316
|
+
|
|
317
|
+
expect(output.env.AGENT_MOCKINGBIRD_CONFIG_API_BASE_URL).toBe("http://127.0.0.1:3001");
|
|
318
|
+
expect(output.env.AGENT_MOCKINGBIRD_MEMORY_API_BASE_URL).toBe("http://127.0.0.1:3001");
|
|
319
|
+
expect(output.env.AGENT_MOCKINGBIRD_CRON_API_BASE_URL).toBe("http://127.0.0.1:3001");
|
|
320
|
+
expect(output.env.AGENT_MOCKINGBIRD_PORT).toBe("3001");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("notify_main_thread sends an escalation request using the calling session", async () => {
|
|
324
|
+
process.env.AGENT_MOCKINGBIRD_CRON_API_BASE_URL = "http://127.0.0.1:3001";
|
|
325
|
+
|
|
326
|
+
globalThis.fetch = (async (input, init) => {
|
|
327
|
+
expect(String(input)).toBe("http://127.0.0.1:3001/api/mockingbird/runtime/notify-main-thread");
|
|
328
|
+
expect(init?.method).toBe("POST");
|
|
329
|
+
expect(init?.headers).toEqual({ "Content-Type": "application/json" });
|
|
330
|
+
expect(init?.body).toBe(
|
|
331
|
+
JSON.stringify({
|
|
332
|
+
sessionId: "sess-cron",
|
|
333
|
+
prompt: "Please ask the user whether to place the trade.",
|
|
334
|
+
severity: "warn",
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
return new Response(JSON.stringify({ delivered: true, cronJobId: "cron-stock", threadSessionId: "session-cron-1" }), {
|
|
338
|
+
headers: {
|
|
339
|
+
"Content-Type": "application/json",
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}) as typeof fetch;
|
|
343
|
+
|
|
344
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
345
|
+
const raw = await hooks.tool?.notify_main_thread?.execute(
|
|
346
|
+
{
|
|
347
|
+
prompt: "Please ask the user whether to place the trade.",
|
|
348
|
+
severity: "warn",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
sessionID: "sess-cron",
|
|
352
|
+
} as never,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(JSON.parse(raw ?? "{}")).toEqual({
|
|
356
|
+
ok: true,
|
|
357
|
+
delivered: true,
|
|
358
|
+
cronJobId: "cron-stock",
|
|
359
|
+
threadSessionId: "session-cron-1",
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("memory_remember forwards the calling session id", async () => {
|
|
364
|
+
process.env.AGENT_MOCKINGBIRD_MEMORY_API_BASE_URL = "http://127.0.0.1:3001";
|
|
365
|
+
|
|
366
|
+
globalThis.fetch = (async (input, init) => {
|
|
367
|
+
expect(String(input)).toBe("http://127.0.0.1:3001/api/mockingbird/memory/remember");
|
|
368
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
369
|
+
content?: string;
|
|
370
|
+
source?: string;
|
|
371
|
+
sessionId?: string;
|
|
372
|
+
};
|
|
373
|
+
expect(body.content).toBe("Remember this.");
|
|
374
|
+
expect(body.source).toBe("assistant");
|
|
375
|
+
expect(body.sessionId).toBe("sess-heartbeat");
|
|
376
|
+
return new Response(JSON.stringify({ accepted: true, reason: "accepted" }), {
|
|
377
|
+
status: 201,
|
|
378
|
+
headers: {
|
|
379
|
+
"Content-Type": "application/json",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}) as typeof fetch;
|
|
383
|
+
|
|
384
|
+
const hooks = await AgentMockingbirdPlugin({} as never);
|
|
385
|
+
const raw = await hooks.tool?.memory_remember?.execute(
|
|
386
|
+
{
|
|
387
|
+
content: "Remember this.",
|
|
388
|
+
},
|
|
389
|
+
{ sessionID: "sess-heartbeat" } as never,
|
|
390
|
+
);
|
|
391
|
+
const payload = JSON.parse(raw ?? "{}") as { ok: boolean; result: { accepted: boolean } };
|
|
392
|
+
|
|
393
|
+
expect(payload.ok).toBe(true);
|
|
394
|
+
expect(payload.result.accepted).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createOpencodeClient as createSdkClient, type OpencodeClient } from "@opencode-ai/sdk/client";
|
|
2
|
+
import {
|
|
3
|
+
createOpencodeClient as createSdkV2Client,
|
|
4
|
+
type OpencodeClient as OpencodeV2Client,
|
|
5
|
+
} from "@opencode-ai/sdk/v2/client";
|
|
6
|
+
|
|
7
|
+
import { env } from "../env";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OPENCODE_BASE_URL =
|
|
10
|
+
process.env.AGENT_MOCKINGBIRD_OPENCODE_BASE_URL?.trim() ||
|
|
11
|
+
`http://127.0.0.1:${process.env.OPENCODE_PORT?.trim() || "4096"}`;
|
|
12
|
+
const DEFAULT_OPENCODE_TIMEOUT_MS = 120_000;
|
|
13
|
+
|
|
14
|
+
interface OpencodeConnectionConfig {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
directory?: string | null;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpencodeConnectionInfo {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
timeoutMs: number;
|
|
23
|
+
directoryConfigured: boolean;
|
|
24
|
+
authConfigured: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveAuthHeader() {
|
|
28
|
+
const explicit = env.AGENT_MOCKINGBIRD_OPENCODE_AUTH_HEADER?.trim();
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
|
|
31
|
+
const username = env.AGENT_MOCKINGBIRD_OPENCODE_USERNAME?.trim();
|
|
32
|
+
const password = env.AGENT_MOCKINGBIRD_OPENCODE_PASSWORD ?? "";
|
|
33
|
+
if (!username) return undefined;
|
|
34
|
+
return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getOpencodeConnectionInfo(connection?: OpencodeConnectionConfig): OpencodeConnectionInfo {
|
|
38
|
+
const authHeader = resolveAuthHeader();
|
|
39
|
+
const resolved = connection ?? {
|
|
40
|
+
baseUrl: DEFAULT_OPENCODE_BASE_URL,
|
|
41
|
+
timeoutMs: DEFAULT_OPENCODE_TIMEOUT_MS,
|
|
42
|
+
directory: undefined,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
baseUrl: resolved.baseUrl,
|
|
46
|
+
timeoutMs: resolved.timeoutMs,
|
|
47
|
+
directoryConfigured: Boolean(resolved.directory),
|
|
48
|
+
authConfigured: Boolean(authHeader),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createOpencodeClient(): OpencodeClient {
|
|
53
|
+
return createOpencodeClientFromConnection({
|
|
54
|
+
baseUrl: DEFAULT_OPENCODE_BASE_URL,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createOpencodeClientFromConnection(connection: {
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
directory?: string | null;
|
|
61
|
+
}): OpencodeClient {
|
|
62
|
+
const authHeader = resolveAuthHeader();
|
|
63
|
+
return createSdkClient({
|
|
64
|
+
baseUrl: connection.baseUrl,
|
|
65
|
+
headers: authHeader ? { Authorization: authHeader } : undefined,
|
|
66
|
+
directory: connection.directory ?? undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createOpencodeV2ClientFromConnection(connection: {
|
|
71
|
+
baseUrl: string;
|
|
72
|
+
directory?: string | null;
|
|
73
|
+
}): OpencodeV2Client {
|
|
74
|
+
const authHeader = resolveAuthHeader();
|
|
75
|
+
return createSdkV2Client({
|
|
76
|
+
baseUrl: connection.baseUrl,
|
|
77
|
+
headers: authHeader ? { Authorization: authHeader } : undefined,
|
|
78
|
+
directory: connection.directory ?? undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getOpencodeErrorStatus(error: unknown): number | null {
|
|
83
|
+
if (!error || typeof error !== "object") return null;
|
|
84
|
+
const status = (error as { status?: unknown }).status;
|
|
85
|
+
return typeof status === "number" ? status : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function unwrapSdkData<T>(result: T | { data: T }): T {
|
|
89
|
+
if (
|
|
90
|
+
result &&
|
|
91
|
+
typeof result === "object" &&
|
|
92
|
+
"data" in result &&
|
|
93
|
+
typeof (result as { data?: unknown }).data !== "undefined"
|
|
94
|
+
) {
|
|
95
|
+
return (result as { data: T }).data;
|
|
96
|
+
}
|
|
97
|
+
return result as T;
|
|
98
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ConfigProvidersResponse } from "@opencode-ai/sdk/client";
|
|
2
|
+
|
|
3
|
+
import { createOpencodeClientFromConnection, unwrapSdkData } from "./client";
|
|
4
|
+
import { getConfig } from "../config/service";
|
|
5
|
+
|
|
6
|
+
interface OpencodeModelOption {
|
|
7
|
+
id: string;
|
|
8
|
+
providerId: string;
|
|
9
|
+
modelId: string;
|
|
10
|
+
label: string;
|
|
11
|
+
supportsImageInput?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function listOpencodeModelOptions(): Promise<OpencodeModelOption[]> {
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
const client = createOpencodeClientFromConnection({
|
|
17
|
+
baseUrl: config.runtime.opencode.baseUrl,
|
|
18
|
+
directory: config.runtime.opencode.directory,
|
|
19
|
+
});
|
|
20
|
+
const payload = unwrapSdkData<ConfigProvidersResponse>(await client.config.providers({
|
|
21
|
+
responseStyle: "data",
|
|
22
|
+
throwOnError: true,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const options: OpencodeModelOption[] = [];
|
|
26
|
+
for (const provider of payload.providers) {
|
|
27
|
+
for (const [modelKey, model] of Object.entries(provider.models)) {
|
|
28
|
+
const modelId = model.id ?? modelKey;
|
|
29
|
+
const id = `${provider.id}/${modelId}`;
|
|
30
|
+
options.push({
|
|
31
|
+
id,
|
|
32
|
+
providerId: provider.id,
|
|
33
|
+
modelId,
|
|
34
|
+
label: `${provider.name} / ${model.name}`,
|
|
35
|
+
supportsImageInput: model.capabilities?.input?.image === true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return options.sort((a, b) => a.label.localeCompare(b.label));
|
|
41
|
+
}
|