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.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- 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
|
+
}
|