nemoris 0.1.0 → 0.1.2
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/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/runtime/migration.js
CHANGED
|
@@ -1,323 +1,323 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import { OpenClawShadowBridge } from "../shadow/bridge.js";
|
|
4
|
-
import { MemoryStore } from "../memory/memory-store.js";
|
|
5
|
-
import { ensureDir, readText, statPath } from "../utils/fs.js";
|
|
6
|
-
|
|
7
|
-
const MODEL_LANE_MAP = {
|
|
8
|
-
"anthropic/claude-opus-4-6": "interactive_primary",
|
|
9
|
-
"anthropic/claude-opus-4": "interactive_primary",
|
|
10
|
-
"anthropic/claude-sonnet-4-6": "interactive_primary",
|
|
11
|
-
"anthropic/claude-sonnet-4": "interactive_primary",
|
|
12
|
-
"anthropic/claude-haiku-4-5": "local_cheap",
|
|
13
|
-
"anthropic/claude-haiku-4": "local_cheap",
|
|
14
|
-
"openrouter/openai/gpt-5.2": "interactive_primary",
|
|
15
|
-
"openrouter/anthropic/claude-haiku-4-5": "local_cheap",
|
|
16
|
-
"ollama/qwen3:8b": "local_primary",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function modelToLane(primaryModel) {
|
|
20
|
-
if (!primaryModel) return "interactive_primary";
|
|
21
|
-
if (MODEL_LANE_MAP[primaryModel]) return MODEL_LANE_MAP[primaryModel];
|
|
22
|
-
// Fallback: contains-based detection
|
|
23
|
-
if (primaryModel.includes("haiku")) return "local_cheap";
|
|
24
|
-
if (primaryModel.startsWith("ollama/")) return "local_primary";
|
|
25
|
-
return "interactive_primary";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function escapeTOMLString(s) {
|
|
29
|
-
return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Migrate agent configurations, cron jobs, and preferences from OpenClaw to Nemoris.
|
|
34
|
-
*
|
|
35
|
-
* @param {Object} options
|
|
36
|
-
* @param {string} options.installDir - Target Nemoris installation directory
|
|
37
|
-
* @param {string} options.liveRoot - Source OpenClaw directory
|
|
38
|
-
* @param {boolean} options.dryRun - If true, do not write any files
|
|
39
|
-
* @returns {Promise<Object>} Migration report
|
|
40
|
-
*/
|
|
41
|
-
export async function runMigration({ installDir, liveRoot, dryRun, sessionSearch = null }) {
|
|
42
|
-
const bridge = new OpenClawShadowBridge({ liveRoot });
|
|
43
|
-
const report = {
|
|
44
|
-
agentsMigrated: [],
|
|
45
|
-
agentsSkipped: [],
|
|
46
|
-
cronJobsMigrated: [],
|
|
47
|
-
cronJobsSkipped: [],
|
|
48
|
-
telegramConfigMigrated: false,
|
|
49
|
-
telegramConfigSkipped: false,
|
|
50
|
-
memoryImported: 0,
|
|
51
|
-
memorySkipped: false,
|
|
52
|
-
workspaceDocsImported: 0,
|
|
53
|
-
agentSessionsIndexed: 0,
|
|
54
|
-
messagesIndexed: 0,
|
|
55
|
-
launchctlHint: null,
|
|
56
|
-
warnings: [],
|
|
57
|
-
dryRun
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Check if liveRoot exists
|
|
61
|
-
const rootStat = await statPath(liveRoot);
|
|
62
|
-
if (!rootStat) {
|
|
63
|
-
throw new Error(`OpenClaw live root not found at ${liveRoot}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 1. Agents
|
|
67
|
-
const agents = await bridge.listAgents();
|
|
68
|
-
for (const agent of agents) {
|
|
69
|
-
const agentFile = path.join(installDir, "config", "agents", `${agent.id}.toml`);
|
|
70
|
-
const exists = await statPath(agentFile);
|
|
71
|
-
if (exists) {
|
|
72
|
-
report.agentsSkipped.push(agent.id);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const primaryLane = modelToLane(agent.primaryModel);
|
|
77
|
-
|
|
78
|
-
const soulRef = path.join(installDir, "config", "identity", `${agent.id}-soul.md`);
|
|
79
|
-
const purposeRef = path.join(installDir, "config", "identity", `${agent.id}-purpose.md`);
|
|
80
|
-
|
|
81
|
-
const tomlLines = [];
|
|
82
|
-
tomlLines.push(`# Generated by nemoris migrate — edit to personalise`);
|
|
83
|
-
if (agent.primaryModel) {
|
|
84
|
-
tomlLines.push(`# openclaw_model = "${escapeTOMLString(agent.primaryModel)}"`);
|
|
85
|
-
}
|
|
86
|
-
tomlLines.push(`id = "${agent.id}"`);
|
|
87
|
-
tomlLines.push(`primary_lane = "${primaryLane}"`);
|
|
88
|
-
tomlLines.push(`memory_policy = "default"`);
|
|
89
|
-
tomlLines.push(`tool_policy = "interactive_safe"`);
|
|
90
|
-
tomlLines.push(`soul_ref = "${soulRef}"`);
|
|
91
|
-
tomlLines.push(`purpose_ref = "${purposeRef}"`);
|
|
92
|
-
tomlLines.push(`workspace_root = "${agent.workspace}"`);
|
|
93
|
-
tomlLines.push(`workspace_context_files = ["MEMORY.md", "AGENTS.md"]`);
|
|
94
|
-
tomlLines.push(`workspace_context_cap = 8000`);
|
|
95
|
-
tomlLines.push(`checkpoint_policy = "compact"`);
|
|
96
|
-
if (agent.skills && agent.skills.length > 0) {
|
|
97
|
-
tomlLines.push(`skills = [${agent.skills.map(s => `"${escapeTOMLString(s)}"`).join(", ")}]`);
|
|
98
|
-
report.warnings.push(`Skills imported as references for ${agent.id} — Nemoris skill registry not yet active.`);
|
|
99
|
-
}
|
|
100
|
-
if (agent.deniedTools && agent.deniedTools.length > 0) {
|
|
101
|
-
tomlLines.push(`tools_deny = [${agent.deniedTools.map(t => `"${escapeTOMLString(t)}"`).join(", ")}]`);
|
|
102
|
-
}
|
|
103
|
-
tomlLines.push(``);
|
|
104
|
-
tomlLines.push(`[limits]`);
|
|
105
|
-
tomlLines.push(`max_tokens_per_turn = 16000`);
|
|
106
|
-
tomlLines.push(`max_tool_calls_per_turn = 6`);
|
|
107
|
-
tomlLines.push(`max_runtime_seconds = 120`);
|
|
108
|
-
tomlLines.push(``);
|
|
109
|
-
tomlLines.push(`[access]`);
|
|
110
|
-
tomlLines.push(`workspace = "rw"`);
|
|
111
|
-
tomlLines.push(`network = "restricted"`);
|
|
112
|
-
const toml = tomlLines.join("\n") + "\n";
|
|
113
|
-
|
|
114
|
-
if (!dryRun) {
|
|
115
|
-
await ensureDir(path.dirname(agentFile));
|
|
116
|
-
await fs.writeFile(agentFile, toml, "utf8");
|
|
117
|
-
}
|
|
118
|
-
report.agentsMigrated.push(agent.id);
|
|
119
|
-
|
|
120
|
-
// Generate soul/purpose files — use real content if available, stub as last resort
|
|
121
|
-
if (!dryRun) {
|
|
122
|
-
const identityDir = path.join(installDir, "config", "identity");
|
|
123
|
-
await ensureDir(identityDir);
|
|
124
|
-
|
|
125
|
-
const soulPath = path.join(identityDir, `${agent.id}-soul.md`);
|
|
126
|
-
if (!(await statPath(soulPath))) {
|
|
127
|
-
const realSoul = await bridge.readIdentityFile(agent, "SOUL.md");
|
|
128
|
-
if (realSoul) {
|
|
129
|
-
await fs.writeFile(soulPath, `# Imported from OpenClaw — edit to personalise\n\n${realSoul}\n`, "utf8");
|
|
130
|
-
} else {
|
|
131
|
-
await fs.writeFile(soulPath, [
|
|
132
|
-
`# ${agent.name} — Soul`,
|
|
133
|
-
``,
|
|
134
|
-
`<!-- Generated by nemoris migrate — edit to personalise -->`,
|
|
135
|
-
``,
|
|
136
|
-
`You are ${agent.name}, an AI assistant.`,
|
|
137
|
-
``
|
|
138
|
-
].join("\n"), "utf8");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const purposePath = path.join(identityDir, `${agent.id}-purpose.md`);
|
|
143
|
-
if (!(await statPath(purposePath))) {
|
|
144
|
-
// Check IDENTITY.md as the OpenClaw equivalent of purpose
|
|
145
|
-
const realPurpose = await bridge.readIdentityFile(agent, "IDENTITY.md");
|
|
146
|
-
if (realPurpose) {
|
|
147
|
-
await fs.writeFile(purposePath, `# Imported from OpenClaw — edit to personalise\n\n${realPurpose}\n`, "utf8");
|
|
148
|
-
} else {
|
|
149
|
-
await fs.writeFile(purposePath, [
|
|
150
|
-
`# ${agent.name} — Purpose`,
|
|
151
|
-
``,
|
|
152
|
-
`<!-- Generated by nemoris migrate — edit to personalise -->`,
|
|
153
|
-
``,
|
|
154
|
-
`Help your user accomplish their goals efficiently.`,
|
|
155
|
-
``
|
|
156
|
-
].join("\n"), "utf8");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 1b. Memory import for migrated agents
|
|
163
|
-
const memoryRootDir = path.join(installDir, "state", "memory");
|
|
164
|
-
for (const agentId of report.agentsMigrated) {
|
|
165
|
-
try {
|
|
166
|
-
const memoryStore = new MemoryStore({ rootDir: memoryRootDir, agentId });
|
|
167
|
-
const existing = await memoryStore.listAll(agentId, Date.now(), { allowCrossAgentRead: true });
|
|
168
|
-
if (existing.length > 0) {
|
|
169
|
-
report.memorySkipped = true;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (dryRun) {
|
|
174
|
-
const snapshot = await bridge.buildWorkspaceSnapshot(agentId);
|
|
175
|
-
report.memoryImported += snapshot.recentMemory.length;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const policy = {
|
|
180
|
-
allow_durable_writes: true,
|
|
181
|
-
categories: { allowed: ["fact", "artifact_summary"] },
|
|
182
|
-
max_writes_per_run: 999,
|
|
183
|
-
};
|
|
184
|
-
const importResult = await bridge.importWorkspaceSnapshot(agentId, memoryStore, policy, {
|
|
185
|
-
memoryImportLimit: Infinity,
|
|
186
|
-
workspaceOverride: null,
|
|
187
|
-
});
|
|
188
|
-
report.memoryImported = importResult.importStats.importedFacts;
|
|
189
|
-
} catch (err) {
|
|
190
|
-
report.warnings.push(`Memory import failed for ${agentId}: ${err.message}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 1c. Workspace docs copy for migrated agents
|
|
195
|
-
for (const agentId of report.agentsMigrated) {
|
|
196
|
-
try {
|
|
197
|
-
const docs = await bridge.readWorkspaceDocs(agentId);
|
|
198
|
-
if (docs.length > 0 && !dryRun) {
|
|
199
|
-
const wsDir = path.join(installDir, "state", "workspace");
|
|
200
|
-
await ensureDir(wsDir);
|
|
201
|
-
for (const doc of docs) {
|
|
202
|
-
const targetPath = path.join(wsDir, doc.fileName);
|
|
203
|
-
if (!(await statPath(targetPath))) {
|
|
204
|
-
await fs.writeFile(targetPath, doc.content, "utf8");
|
|
205
|
-
report.workspaceDocsImported++;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} else if (docs.length > 0 && dryRun) {
|
|
209
|
-
report.workspaceDocsImported += docs.length;
|
|
210
|
-
}
|
|
211
|
-
} catch (err) {
|
|
212
|
-
report.warnings.push(`Workspace docs copy failed for ${agentId}: ${err.message}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 1d. Session history import for FTS5 (if sessionSearch provided).
|
|
217
|
-
// Note: only indexes agents in report.agentsMigrated (first-time migrations).
|
|
218
|
-
// Agents already in report.agentsSkipped (existing .toml) are not re-indexed;
|
|
219
|
-
// this is intentional idempotency — call with a fresh sessionSearch to reindex.
|
|
220
|
-
if (sessionSearch && !dryRun) {
|
|
221
|
-
for (const agentId of report.agentsMigrated) {
|
|
222
|
-
try {
|
|
223
|
-
const messages = await bridge.readSessionMessages(agentId);
|
|
224
|
-
let indexed = 0;
|
|
225
|
-
for (const msg of messages) {
|
|
226
|
-
try {
|
|
227
|
-
sessionSearch.indexEvent(msg);
|
|
228
|
-
indexed++;
|
|
229
|
-
} catch (err) {
|
|
230
|
-
report.warnings.push(`Failed to index message ${msg.id}: ${err.message}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (indexed > 0) {
|
|
234
|
-
report.agentSessionsIndexed++;
|
|
235
|
-
report.messagesIndexed += indexed;
|
|
236
|
-
}
|
|
237
|
-
} catch (err) {
|
|
238
|
-
report.warnings.push(`Session index failed for ${agentId}: ${err.message}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 2. Cron Jobs
|
|
244
|
-
const jobs = await bridge.loadCronJobs();
|
|
245
|
-
for (const job of jobs) {
|
|
246
|
-
const jobFile = path.join(installDir, "config", "jobs", `${job.id}.toml`);
|
|
247
|
-
const exists = await statPath(jobFile);
|
|
248
|
-
if (exists) {
|
|
249
|
-
report.cronJobsSkipped.push(job.id);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Convert OpenClaw job to Nemoris TOML
|
|
254
|
-
if (job.id && job.schedule) {
|
|
255
|
-
let trigger;
|
|
256
|
-
if (job.schedule.kind === "interval") {
|
|
257
|
-
trigger = `every:${job.schedule.expr || "30m"}`;
|
|
258
|
-
} else {
|
|
259
|
-
trigger = `cron:${job.schedule.expr || "0 * * * *"}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const toml = [
|
|
263
|
-
`# Generated by nemoris migrate — edit to personalise`,
|
|
264
|
-
`id = "${escapeTOMLString(job.id)}"`,
|
|
265
|
-
`trigger = "${escapeTOMLString(trigger)}"`,
|
|
266
|
-
`task_type = "${escapeTOMLString(job.id.toLowerCase())}"`,
|
|
267
|
-
`agent_id = "${escapeTOMLString(job.agentId || "ops")}"`,
|
|
268
|
-
`model_lane = "local_report"`,
|
|
269
|
-
`source = "openclaw-import"`,
|
|
270
|
-
``,
|
|
271
|
-
`[budget]`,
|
|
272
|
-
`max_tokens = 4000`,
|
|
273
|
-
`max_runtime_seconds = 120`,
|
|
274
|
-
``,
|
|
275
|
-
`[retry]`,
|
|
276
|
-
`max_attempts = 1`,
|
|
277
|
-
].join("\n") + "\n";
|
|
278
|
-
|
|
279
|
-
if (!dryRun) {
|
|
280
|
-
await ensureDir(path.dirname(jobFile));
|
|
281
|
-
await fs.writeFile(jobFile, toml, "utf8");
|
|
282
|
-
}
|
|
283
|
-
report.cronJobsMigrated.push(job.id);
|
|
284
|
-
} else {
|
|
285
|
-
report.warnings.push(`Skipped invalid cron job: ${job.id || "unknown"}`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 3. Telegram Config
|
|
290
|
-
const ocConfig = await bridge.loadConfig();
|
|
291
|
-
const tg = ocConfig.telegram;
|
|
292
|
-
if (tg && (tg.botTokenEnv || tg.operatorChatId)) {
|
|
293
|
-
const runtimeFile = path.join(installDir, "config", "runtime.toml");
|
|
294
|
-
const runtimeContent = await readText(runtimeFile, "");
|
|
295
|
-
|
|
296
|
-
if (runtimeContent.includes("[telegram]")) {
|
|
297
|
-
report.telegramConfigSkipped = true;
|
|
298
|
-
} else {
|
|
299
|
-
const telegramToml = [
|
|
300
|
-
"",
|
|
301
|
-
"[telegram]",
|
|
302
|
-
tg.botTokenEnv ? `botTokenEnv = "${escapeTOMLString(tg.botTokenEnv)}"` : null,
|
|
303
|
-
tg.operatorChatId ? `operatorChatId = "${escapeTOMLString(String(tg.operatorChatId))}"` : null,
|
|
304
|
-
"pollingMode = true"
|
|
305
|
-
].filter(Boolean).join("\n") + "\n";
|
|
306
|
-
|
|
307
|
-
if (!dryRun) {
|
|
308
|
-
await fs.appendFile(runtimeFile, telegramToml, "utf8");
|
|
309
|
-
}
|
|
310
|
-
report.telegramConfigMigrated = true;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 4. launchctl suggestion (macOS only, non-dry-run only)
|
|
315
|
-
if (!dryRun && process.platform === "darwin") {
|
|
316
|
-
const plistPath = path.join(process.env.HOME || "", "Library", "LaunchAgents", "ai.openclaw.daemon.plist");
|
|
317
|
-
if (await statPath(plistPath)) {
|
|
318
|
-
report.launchctlHint = `To disable OpenClaw: launchctl unload ${plistPath}`;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return report;
|
|
323
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { OpenClawShadowBridge } from "../shadow/bridge.js";
|
|
4
|
+
import { MemoryStore } from "../memory/memory-store.js";
|
|
5
|
+
import { ensureDir, readText, statPath } from "../utils/fs.js";
|
|
6
|
+
|
|
7
|
+
const MODEL_LANE_MAP = {
|
|
8
|
+
"anthropic/claude-opus-4-6": "interactive_primary",
|
|
9
|
+
"anthropic/claude-opus-4": "interactive_primary",
|
|
10
|
+
"anthropic/claude-sonnet-4-6": "interactive_primary",
|
|
11
|
+
"anthropic/claude-sonnet-4": "interactive_primary",
|
|
12
|
+
"anthropic/claude-haiku-4-5": "local_cheap",
|
|
13
|
+
"anthropic/claude-haiku-4": "local_cheap",
|
|
14
|
+
"openrouter/openai/gpt-5.2": "interactive_primary",
|
|
15
|
+
"openrouter/anthropic/claude-haiku-4-5": "local_cheap",
|
|
16
|
+
"ollama/qwen3:8b": "local_primary",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function modelToLane(primaryModel) {
|
|
20
|
+
if (!primaryModel) return "interactive_primary";
|
|
21
|
+
if (MODEL_LANE_MAP[primaryModel]) return MODEL_LANE_MAP[primaryModel];
|
|
22
|
+
// Fallback: contains-based detection
|
|
23
|
+
if (primaryModel.includes("haiku")) return "local_cheap";
|
|
24
|
+
if (primaryModel.startsWith("ollama/")) return "local_primary";
|
|
25
|
+
return "interactive_primary";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeTOMLString(s) {
|
|
29
|
+
return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Migrate agent configurations, cron jobs, and preferences from OpenClaw to Nemoris.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} options
|
|
36
|
+
* @param {string} options.installDir - Target Nemoris installation directory
|
|
37
|
+
* @param {string} options.liveRoot - Source OpenClaw directory
|
|
38
|
+
* @param {boolean} options.dryRun - If true, do not write any files
|
|
39
|
+
* @returns {Promise<Object>} Migration report
|
|
40
|
+
*/
|
|
41
|
+
export async function runMigration({ installDir, liveRoot, dryRun, sessionSearch = null }) {
|
|
42
|
+
const bridge = new OpenClawShadowBridge({ liveRoot });
|
|
43
|
+
const report = {
|
|
44
|
+
agentsMigrated: [],
|
|
45
|
+
agentsSkipped: [],
|
|
46
|
+
cronJobsMigrated: [],
|
|
47
|
+
cronJobsSkipped: [],
|
|
48
|
+
telegramConfigMigrated: false,
|
|
49
|
+
telegramConfigSkipped: false,
|
|
50
|
+
memoryImported: 0,
|
|
51
|
+
memorySkipped: false,
|
|
52
|
+
workspaceDocsImported: 0,
|
|
53
|
+
agentSessionsIndexed: 0,
|
|
54
|
+
messagesIndexed: 0,
|
|
55
|
+
launchctlHint: null,
|
|
56
|
+
warnings: [],
|
|
57
|
+
dryRun
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Check if liveRoot exists
|
|
61
|
+
const rootStat = await statPath(liveRoot);
|
|
62
|
+
if (!rootStat) {
|
|
63
|
+
throw new Error(`OpenClaw live root not found at ${liveRoot}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1. Agents
|
|
67
|
+
const agents = await bridge.listAgents();
|
|
68
|
+
for (const agent of agents) {
|
|
69
|
+
const agentFile = path.join(installDir, "config", "agents", `${agent.id}.toml`);
|
|
70
|
+
const exists = await statPath(agentFile);
|
|
71
|
+
if (exists) {
|
|
72
|
+
report.agentsSkipped.push(agent.id);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const primaryLane = modelToLane(agent.primaryModel);
|
|
77
|
+
|
|
78
|
+
const soulRef = path.join(installDir, "config", "identity", `${agent.id}-soul.md`);
|
|
79
|
+
const purposeRef = path.join(installDir, "config", "identity", `${agent.id}-purpose.md`);
|
|
80
|
+
|
|
81
|
+
const tomlLines = [];
|
|
82
|
+
tomlLines.push(`# Generated by nemoris migrate — edit to personalise`);
|
|
83
|
+
if (agent.primaryModel) {
|
|
84
|
+
tomlLines.push(`# openclaw_model = "${escapeTOMLString(agent.primaryModel)}"`);
|
|
85
|
+
}
|
|
86
|
+
tomlLines.push(`id = "${agent.id}"`);
|
|
87
|
+
tomlLines.push(`primary_lane = "${primaryLane}"`);
|
|
88
|
+
tomlLines.push(`memory_policy = "default"`);
|
|
89
|
+
tomlLines.push(`tool_policy = "interactive_safe"`);
|
|
90
|
+
tomlLines.push(`soul_ref = "${soulRef}"`);
|
|
91
|
+
tomlLines.push(`purpose_ref = "${purposeRef}"`);
|
|
92
|
+
tomlLines.push(`workspace_root = "${agent.workspace}"`);
|
|
93
|
+
tomlLines.push(`workspace_context_files = ["MEMORY.md", "AGENTS.md"]`);
|
|
94
|
+
tomlLines.push(`workspace_context_cap = 8000`);
|
|
95
|
+
tomlLines.push(`checkpoint_policy = "compact"`);
|
|
96
|
+
if (agent.skills && agent.skills.length > 0) {
|
|
97
|
+
tomlLines.push(`skills = [${agent.skills.map(s => `"${escapeTOMLString(s)}"`).join(", ")}]`);
|
|
98
|
+
report.warnings.push(`Skills imported as references for ${agent.id} — Nemoris skill registry not yet active.`);
|
|
99
|
+
}
|
|
100
|
+
if (agent.deniedTools && agent.deniedTools.length > 0) {
|
|
101
|
+
tomlLines.push(`tools_deny = [${agent.deniedTools.map(t => `"${escapeTOMLString(t)}"`).join(", ")}]`);
|
|
102
|
+
}
|
|
103
|
+
tomlLines.push(``);
|
|
104
|
+
tomlLines.push(`[limits]`);
|
|
105
|
+
tomlLines.push(`max_tokens_per_turn = 16000`);
|
|
106
|
+
tomlLines.push(`max_tool_calls_per_turn = 6`);
|
|
107
|
+
tomlLines.push(`max_runtime_seconds = 120`);
|
|
108
|
+
tomlLines.push(``);
|
|
109
|
+
tomlLines.push(`[access]`);
|
|
110
|
+
tomlLines.push(`workspace = "rw"`);
|
|
111
|
+
tomlLines.push(`network = "restricted"`);
|
|
112
|
+
const toml = tomlLines.join("\n") + "\n";
|
|
113
|
+
|
|
114
|
+
if (!dryRun) {
|
|
115
|
+
await ensureDir(path.dirname(agentFile));
|
|
116
|
+
await fs.writeFile(agentFile, toml, "utf8");
|
|
117
|
+
}
|
|
118
|
+
report.agentsMigrated.push(agent.id);
|
|
119
|
+
|
|
120
|
+
// Generate soul/purpose files — use real content if available, stub as last resort
|
|
121
|
+
if (!dryRun) {
|
|
122
|
+
const identityDir = path.join(installDir, "config", "identity");
|
|
123
|
+
await ensureDir(identityDir);
|
|
124
|
+
|
|
125
|
+
const soulPath = path.join(identityDir, `${agent.id}-soul.md`);
|
|
126
|
+
if (!(await statPath(soulPath))) {
|
|
127
|
+
const realSoul = await bridge.readIdentityFile(agent, "SOUL.md");
|
|
128
|
+
if (realSoul) {
|
|
129
|
+
await fs.writeFile(soulPath, `# Imported from OpenClaw — edit to personalise\n\n${realSoul}\n`, "utf8");
|
|
130
|
+
} else {
|
|
131
|
+
await fs.writeFile(soulPath, [
|
|
132
|
+
`# ${agent.name} — Soul`,
|
|
133
|
+
``,
|
|
134
|
+
`<!-- Generated by nemoris migrate — edit to personalise -->`,
|
|
135
|
+
``,
|
|
136
|
+
`You are ${agent.name}, an AI assistant.`,
|
|
137
|
+
``
|
|
138
|
+
].join("\n"), "utf8");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const purposePath = path.join(identityDir, `${agent.id}-purpose.md`);
|
|
143
|
+
if (!(await statPath(purposePath))) {
|
|
144
|
+
// Check IDENTITY.md as the OpenClaw equivalent of purpose
|
|
145
|
+
const realPurpose = await bridge.readIdentityFile(agent, "IDENTITY.md");
|
|
146
|
+
if (realPurpose) {
|
|
147
|
+
await fs.writeFile(purposePath, `# Imported from OpenClaw — edit to personalise\n\n${realPurpose}\n`, "utf8");
|
|
148
|
+
} else {
|
|
149
|
+
await fs.writeFile(purposePath, [
|
|
150
|
+
`# ${agent.name} — Purpose`,
|
|
151
|
+
``,
|
|
152
|
+
`<!-- Generated by nemoris migrate — edit to personalise -->`,
|
|
153
|
+
``,
|
|
154
|
+
`Help your user accomplish their goals efficiently.`,
|
|
155
|
+
``
|
|
156
|
+
].join("\n"), "utf8");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 1b. Memory import for migrated agents
|
|
163
|
+
const memoryRootDir = path.join(installDir, "state", "memory");
|
|
164
|
+
for (const agentId of report.agentsMigrated) {
|
|
165
|
+
try {
|
|
166
|
+
const memoryStore = new MemoryStore({ rootDir: memoryRootDir, agentId });
|
|
167
|
+
const existing = await memoryStore.listAll(agentId, Date.now(), { allowCrossAgentRead: true });
|
|
168
|
+
if (existing.length > 0) {
|
|
169
|
+
report.memorySkipped = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
const snapshot = await bridge.buildWorkspaceSnapshot(agentId);
|
|
175
|
+
report.memoryImported += snapshot.recentMemory.length;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const policy = {
|
|
180
|
+
allow_durable_writes: true,
|
|
181
|
+
categories: { allowed: ["fact", "artifact_summary"] },
|
|
182
|
+
max_writes_per_run: 999,
|
|
183
|
+
};
|
|
184
|
+
const importResult = await bridge.importWorkspaceSnapshot(agentId, memoryStore, policy, {
|
|
185
|
+
memoryImportLimit: Infinity,
|
|
186
|
+
workspaceOverride: null,
|
|
187
|
+
});
|
|
188
|
+
report.memoryImported = importResult.importStats.importedFacts;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
report.warnings.push(`Memory import failed for ${agentId}: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 1c. Workspace docs copy for migrated agents
|
|
195
|
+
for (const agentId of report.agentsMigrated) {
|
|
196
|
+
try {
|
|
197
|
+
const docs = await bridge.readWorkspaceDocs(agentId);
|
|
198
|
+
if (docs.length > 0 && !dryRun) {
|
|
199
|
+
const wsDir = path.join(installDir, "state", "workspace");
|
|
200
|
+
await ensureDir(wsDir);
|
|
201
|
+
for (const doc of docs) {
|
|
202
|
+
const targetPath = path.join(wsDir, doc.fileName);
|
|
203
|
+
if (!(await statPath(targetPath))) {
|
|
204
|
+
await fs.writeFile(targetPath, doc.content, "utf8");
|
|
205
|
+
report.workspaceDocsImported++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (docs.length > 0 && dryRun) {
|
|
209
|
+
report.workspaceDocsImported += docs.length;
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
report.warnings.push(`Workspace docs copy failed for ${agentId}: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 1d. Session history import for FTS5 (if sessionSearch provided).
|
|
217
|
+
// Note: only indexes agents in report.agentsMigrated (first-time migrations).
|
|
218
|
+
// Agents already in report.agentsSkipped (existing .toml) are not re-indexed;
|
|
219
|
+
// this is intentional idempotency — call with a fresh sessionSearch to reindex.
|
|
220
|
+
if (sessionSearch && !dryRun) {
|
|
221
|
+
for (const agentId of report.agentsMigrated) {
|
|
222
|
+
try {
|
|
223
|
+
const messages = await bridge.readSessionMessages(agentId);
|
|
224
|
+
let indexed = 0;
|
|
225
|
+
for (const msg of messages) {
|
|
226
|
+
try {
|
|
227
|
+
sessionSearch.indexEvent(msg);
|
|
228
|
+
indexed++;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
report.warnings.push(`Failed to index message ${msg.id}: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (indexed > 0) {
|
|
234
|
+
report.agentSessionsIndexed++;
|
|
235
|
+
report.messagesIndexed += indexed;
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
report.warnings.push(`Session index failed for ${agentId}: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Cron Jobs
|
|
244
|
+
const jobs = await bridge.loadCronJobs();
|
|
245
|
+
for (const job of jobs) {
|
|
246
|
+
const jobFile = path.join(installDir, "config", "jobs", `${job.id}.toml`);
|
|
247
|
+
const exists = await statPath(jobFile);
|
|
248
|
+
if (exists) {
|
|
249
|
+
report.cronJobsSkipped.push(job.id);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Convert OpenClaw job to Nemoris TOML
|
|
254
|
+
if (job.id && job.schedule) {
|
|
255
|
+
let trigger;
|
|
256
|
+
if (job.schedule.kind === "interval") {
|
|
257
|
+
trigger = `every:${job.schedule.expr || "30m"}`;
|
|
258
|
+
} else {
|
|
259
|
+
trigger = `cron:${job.schedule.expr || "0 * * * *"}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const toml = [
|
|
263
|
+
`# Generated by nemoris migrate — edit to personalise`,
|
|
264
|
+
`id = "${escapeTOMLString(job.id)}"`,
|
|
265
|
+
`trigger = "${escapeTOMLString(trigger)}"`,
|
|
266
|
+
`task_type = "${escapeTOMLString(job.id.toLowerCase())}"`,
|
|
267
|
+
`agent_id = "${escapeTOMLString(job.agentId || "ops")}"`,
|
|
268
|
+
`model_lane = "local_report"`,
|
|
269
|
+
`source = "openclaw-import"`,
|
|
270
|
+
``,
|
|
271
|
+
`[budget]`,
|
|
272
|
+
`max_tokens = 4000`,
|
|
273
|
+
`max_runtime_seconds = 120`,
|
|
274
|
+
``,
|
|
275
|
+
`[retry]`,
|
|
276
|
+
`max_attempts = 1`,
|
|
277
|
+
].join("\n") + "\n";
|
|
278
|
+
|
|
279
|
+
if (!dryRun) {
|
|
280
|
+
await ensureDir(path.dirname(jobFile));
|
|
281
|
+
await fs.writeFile(jobFile, toml, "utf8");
|
|
282
|
+
}
|
|
283
|
+
report.cronJobsMigrated.push(job.id);
|
|
284
|
+
} else {
|
|
285
|
+
report.warnings.push(`Skipped invalid cron job: ${job.id || "unknown"}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3. Telegram Config
|
|
290
|
+
const ocConfig = await bridge.loadConfig();
|
|
291
|
+
const tg = ocConfig.telegram;
|
|
292
|
+
if (tg && (tg.botTokenEnv || tg.operatorChatId)) {
|
|
293
|
+
const runtimeFile = path.join(installDir, "config", "runtime.toml");
|
|
294
|
+
const runtimeContent = await readText(runtimeFile, "");
|
|
295
|
+
|
|
296
|
+
if (runtimeContent.includes("[telegram]")) {
|
|
297
|
+
report.telegramConfigSkipped = true;
|
|
298
|
+
} else {
|
|
299
|
+
const telegramToml = [
|
|
300
|
+
"",
|
|
301
|
+
"[telegram]",
|
|
302
|
+
tg.botTokenEnv ? `botTokenEnv = "${escapeTOMLString(tg.botTokenEnv)}"` : null,
|
|
303
|
+
tg.operatorChatId ? `operatorChatId = "${escapeTOMLString(String(tg.operatorChatId))}"` : null,
|
|
304
|
+
"pollingMode = true"
|
|
305
|
+
].filter(Boolean).join("\n") + "\n";
|
|
306
|
+
|
|
307
|
+
if (!dryRun) {
|
|
308
|
+
await fs.appendFile(runtimeFile, telegramToml, "utf8");
|
|
309
|
+
}
|
|
310
|
+
report.telegramConfigMigrated = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 4. launchctl suggestion (macOS only, non-dry-run only)
|
|
315
|
+
if (!dryRun && process.platform === "darwin") {
|
|
316
|
+
const plistPath = path.join(process.env.HOME || "", "Library", "LaunchAgents", "ai.openclaw.daemon.plist");
|
|
317
|
+
if (await statPath(plistPath)) {
|
|
318
|
+
report.launchctlHint = `To disable OpenClaw: launchctl unload ${plistPath}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return report;
|
|
323
|
+
}
|