nemoris 0.1.0

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.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,78 @@
1
+ const MODEL_TIER_MAP = Object.freeze({
2
+ sonnet: "anthropic/claude-sonnet-4-6",
3
+ haiku: "anthropic/claude-haiku-4-5",
4
+ opus: "anthropic/claude-opus-4-6",
5
+ });
6
+
7
+ const CLAUDE_MODEL_BY_TIER = Object.freeze({
8
+ haiku: "claude-haiku-4-5",
9
+ sonnet: "claude-sonnet-4-6",
10
+ opus: "claude-opus-4-6",
11
+ });
12
+
13
+ function getLaneValue(lane, camelKey, snakeKey) {
14
+ if (!lane || typeof lane !== "object") return null;
15
+ return lane[camelKey] ?? lane[snakeKey] ?? null;
16
+ }
17
+
18
+ export function getInteractivePrimaryLane(routerConfig = null) {
19
+ return routerConfig?.interactive_primary || routerConfig?.interactivePrimary || null;
20
+ }
21
+
22
+ function inferAnthropicPrefix(routerConfig = null) {
23
+ const lane = getInteractivePrimaryLane(routerConfig);
24
+ const sourceModel = getLaneValue(lane, "manualBump", "manual_bump")
25
+ || getLaneValue(lane, "primary", "primary");
26
+
27
+ if (typeof sourceModel !== "string") return null;
28
+ if (sourceModel.startsWith("openrouter/anthropic/")) return "openrouter/anthropic";
29
+ if (sourceModel.startsWith("anthropic/")) return "anthropic";
30
+ return null;
31
+ }
32
+
33
+ function inferAnthropicModelForTier(tier, { routerConfig = null } = {}) {
34
+ const prefix = inferAnthropicPrefix(routerConfig);
35
+ const modelName = CLAUDE_MODEL_BY_TIER[tier] || null;
36
+ if (!prefix || !modelName) return null;
37
+ return `${prefix}/${modelName}`;
38
+ }
39
+
40
+ export function resolveModelOverride(modelOverride, { routerConfig = null } = {}) {
41
+ if (!modelOverride || modelOverride === "default") {
42
+ return null;
43
+ }
44
+ if (modelOverride.includes("/")) {
45
+ return modelOverride;
46
+ }
47
+
48
+ const interactivePrimaryLane = getInteractivePrimaryLane(routerConfig);
49
+ if (modelOverride === "haiku") {
50
+ return getLaneValue(interactivePrimaryLane, "primary", "primary")
51
+ || inferAnthropicModelForTier("haiku", { routerConfig })
52
+ || MODEL_TIER_MAP.haiku;
53
+ }
54
+ if (modelOverride === "sonnet") {
55
+ return getLaneValue(interactivePrimaryLane, "manualBump", "manual_bump")
56
+ || inferAnthropicModelForTier("sonnet", { routerConfig })
57
+ || MODEL_TIER_MAP.sonnet;
58
+ }
59
+ if (modelOverride === "opus") {
60
+ return inferAnthropicModelForTier("opus", { routerConfig }) || MODEL_TIER_MAP.opus;
61
+ }
62
+
63
+ return MODEL_TIER_MAP[modelOverride] || modelOverride;
64
+ }
65
+
66
+ export function resolveSessionStatusModel(session, { routerConfig = null, agentConfigs = null } = {}) {
67
+ const modelOverride = resolveModelOverride(session?.model_override, { routerConfig });
68
+ if (modelOverride) {
69
+ return modelOverride;
70
+ }
71
+
72
+ const agentConfig = agentConfigs?.[session?.agent_id] || null;
73
+ const primaryLane = agentConfig?.primaryLane || agentConfig?.primary_lane || "interactive_primary";
74
+ const resolvedModel = getLaneValue(routerConfig?.[primaryLane], "primary", "primary");
75
+ const interactivePrimaryModel = getLaneValue(getInteractivePrimaryLane(routerConfig), "primary", "primary");
76
+
77
+ return resolvedModel || interactivePrimaryModel || "default";
78
+ }
@@ -0,0 +1,64 @@
1
+ export function classifyNetworkFailure(errorOrMessage, { surface = "runtime" } = {}) {
2
+ const error = typeof errorOrMessage === "string" ? null : errorOrMessage;
3
+ const message = String(error?.message || errorOrMessage || "");
4
+ const normalized = message.toLowerCase();
5
+ const code = String(error?.code || "").toUpperCase();
6
+ const errno = String(error?.errno || "").toUpperCase();
7
+ const signalTimedOut =
8
+ error?.name === "TimeoutError" ||
9
+ error?.name === "AbortError" ||
10
+ normalized.includes("timed out") ||
11
+ normalized.includes("timeout");
12
+
13
+ let networkClass = null;
14
+ if (signalTimedOut || code === "ETIMEDOUT" || errno === "ETIMEDOUT") {
15
+ networkClass = normalized.includes("read") ? "read_timeout" : "connect_timeout";
16
+ } else if (code === "EHOSTUNREACH" || errno === "EHOSTUNREACH" || normalized.includes("hostunreach")) {
17
+ networkClass = "host_unreachable";
18
+ } else if (code === "ENETUNREACH" || errno === "ENETUNREACH" || normalized.includes("network is unreachable")) {
19
+ networkClass = "network_unreachable";
20
+ } else if (
21
+ code === "ENOTFOUND" ||
22
+ code === "EAI_AGAIN" ||
23
+ errno === "ENOTFOUND" ||
24
+ errno === "EAI_AGAIN" ||
25
+ normalized.includes("getaddrinfo") ||
26
+ normalized.includes("dns")
27
+ ) {
28
+ networkClass = "dns_resolution_failure";
29
+ }
30
+
31
+ let failureClass = null;
32
+ if (surface === "provider") {
33
+ if (signalTimedOut || networkClass === "connect_timeout" || networkClass === "read_timeout") {
34
+ failureClass = "provider_timeout";
35
+ } else if (networkClass) {
36
+ failureClass = networkClass;
37
+ }
38
+ } else if (surface === "delivery") {
39
+ if (signalTimedOut || networkClass === "connect_timeout" || networkClass === "read_timeout") {
40
+ failureClass = "delivery_timeout";
41
+ } else if (networkClass) {
42
+ failureClass = networkClass;
43
+ }
44
+ } else if (networkClass) {
45
+ failureClass = networkClass;
46
+ }
47
+
48
+ return {
49
+ failureClass,
50
+ networkClass
51
+ };
52
+ }
53
+
54
+ export function annotateError(errorOrMessage, options = {}) {
55
+ const error = typeof errorOrMessage === "string" ? new Error(errorOrMessage) : errorOrMessage;
56
+ const classification = classifyNetworkFailure(error, options);
57
+ if (classification.failureClass && !error.failureClass) {
58
+ error.failureClass = classification.failureClass;
59
+ }
60
+ if (classification.networkClass && !error.networkClass) {
61
+ error.networkClass = classification.networkClass;
62
+ }
63
+ return error;
64
+ }
@@ -0,0 +1,97 @@
1
+ import path from "node:path";
2
+ import { ensureDir, listFilesRecursive, readJson, writeJson } from "../utils/fs.js";
3
+ import { buildRetentionPolicy, pruneJsonBuckets } from "./retention.js";
4
+ import { createRuntimeId } from "../utils/ids.js";
5
+
6
+ function stamp() {
7
+ return new Date().toISOString().replace(/[:.]/g, "-");
8
+ }
9
+
10
+ function normalizeFilePath(filePath) {
11
+ return path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
12
+ }
13
+
14
+ export class NotificationStore {
15
+ constructor({ rootDir, retention = {} }) {
16
+ this.rootDir = rootDir;
17
+ this.setRetentionPolicy(retention);
18
+ }
19
+
20
+ setRetentionPolicy(retention = {}) {
21
+ this.retentionPolicy = buildRetentionPolicy(retention, {
22
+ ttlDays: 14,
23
+ maxFilesPerBucket: 1000
24
+ });
25
+ }
26
+
27
+ async saveNotification(jobId, notification) {
28
+ const notificationDir = path.join(this.rootDir, jobId);
29
+ await ensureDir(notificationDir);
30
+ const filePath = path.join(notificationDir, `${stamp()}.json`);
31
+ await writeJson(filePath, {
32
+ id: notification.id || createRuntimeId("notification"),
33
+ ...notification
34
+ });
35
+ await pruneJsonBuckets(this.rootDir, this.retentionPolicy);
36
+ return filePath;
37
+ }
38
+
39
+ async updateNotification(filePath, patch = {}) {
40
+ const resolvedPath = normalizeFilePath(filePath);
41
+ const current = await readJson(resolvedPath, null);
42
+ if (!current) {
43
+ throw new Error(`Notification file not found: ${resolvedPath}`);
44
+ }
45
+ const updated = {
46
+ ...current,
47
+ ...patch
48
+ };
49
+ await writeJson(resolvedPath, updated);
50
+ return {
51
+ filePath: resolvedPath,
52
+ ...updated
53
+ };
54
+ }
55
+
56
+ async getNotification(filePath) {
57
+ const resolvedPath = normalizeFilePath(filePath);
58
+ const current = await readJson(resolvedPath, null);
59
+ if (!current) return null;
60
+ return {
61
+ filePath: resolvedPath,
62
+ ...current
63
+ };
64
+ }
65
+
66
+ async getNotifications(filePaths = []) {
67
+ const notifications = await Promise.all((filePaths || []).map((filePath) => this.getNotification(filePath)));
68
+ return notifications.filter(Boolean);
69
+ }
70
+
71
+ async listRecent(limit = 10) {
72
+ const notifications = await this.listAll();
73
+ return notifications
74
+ .sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")))
75
+ .slice(0, limit);
76
+ }
77
+
78
+ async listAll() {
79
+ const files = (await listFilesRecursive(this.rootDir)).filter((filePath) => filePath.endsWith(".json"));
80
+ const notifications = [];
81
+
82
+ for (const filePath of files) {
83
+ const data = await readJson(filePath, null);
84
+ if (!data) continue;
85
+ notifications.push({
86
+ filePath,
87
+ ...data
88
+ });
89
+ }
90
+
91
+ return notifications.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
92
+ }
93
+
94
+ async prune(options = {}) {
95
+ return pruneJsonBuckets(this.rootDir, this.retentionPolicy, options);
96
+ }
97
+ }