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,797 @@
|
|
|
1
|
+
import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/client";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
copyFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
statSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import type { Stats } from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import type { AgentMockingbirdConfig } from "../config/schema";
|
|
22
|
+
import { getConfigSnapshot } from "../config/service";
|
|
23
|
+
import { createOpencodeClientFromConnection, unwrapSdkData } from "../opencode/client";
|
|
24
|
+
import { resolveOpencodeWorkspaceDir } from "../workspace/resolve";
|
|
25
|
+
|
|
26
|
+
const EXCLUDED_DIR_NAMES = new Set([".git", "node_modules", ".next", "dist", "build", "coverage"]);
|
|
27
|
+
const MAX_DISCOVERED_FILES = 5_000;
|
|
28
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
29
|
+
const MAX_LLM_FILE_BYTES = 256 * 1024;
|
|
30
|
+
const REFERENCE_SNIPPET_MAX_CHARS = 3_500;
|
|
31
|
+
|
|
32
|
+
const INCLUDED_ROOT_PREFIXES = ["memory/", "scripts/", "cron/", "crons/", "skills/", ".agents/", ".agent/", ".opencode/"];
|
|
33
|
+
const INCLUDED_ROOT_FILES = new Set(["AGENTS.md", "SOUL.md", "IDENTITY.md", "CLAUDE.md", "README.md"]);
|
|
34
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]);
|
|
35
|
+
const TEXT_EXTENSIONS = new Set([
|
|
36
|
+
".md",
|
|
37
|
+
".markdown",
|
|
38
|
+
".txt",
|
|
39
|
+
".json",
|
|
40
|
+
".jsonc",
|
|
41
|
+
".yaml",
|
|
42
|
+
".yml",
|
|
43
|
+
".toml",
|
|
44
|
+
".ini",
|
|
45
|
+
".conf",
|
|
46
|
+
".cfg",
|
|
47
|
+
".sh",
|
|
48
|
+
".bash",
|
|
49
|
+
".zsh",
|
|
50
|
+
".fish",
|
|
51
|
+
".mjs",
|
|
52
|
+
".cjs",
|
|
53
|
+
".js",
|
|
54
|
+
".jsx",
|
|
55
|
+
".ts",
|
|
56
|
+
".tsx",
|
|
57
|
+
".py",
|
|
58
|
+
".rb",
|
|
59
|
+
".go",
|
|
60
|
+
".rs",
|
|
61
|
+
".java",
|
|
62
|
+
".kt",
|
|
63
|
+
".swift",
|
|
64
|
+
".sql",
|
|
65
|
+
".xml",
|
|
66
|
+
".html",
|
|
67
|
+
".css",
|
|
68
|
+
]);
|
|
69
|
+
const PROTECTED_TARGET_PATHS = new Set([
|
|
70
|
+
".opencode/opencode.jsonc",
|
|
71
|
+
".opencode/package.json",
|
|
72
|
+
".opencode/bun.lock",
|
|
73
|
+
".opencode/bun.lockb",
|
|
74
|
+
".opencode/node_modules",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
type OpenclawMergeChoice = {
|
|
79
|
+
decision: "merge" | "keep_target" | "keep_source";
|
|
80
|
+
mergedContent: string;
|
|
81
|
+
notes?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type OpenclawLlmMerger = {
|
|
85
|
+
client: OpencodeClient;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
model: {
|
|
88
|
+
providerId: string;
|
|
89
|
+
modelId: string;
|
|
90
|
+
};
|
|
91
|
+
timeoutMs: number;
|
|
92
|
+
openclawContext: string;
|
|
93
|
+
opencodeContext: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type OpenclawImportSource =
|
|
97
|
+
| { mode: "local"; path: string }
|
|
98
|
+
| { mode: "git"; url: string; ref?: string };
|
|
99
|
+
|
|
100
|
+
interface OpenclawMigrationInput {
|
|
101
|
+
source: OpenclawImportSource;
|
|
102
|
+
targetDirectory?: string;
|
|
103
|
+
config?: AgentMockingbirdConfig;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface MaterializedImportSource {
|
|
107
|
+
mode: "local" | "git";
|
|
108
|
+
resolvedDirectory: string;
|
|
109
|
+
url?: string;
|
|
110
|
+
requestedRef?: string;
|
|
111
|
+
resolvedRef?: string;
|
|
112
|
+
commit?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface DiscoveredSourceFile {
|
|
116
|
+
relativePath: string;
|
|
117
|
+
sourcePath: string;
|
|
118
|
+
targetRelativePath: string;
|
|
119
|
+
sourceHash: string;
|
|
120
|
+
sizeBytes: number;
|
|
121
|
+
notes?: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface OpenclawMigrationResult {
|
|
125
|
+
source: {
|
|
126
|
+
mode: "local" | "git";
|
|
127
|
+
resolvedDirectory: string;
|
|
128
|
+
url?: string;
|
|
129
|
+
requestedRef?: string;
|
|
130
|
+
resolvedRef?: string;
|
|
131
|
+
commit?: string;
|
|
132
|
+
};
|
|
133
|
+
targetDirectory: string;
|
|
134
|
+
discoveredCount: number;
|
|
135
|
+
copied: Array<{ relativePath: string; sourcePath: string; targetPath: string }>;
|
|
136
|
+
merged: Array<{ relativePath: string; sourcePath: string; targetPath: string; strategy: "llm" | "deterministic" }>;
|
|
137
|
+
skippedExisting: Array<{ relativePath: string; targetPath: string }>;
|
|
138
|
+
skippedIdentical: Array<{ relativePath: string; targetPath: string }>;
|
|
139
|
+
skippedProtected: Array<{ relativePath: string; targetPath: string }>;
|
|
140
|
+
failed: Array<{ relativePath: string; reason: string }>;
|
|
141
|
+
warnings: string[];
|
|
142
|
+
summary: {
|
|
143
|
+
discovered: number;
|
|
144
|
+
copied: number;
|
|
145
|
+
merged: number;
|
|
146
|
+
mergedByLlm: number;
|
|
147
|
+
mergedDeterministic: number;
|
|
148
|
+
skippedExisting: number;
|
|
149
|
+
skippedIdentical: number;
|
|
150
|
+
skippedProtected: number;
|
|
151
|
+
failed: number;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveWorkspaceDir(config: AgentMockingbirdConfig): string {
|
|
156
|
+
return resolveOpencodeWorkspaceDir(config);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function importCacheRoot() {
|
|
160
|
+
return path.join(os.tmpdir(), "agent-mockingbird-openclaw-import");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function gitCacheRootDir() {
|
|
164
|
+
return path.join(importCacheRoot(), "git");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ensureDir(dirPath: string) {
|
|
168
|
+
mkdirSync(dirPath, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeRelativePath(relPath: string) {
|
|
172
|
+
return relPath.split(path.sep).join("/");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeCaseInsensitivePath(relPath: string) {
|
|
176
|
+
return normalizeRelativePath(relPath).toLowerCase();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function assertInsideRoot(root: string, candidate: string) {
|
|
180
|
+
const resolvedRoot = path.resolve(root);
|
|
181
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
182
|
+
const relative = path.relative(resolvedRoot, resolvedCandidate);
|
|
183
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
184
|
+
throw new Error(`Path escapes source root: ${candidate}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hashFile(filePath: string) {
|
|
189
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type TargetPathState =
|
|
193
|
+
| { kind: "missing" }
|
|
194
|
+
| { kind: "file"; hash: string }
|
|
195
|
+
| { kind: "non-file"; fileType: string };
|
|
196
|
+
|
|
197
|
+
function describeFileType(stats: Stats) {
|
|
198
|
+
if (stats.isFile()) return "file";
|
|
199
|
+
if (stats.isDirectory()) return "directory";
|
|
200
|
+
if (stats.isSymbolicLink()) return "symlink";
|
|
201
|
+
if (stats.isBlockDevice()) return "block-device";
|
|
202
|
+
if (stats.isCharacterDevice()) return "character-device";
|
|
203
|
+
if (stats.isFIFO()) return "fifo";
|
|
204
|
+
if (stats.isSocket()) return "socket";
|
|
205
|
+
return "other";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readTargetPathState(targetPath: string): TargetPathState {
|
|
209
|
+
try {
|
|
210
|
+
const stats = lstatSync(targetPath);
|
|
211
|
+
const fileType = describeFileType(stats);
|
|
212
|
+
if (!stats.isFile()) return { kind: "non-file", fileType };
|
|
213
|
+
return { kind: "file", hash: hashFile(targetPath) };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
216
|
+
return { kind: "missing" };
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function runGit(args: string[], cwd?: string) {
|
|
223
|
+
const result = spawnSync("git", args, {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
cwd,
|
|
226
|
+
});
|
|
227
|
+
if (result.status !== 0) {
|
|
228
|
+
const output = `${result.stderr || ""}${result.stdout || ""}`.trim();
|
|
229
|
+
throw new Error(output || `git ${args.join(" ")} failed`);
|
|
230
|
+
}
|
|
231
|
+
return (result.stdout || "").trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolveDefaultRemoteBranch(repoDir: string) {
|
|
235
|
+
const head = runGit(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoDir);
|
|
236
|
+
const slash = head.indexOf("/");
|
|
237
|
+
if (slash === -1) return head;
|
|
238
|
+
return head.slice(slash + 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function materializeGitSource(input: { url: string; ref?: string }): MaterializedImportSource {
|
|
242
|
+
const rawUrl = input.url.trim();
|
|
243
|
+
if (!rawUrl) {
|
|
244
|
+
throw new Error("source.url is required for git import");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ensureDir(gitCacheRootDir());
|
|
248
|
+
const key = createHash("sha256")
|
|
249
|
+
.update(`${rawUrl}\n${(input.ref ?? "").trim()}`)
|
|
250
|
+
.digest("hex")
|
|
251
|
+
.slice(0, 16);
|
|
252
|
+
const repoDir = path.join(gitCacheRootDir(), key);
|
|
253
|
+
|
|
254
|
+
if (!existsSync(path.join(repoDir, ".git"))) {
|
|
255
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
256
|
+
runGit(["clone", "--quiet", rawUrl, repoDir]);
|
|
257
|
+
} else {
|
|
258
|
+
runGit(["fetch", "--all", "--prune"], repoDir);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const resolvedRef = (input.ref ?? "").trim() || resolveDefaultRemoteBranch(repoDir);
|
|
262
|
+
runGit(["checkout", "--force", resolvedRef], repoDir);
|
|
263
|
+
if (!(input.ref ?? "").trim()) {
|
|
264
|
+
runGit(["pull", "--ff-only"], repoDir);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const commit = runGit(["rev-parse", "HEAD"], repoDir);
|
|
268
|
+
return {
|
|
269
|
+
mode: "git",
|
|
270
|
+
resolvedDirectory: repoDir,
|
|
271
|
+
url: rawUrl,
|
|
272
|
+
requestedRef: (input.ref ?? "").trim() || undefined,
|
|
273
|
+
resolvedRef,
|
|
274
|
+
commit,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function materializeLocalSource(input: { path: string }): MaterializedImportSource {
|
|
279
|
+
const sourcePath = input.path.trim();
|
|
280
|
+
if (!sourcePath) {
|
|
281
|
+
throw new Error("source.path is required for local import");
|
|
282
|
+
}
|
|
283
|
+
const resolvedDirectory = path.resolve(sourcePath);
|
|
284
|
+
const stats = statSync(resolvedDirectory);
|
|
285
|
+
if (!stats.isDirectory()) {
|
|
286
|
+
throw new Error(`source.path is not a directory: ${resolvedDirectory}`);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
mode: "local",
|
|
290
|
+
resolvedDirectory,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function materializeSource(source: OpenclawImportSource): MaterializedImportSource {
|
|
295
|
+
return source.mode === "git" ? materializeGitSource(source) : materializeLocalSource(source);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function shouldIncludePath(relativePath: string): boolean {
|
|
299
|
+
if (!relativePath || relativePath.startsWith("../")) return false;
|
|
300
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
301
|
+
const baseName = path.basename(normalized);
|
|
302
|
+
if (INCLUDED_ROOT_FILES.has(baseName)) return true;
|
|
303
|
+
if (!normalized.includes("/") && isMarkdownPath(normalized)) return true;
|
|
304
|
+
return INCLUDED_ROOT_PREFIXES.some(prefix => normalized.startsWith(prefix));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mapTargetRelativePath(relativePath: string): string {
|
|
308
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
309
|
+
if (normalized.startsWith("skills/")) {
|
|
310
|
+
return `.agents/skills/${normalized.slice("skills/".length)}`;
|
|
311
|
+
}
|
|
312
|
+
return normalized;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function discoverMigrationFiles(sourceDirectory: string) {
|
|
316
|
+
const warnings: string[] = [];
|
|
317
|
+
const discovered: Omit<DiscoveredSourceFile, "targetRelativePath" | "notes">[] = [];
|
|
318
|
+
|
|
319
|
+
const walk = (dirPath: string) => {
|
|
320
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (entry.name === "." || entry.name === "..") continue;
|
|
323
|
+
if (entry.isDirectory() && EXCLUDED_DIR_NAMES.has(entry.name)) continue;
|
|
324
|
+
|
|
325
|
+
const absolutePath = path.join(dirPath, entry.name);
|
|
326
|
+
const relativePath = normalizeRelativePath(path.relative(sourceDirectory, absolutePath));
|
|
327
|
+
if (!relativePath || relativePath.startsWith("../")) continue;
|
|
328
|
+
|
|
329
|
+
let stats;
|
|
330
|
+
try {
|
|
331
|
+
stats = lstatSync(absolutePath);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
warnings.push(
|
|
334
|
+
`Skipped unreadable path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (stats.isSymbolicLink()) {
|
|
340
|
+
try {
|
|
341
|
+
const resolved = realpathSync(absolutePath);
|
|
342
|
+
assertInsideRoot(sourceDirectory, resolved);
|
|
343
|
+
} catch {
|
|
344
|
+
warnings.push(`Skipped symlink outside source root: ${relativePath}`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (stats.isDirectory()) {
|
|
350
|
+
walk(absolutePath);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!stats.isFile()) continue;
|
|
355
|
+
if (!shouldIncludePath(relativePath)) continue;
|
|
356
|
+
|
|
357
|
+
if (stats.size > MAX_FILE_BYTES) {
|
|
358
|
+
warnings.push(`Skipped large file ${relativePath} (${stats.size} bytes)`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (discovered.length >= MAX_DISCOVERED_FILES) {
|
|
363
|
+
warnings.push(`Reached file limit (${MAX_DISCOVERED_FILES}); remaining files were skipped`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
discovered.push({
|
|
368
|
+
relativePath,
|
|
369
|
+
sourcePath: absolutePath,
|
|
370
|
+
sourceHash: hashFile(absolutePath),
|
|
371
|
+
sizeBytes: stats.size,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
walk(sourceDirectory);
|
|
377
|
+
const hasAgentsSource = discovered.some(file => normalizeCaseInsensitivePath(file.relativePath) === "agents.md");
|
|
378
|
+
const files: DiscoveredSourceFile[] = discovered.map((file) => {
|
|
379
|
+
const normalized = normalizeCaseInsensitivePath(file.relativePath);
|
|
380
|
+
const notes: string[] = [];
|
|
381
|
+
let targetRelativePath = mapTargetRelativePath(file.relativePath);
|
|
382
|
+
|
|
383
|
+
// Compatibility bridge: OpenClaw commonly stores top-level prompt guidance in CLAUDE.md.
|
|
384
|
+
if (normalized === "claude.md" && !hasAgentsSource) {
|
|
385
|
+
targetRelativePath = "AGENTS.md";
|
|
386
|
+
notes.push("compat_map:CLAUDE.md->AGENTS.md");
|
|
387
|
+
warnings.push("Mapped CLAUDE.md to AGENTS.md because source AGENTS.md was not present");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
...file,
|
|
392
|
+
targetRelativePath,
|
|
393
|
+
notes,
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
return { files, warnings };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function resolveTargetDirectory(input: { targetDirectory?: string; config?: AgentMockingbirdConfig }) {
|
|
400
|
+
if (input.targetDirectory?.trim()) {
|
|
401
|
+
return path.resolve(input.targetDirectory.trim());
|
|
402
|
+
}
|
|
403
|
+
const config = input.config ?? getConfigSnapshot().config;
|
|
404
|
+
return resolveWorkspaceDir(config);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function isMarkdownPath(filePath: string) {
|
|
408
|
+
return MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isLikelyTextPath(filePath: string) {
|
|
412
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
413
|
+
return TEXT_EXTENSIONS.has(ext) || ext === "";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const OPENCLAW_SPECIFIC_PATTERNS = [
|
|
417
|
+
/\bopenclaw\b/i,
|
|
418
|
+
/\bclaude\.md\b/i,
|
|
419
|
+
/\bclaude code\b/i,
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
function isAgentInstructionsPath(relativePath: string) {
|
|
423
|
+
return normalizeCaseInsensitivePath(relativePath) === "agents.md";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateAgentMergeContent(content: string): string | null {
|
|
427
|
+
if (!content.trim()) return "merged content was empty";
|
|
428
|
+
const offenders = OPENCLAW_SPECIFIC_PATTERNS.filter(pattern => pattern.test(content));
|
|
429
|
+
if (!offenders.length) return null;
|
|
430
|
+
return "merged content still contained OpenClaw-specific references";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function clipForPrompt(text: string, limit = REFERENCE_SNIPPET_MAX_CHARS) {
|
|
434
|
+
const trimmed = text.trim();
|
|
435
|
+
if (trimmed.length <= limit) return trimmed;
|
|
436
|
+
const head = Math.floor(limit * 0.65);
|
|
437
|
+
const tail = Math.floor(limit * 0.35);
|
|
438
|
+
return `${trimmed.slice(0, head)}\n\n...[truncated ${trimmed.length - limit} chars]...\n\n${trimmed.slice(-tail)}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function readPromptReferenceIfExists(filePath: string) {
|
|
442
|
+
try {
|
|
443
|
+
if (!existsSync(filePath)) return "";
|
|
444
|
+
return clipForPrompt(readFileSync(filePath, "utf8"));
|
|
445
|
+
} catch {
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function loadExternalReferenceContexts() {
|
|
451
|
+
const openclawAgents = readPromptReferenceIfExists("/tmp/openclaw/AGENTS.md");
|
|
452
|
+
const openclawReadme = readPromptReferenceIfExists("/tmp/openclaw/README.md");
|
|
453
|
+
const opencodeAgents = readPromptReferenceIfExists("/tmp/opencode-7vCebI/AGENTS.md");
|
|
454
|
+
const opencodeReadme = readPromptReferenceIfExists("/tmp/opencode-7vCebI/README.md");
|
|
455
|
+
|
|
456
|
+
const openclawContext = [
|
|
457
|
+
"# OpenClaw reference",
|
|
458
|
+
openclawAgents ? `## AGENTS.md\n${openclawAgents}` : "",
|
|
459
|
+
openclawReadme ? `## README.md\n${openclawReadme}` : "",
|
|
460
|
+
]
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.join("\n\n");
|
|
463
|
+
|
|
464
|
+
const opencodeContext = [
|
|
465
|
+
"# OpenCode/Waffle-style reference",
|
|
466
|
+
opencodeAgents ? `## AGENTS.md\n${opencodeAgents}` : "",
|
|
467
|
+
opencodeReadme ? `## README.md\n${opencodeReadme}` : "",
|
|
468
|
+
]
|
|
469
|
+
.filter(Boolean)
|
|
470
|
+
.join("\n\n");
|
|
471
|
+
|
|
472
|
+
return { openclawContext, opencodeContext };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function decodeTextFileIfPossible(filePath: string, maxBytes = MAX_LLM_FILE_BYTES): string | null {
|
|
476
|
+
const buffer = readFileSync(filePath);
|
|
477
|
+
if (buffer.length > maxBytes) return null;
|
|
478
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
479
|
+
if (buffer[index] === 0) return null;
|
|
480
|
+
}
|
|
481
|
+
return buffer.toString("utf8");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function extractAssistantText(parts: Array<Part>) {
|
|
485
|
+
const text = parts
|
|
486
|
+
.filter((part): part is Extract<Part, { type: "text" }> => part.type === "text")
|
|
487
|
+
.map(part => part.text.trim())
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
.join("\n\n");
|
|
490
|
+
return text || null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function parseMergeChoice(text: string): OpenclawMergeChoice | null {
|
|
494
|
+
const candidates: string[] = [text];
|
|
495
|
+
const fenced = /```json\s*([\s\S]*?)```/i.exec(text);
|
|
496
|
+
if (fenced?.[1]) candidates.push(fenced[1]);
|
|
497
|
+
const firstBrace = text.indexOf("{");
|
|
498
|
+
const lastBrace = text.lastIndexOf("}");
|
|
499
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
500
|
+
candidates.push(text.slice(firstBrace, lastBrace + 1));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const candidate of candidates) {
|
|
504
|
+
try {
|
|
505
|
+
const parsed = JSON.parse(candidate) as Partial<OpenclawMergeChoice>;
|
|
506
|
+
if (typeof parsed?.mergedContent !== "string") continue;
|
|
507
|
+
const rawDecision = typeof parsed.decision === "string" ? parsed.decision.trim().toLowerCase() : "merge";
|
|
508
|
+
const decision = rawDecision === "keep_target" || rawDecision === "keep_source" ? rawDecision : "merge";
|
|
509
|
+
return {
|
|
510
|
+
decision,
|
|
511
|
+
mergedContent: parsed.mergedContent,
|
|
512
|
+
notes: typeof parsed.notes === "string" ? parsed.notes : undefined,
|
|
513
|
+
};
|
|
514
|
+
} catch {
|
|
515
|
+
// try next format
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function buildLlmMergePrompt(input: {
|
|
522
|
+
sourceRelativePath: string;
|
|
523
|
+
relativePath: string;
|
|
524
|
+
sourceContent: string;
|
|
525
|
+
targetContent: string;
|
|
526
|
+
openclawContext: string;
|
|
527
|
+
opencodeContext: string;
|
|
528
|
+
}) {
|
|
529
|
+
return [
|
|
530
|
+
"You are migrating an OpenClaw workspace file into a Agent Mockingbird/OpenCode workspace.",
|
|
531
|
+
"Return ONLY valid JSON with this exact shape:",
|
|
532
|
+
'{"decision":"merge|keep_target|keep_source","mergedContent":"...","notes":"optional"}',
|
|
533
|
+
"Rules:",
|
|
534
|
+
"1) Remove or rewrite OpenClaw-specific instructions, names, and references.",
|
|
535
|
+
"2) Keep existing Agent Mockingbird/OpenCode-specific instructions from target when there is conflict.",
|
|
536
|
+
"3) Preserve useful non-platform-specific content from source.",
|
|
537
|
+
"4) Output full final file in mergedContent (no markdown fences).",
|
|
538
|
+
"5) Prefer merge unless source is clearly incompatible.",
|
|
539
|
+
"6) Remove references to OpenClaw internals, OpenClaw commands, and CLAUDE.md conventions.",
|
|
540
|
+
"7) Keep result concise and instruction-focused for AGENTS.md.",
|
|
541
|
+
"",
|
|
542
|
+
`Target path: ${input.relativePath}`,
|
|
543
|
+
`Source path: ${input.sourceRelativePath}`,
|
|
544
|
+
"",
|
|
545
|
+
input.openclawContext ? `Reference context (OpenClaw):\n${input.openclawContext}` : "",
|
|
546
|
+
input.opencodeContext ? `Reference context (OpenCode/Waffle):\n${input.opencodeContext}` : "",
|
|
547
|
+
"",
|
|
548
|
+
`Current target file:\n\n${clipForPrompt(input.targetContent, 10_000)}`,
|
|
549
|
+
"",
|
|
550
|
+
`Incoming source file:\n\n${clipForPrompt(input.sourceContent, 10_000)}`,
|
|
551
|
+
]
|
|
552
|
+
.filter(Boolean)
|
|
553
|
+
.join("\n\n");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function createLlmMerger(config: AgentMockingbirdConfig, warnings: string[]): Promise<OpenclawLlmMerger | null> {
|
|
557
|
+
if (process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test") {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
if (process.env.AGENT_MOCKINGBIRD_DISABLE_OPENCLAW_LLM_MERGE === "1") {
|
|
561
|
+
warnings.push("LLM merge disabled via AGENT_MOCKINGBIRD_DISABLE_OPENCLAW_LLM_MERGE=1");
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const providerId = config.runtime.opencode.providerId.trim();
|
|
566
|
+
const modelId = config.runtime.opencode.modelId.trim();
|
|
567
|
+
if (!providerId || !modelId) {
|
|
568
|
+
warnings.push("LLM merge unavailable: runtime.opencode provider/model is not configured");
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const timeoutMs = Math.max(2_000, Math.min(config.runtime.opencode.timeoutMs, 20_000));
|
|
573
|
+
const client = createOpencodeClientFromConnection({
|
|
574
|
+
baseUrl: config.runtime.opencode.baseUrl,
|
|
575
|
+
directory: config.runtime.opencode.directory,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
const session = unwrapSdkData<Session>(
|
|
580
|
+
await client.session.create({
|
|
581
|
+
body: { title: "agent-mockingbird-openclaw-merge" },
|
|
582
|
+
responseStyle: "data",
|
|
583
|
+
throwOnError: true,
|
|
584
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
const references = loadExternalReferenceContexts();
|
|
588
|
+
return {
|
|
589
|
+
client,
|
|
590
|
+
sessionId: session.id,
|
|
591
|
+
model: { providerId, modelId },
|
|
592
|
+
timeoutMs,
|
|
593
|
+
openclawContext: references.openclawContext,
|
|
594
|
+
opencodeContext: references.opencodeContext,
|
|
595
|
+
};
|
|
596
|
+
} catch (error) {
|
|
597
|
+
warnings.push(`LLM merge unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function tryLlmMergeConflict(input: {
|
|
603
|
+
merger: OpenclawLlmMerger;
|
|
604
|
+
sourceRelativePath: string;
|
|
605
|
+
relativePath: string;
|
|
606
|
+
sourceContent: string;
|
|
607
|
+
targetContent: string;
|
|
608
|
+
}): Promise<OpenclawMergeChoice | null> {
|
|
609
|
+
const prompt = buildLlmMergePrompt({
|
|
610
|
+
sourceRelativePath: input.sourceRelativePath,
|
|
611
|
+
relativePath: input.relativePath,
|
|
612
|
+
sourceContent: input.sourceContent,
|
|
613
|
+
targetContent: input.targetContent,
|
|
614
|
+
openclawContext: input.merger.openclawContext,
|
|
615
|
+
opencodeContext: input.merger.opencodeContext,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const response = unwrapSdkData<{ info: Message; parts: Array<Part> }>(
|
|
619
|
+
await input.merger.client.session.prompt({
|
|
620
|
+
path: { id: input.merger.sessionId },
|
|
621
|
+
body: {
|
|
622
|
+
model: {
|
|
623
|
+
providerID: input.merger.model.providerId,
|
|
624
|
+
modelID: input.merger.model.modelId,
|
|
625
|
+
},
|
|
626
|
+
parts: [{ type: "text", text: prompt }],
|
|
627
|
+
},
|
|
628
|
+
responseStyle: "data",
|
|
629
|
+
throwOnError: true,
|
|
630
|
+
signal: AbortSignal.timeout(input.merger.timeoutMs),
|
|
631
|
+
}),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (response.info.role !== "assistant") {
|
|
635
|
+
throw new Error(`unexpected merge role: ${response.info.role}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const text = extractAssistantText(response.parts);
|
|
639
|
+
if (!text) {
|
|
640
|
+
throw new Error("merge response did not include assistant text");
|
|
641
|
+
}
|
|
642
|
+
return parseMergeChoice(text);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export async function migrateOpenclawWorkspace(input: OpenclawMigrationInput): Promise<OpenclawMigrationResult> {
|
|
646
|
+
const source = materializeSource(input.source);
|
|
647
|
+
const config = input.config ?? getConfigSnapshot().config;
|
|
648
|
+
const targetDirectory = resolveTargetDirectory({ targetDirectory: input.targetDirectory, config });
|
|
649
|
+
ensureDir(targetDirectory);
|
|
650
|
+
|
|
651
|
+
const discovered = discoverMigrationFiles(source.resolvedDirectory);
|
|
652
|
+
const copied: OpenclawMigrationResult["copied"] = [];
|
|
653
|
+
const merged: OpenclawMigrationResult["merged"] = [];
|
|
654
|
+
const skippedExisting: OpenclawMigrationResult["skippedExisting"] = [];
|
|
655
|
+
const skippedIdentical: OpenclawMigrationResult["skippedIdentical"] = [];
|
|
656
|
+
const skippedProtected: OpenclawMigrationResult["skippedProtected"] = [];
|
|
657
|
+
const failed: OpenclawMigrationResult["failed"] = [];
|
|
658
|
+
const warnings = [...discovered.warnings];
|
|
659
|
+
|
|
660
|
+
let llmMerger: OpenclawLlmMerger | null | undefined = undefined;
|
|
661
|
+
|
|
662
|
+
for (const file of discovered.files) {
|
|
663
|
+
const targetPath = path.join(targetDirectory, file.targetRelativePath);
|
|
664
|
+
const protectedMatch = [...PROTECTED_TARGET_PATHS].find(protectedPath => {
|
|
665
|
+
const normalizedTarget = normalizeCaseInsensitivePath(file.targetRelativePath);
|
|
666
|
+
const normalizedProtected = normalizeCaseInsensitivePath(protectedPath);
|
|
667
|
+
return normalizedTarget === normalizedProtected || normalizedTarget.startsWith(`${normalizedProtected}/`);
|
|
668
|
+
});
|
|
669
|
+
if (protectedMatch) {
|
|
670
|
+
skippedProtected.push({ relativePath: file.targetRelativePath, targetPath });
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const targetState = readTargetPathState(targetPath);
|
|
676
|
+
if (targetState.kind === "missing") {
|
|
677
|
+
ensureDir(path.dirname(targetPath));
|
|
678
|
+
copyFileSync(file.sourcePath, targetPath);
|
|
679
|
+
copied.push({ relativePath: file.targetRelativePath, sourcePath: file.sourcePath, targetPath });
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (targetState.kind === "non-file") {
|
|
683
|
+
failed.push({
|
|
684
|
+
relativePath: file.targetRelativePath,
|
|
685
|
+
reason: `cannot write over ${targetState.fileType} target: ${targetPath}`,
|
|
686
|
+
});
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (targetState.hash === file.sourceHash) {
|
|
690
|
+
skippedIdentical.push({ relativePath: file.targetRelativePath, targetPath });
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const sourceText =
|
|
695
|
+
file.sizeBytes <= MAX_LLM_FILE_BYTES && isLikelyTextPath(file.sourcePath)
|
|
696
|
+
? decodeTextFileIfPossible(file.sourcePath)
|
|
697
|
+
: null;
|
|
698
|
+
const targetText =
|
|
699
|
+
isLikelyTextPath(targetPath) && statSync(targetPath).size <= MAX_LLM_FILE_BYTES
|
|
700
|
+
? decodeTextFileIfPossible(targetPath)
|
|
701
|
+
: null;
|
|
702
|
+
|
|
703
|
+
if (sourceText !== null && targetText !== null && isAgentInstructionsPath(file.targetRelativePath)) {
|
|
704
|
+
if (typeof llmMerger === "undefined") {
|
|
705
|
+
llmMerger = await createLlmMerger(config, warnings);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (llmMerger) {
|
|
709
|
+
try {
|
|
710
|
+
const choice = await tryLlmMergeConflict({
|
|
711
|
+
merger: llmMerger,
|
|
712
|
+
sourceRelativePath: file.relativePath,
|
|
713
|
+
relativePath: file.targetRelativePath,
|
|
714
|
+
sourceContent: sourceText,
|
|
715
|
+
targetContent: targetText,
|
|
716
|
+
});
|
|
717
|
+
if (choice) {
|
|
718
|
+
if (choice.decision === "keep_target") {
|
|
719
|
+
skippedExisting.push({ relativePath: file.targetRelativePath, targetPath });
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (choice.decision === "keep_source") {
|
|
723
|
+
ensureDir(path.dirname(targetPath));
|
|
724
|
+
copyFileSync(file.sourcePath, targetPath);
|
|
725
|
+
merged.push({ relativePath: file.targetRelativePath, sourcePath: file.sourcePath, targetPath, strategy: "llm" });
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const normalizedContent = choice.mergedContent.endsWith("\n")
|
|
729
|
+
? choice.mergedContent
|
|
730
|
+
: `${choice.mergedContent}\n`;
|
|
731
|
+
if (isAgentInstructionsPath(file.targetRelativePath)) {
|
|
732
|
+
const invalidReason = validateAgentMergeContent(normalizedContent);
|
|
733
|
+
if (invalidReason) {
|
|
734
|
+
warnings.push(
|
|
735
|
+
`LLM merge rejected for ${file.targetRelativePath}: ${invalidReason}; keeping existing target`,
|
|
736
|
+
);
|
|
737
|
+
skippedExisting.push({ relativePath: file.targetRelativePath, targetPath });
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const mergedHash = createHash("sha256").update(normalizedContent).digest("hex");
|
|
742
|
+
if (mergedHash === targetState.hash) {
|
|
743
|
+
skippedIdentical.push({ relativePath: file.targetRelativePath, targetPath });
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
writeFileSync(targetPath, normalizedContent, "utf8");
|
|
747
|
+
merged.push({ relativePath: file.targetRelativePath, sourcePath: file.sourcePath, targetPath, strategy: "llm" });
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
warnings.push(`LLM merge returned invalid JSON for ${file.targetRelativePath}; keeping existing target`);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
warnings.push(
|
|
753
|
+
`LLM merge failed for ${file.targetRelativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (isAgentInstructionsPath(file.targetRelativePath)) {
|
|
760
|
+
warnings.push(`Skipped AGENTS.md merge for ${file.relativePath}; smart merge was unavailable`);
|
|
761
|
+
}
|
|
762
|
+
skippedExisting.push({ relativePath: file.targetRelativePath, targetPath });
|
|
763
|
+
} catch (error) {
|
|
764
|
+
failed.push({
|
|
765
|
+
relativePath: file.targetRelativePath,
|
|
766
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const mergedByLlm = merged.filter(entry => entry.strategy === "llm").length;
|
|
772
|
+
const mergedDeterministic = merged.length - mergedByLlm;
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
source,
|
|
776
|
+
targetDirectory,
|
|
777
|
+
discoveredCount: discovered.files.length,
|
|
778
|
+
copied,
|
|
779
|
+
merged,
|
|
780
|
+
skippedExisting,
|
|
781
|
+
skippedIdentical,
|
|
782
|
+
skippedProtected,
|
|
783
|
+
failed,
|
|
784
|
+
warnings,
|
|
785
|
+
summary: {
|
|
786
|
+
discovered: discovered.files.length,
|
|
787
|
+
copied: copied.length,
|
|
788
|
+
merged: merged.length,
|
|
789
|
+
mergedByLlm,
|
|
790
|
+
mergedDeterministic,
|
|
791
|
+
skippedExisting: skippedExisting.length,
|
|
792
|
+
skippedIdentical: skippedIdentical.length,
|
|
793
|
+
skippedProtected: skippedProtected.length,
|
|
794
|
+
failed: failed.length,
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
}
|