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/shadow/bridge.js
CHANGED
|
@@ -1,303 +1,303 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import { listFilesRecursive, readJson, readJsonLines, readText } from "../utils/fs.js";
|
|
4
|
-
|
|
5
|
-
function sortNewestFirst(items) {
|
|
6
|
-
return [...items].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class OpenClawShadowBridge {
|
|
10
|
-
constructor({ liveRoot }) {
|
|
11
|
-
this.liveRoot = liveRoot;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
get available() {
|
|
15
|
-
return Boolean(this.liveRoot);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async loadConfig() {
|
|
19
|
-
if (!this.available) return {};
|
|
20
|
-
return readJson(path.join(this.liveRoot, "openclaw.json"), {});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async loadCronJobs() {
|
|
24
|
-
if (!this.available) return [];
|
|
25
|
-
const data = await readJson(path.join(this.liveRoot, "cron", "jobs.json"), { jobs: [] });
|
|
26
|
-
return data.jobs || [];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async loadCronRunHistory(jobId, limit = 3) {
|
|
30
|
-
if (!this.available) return [];
|
|
31
|
-
const filePath = path.join(this.liveRoot, "cron", "runs", `${jobId}.jsonl`);
|
|
32
|
-
const entries = await readJsonLines(filePath);
|
|
33
|
-
return entries.slice(-limit).reverse();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async listAgents() {
|
|
37
|
-
const config = await this.loadConfig();
|
|
38
|
-
return (config.agents?.list || []).map((agent) => ({
|
|
39
|
-
id: agent.id,
|
|
40
|
-
name: agent.name,
|
|
41
|
-
workspace: agent.workspace,
|
|
42
|
-
agentDir: agent.agentDir || path.join(this.liveRoot, "agents", agent.id, "agent"),
|
|
43
|
-
primaryModel: agent.model?.primary || null,
|
|
44
|
-
fallbackModels: agent.model?.fallbacks || [],
|
|
45
|
-
deniedTools: agent.tools?.deny || [],
|
|
46
|
-
skills: agent.skills || []
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async loadAgentProfiles(agentId) {
|
|
51
|
-
if (!this.available) return {};
|
|
52
|
-
return readJson(path.join(this.liveRoot, "agents", agentId, "agent", "auth-profiles.json"), {});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async loadAgentModels(agentId) {
|
|
56
|
-
if (!this.available) return {};
|
|
57
|
-
return readJson(path.join(this.liveRoot, "agents", agentId, "agent", "models.json"), {});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async loadSessionIndex(agentId) {
|
|
61
|
-
if (!this.available) return {};
|
|
62
|
-
return readJson(path.join(this.liveRoot, "agents", agentId, "sessions", "sessions.json"), {});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async resolveWorkspacePath(agentId, workspaceOverride = null) {
|
|
66
|
-
const agents = await this.listAgents();
|
|
67
|
-
const agent = agents.find((item) => item.id === agentId);
|
|
68
|
-
if (!agent) {
|
|
69
|
-
if (workspaceOverride) return workspaceOverride;
|
|
70
|
-
throw new Error(`Unknown agent: ${agentId}`);
|
|
71
|
-
}
|
|
72
|
-
return agent.workspace;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Read an identity file (SOUL.md, IDENTITY.md, etc.) with priority:
|
|
77
|
-
* 1. agent.workspace/fileName
|
|
78
|
-
* 2. agent.agentDir/fileName
|
|
79
|
-
* 3. null (caller falls back to stub)
|
|
80
|
-
*
|
|
81
|
-
* Whitespace-only files are treated as absent (same as missing).
|
|
82
|
-
*/
|
|
83
|
-
async readIdentityFile(agent, fileName) {
|
|
84
|
-
// Priority 1: workspace dir
|
|
85
|
-
const wsPath = path.join(agent.workspace, fileName);
|
|
86
|
-
try {
|
|
87
|
-
const content = await fs.readFile(wsPath, "utf8");
|
|
88
|
-
if (content.trim()) return content;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (err.code !== "ENOENT") throw err;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Priority 2: agent dir
|
|
94
|
-
const agentDirPath = path.join(
|
|
95
|
-
agent.agentDir || path.join(this.liveRoot, "agents", agent.id, "agent"),
|
|
96
|
-
fileName
|
|
97
|
-
);
|
|
98
|
-
try {
|
|
99
|
-
const content = await fs.readFile(agentDirPath, "utf8");
|
|
100
|
-
if (content.trim()) return content;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
if (err.code !== "ENOENT") throw err;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Read session messages from OpenClaw JSONL files for FTS5 indexing.
|
|
110
|
-
* Only reads active sessions (skips *.reset.jsonl, *.deleted.jsonl).
|
|
111
|
-
* Returns up to `limit` message events (message_in / message_out).
|
|
112
|
-
*
|
|
113
|
-
* @param {string} agentId
|
|
114
|
-
* @param {number} limit Max messages to return (default 1000)
|
|
115
|
-
* Note: each session file is fully loaded before line filtering; for large
|
|
116
|
-
* installations with many messages, memory usage scales with the largest file.
|
|
117
|
-
*/
|
|
118
|
-
async readSessionMessages(agentId, limit = 1000) {
|
|
119
|
-
if (!this.available) return [];
|
|
120
|
-
const sessionsDir = path.join(this.liveRoot, "agents", agentId, "sessions");
|
|
121
|
-
let fileNames;
|
|
122
|
-
try {
|
|
123
|
-
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
124
|
-
fileNames = entries
|
|
125
|
-
.filter(e => e.isFile() && e.name.endsWith(".jsonl")
|
|
126
|
-
&& !e.name.endsWith(".reset.jsonl")
|
|
127
|
-
&& !e.name.endsWith(".deleted.jsonl"))
|
|
128
|
-
.map(e => e.name);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const messages = [];
|
|
135
|
-
for (const fileName of fileNames) {
|
|
136
|
-
if (messages.length >= limit) break;
|
|
137
|
-
const filePath = path.join(sessionsDir, fileName);
|
|
138
|
-
const lines = await readJsonLines(filePath);
|
|
139
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
140
|
-
if (messages.length >= limit) break;
|
|
141
|
-
const line = lines[lineIdx];
|
|
142
|
-
// Accept events with kind message_in/message_out (Nemoris format)
|
|
143
|
-
// or role user/assistant (OpenClaw legacy format)
|
|
144
|
-
const kind = line.kind || (line.role === "user" ? "message_in" : line.role === "assistant" ? "message_out" : null);
|
|
145
|
-
if (kind !== "message_in" && kind !== "message_out") continue;
|
|
146
|
-
messages.push({
|
|
147
|
-
id: line.id || `oc-${path.basename(fileName, ".jsonl")}-${lineIdx}`,
|
|
148
|
-
session_id: line.session_id || path.basename(fileName, ".jsonl"),
|
|
149
|
-
kind,
|
|
150
|
-
ts: line.ts || 0,
|
|
151
|
-
payload_json: line.payload_json || JSON.stringify({ content: line.content || "" }),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return messages;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async readWorkspaceDocs(agentId, workspaceOverride = null) {
|
|
159
|
-
const workspaceRoot = await this.resolveWorkspacePath(agentId, workspaceOverride);
|
|
160
|
-
const MAX_FILES = 50;
|
|
161
|
-
const MAX_BYTES = 100 * 1024;
|
|
162
|
-
|
|
163
|
-
let fileNames;
|
|
164
|
-
try {
|
|
165
|
-
const entries = await fs.readdir(workspaceRoot, { withFileTypes: true });
|
|
166
|
-
const IDENTITY_FILES = new Set(["SOUL.md", "IDENTITY.md"]);
|
|
167
|
-
fileNames = entries
|
|
168
|
-
.filter(e => e.isFile() && e.name.endsWith(".md") && !IDENTITY_FILES.has(e.name))
|
|
169
|
-
.map(e => e.name)
|
|
170
|
-
.slice(0, MAX_FILES);
|
|
171
|
-
} catch (err) {
|
|
172
|
-
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
173
|
-
throw err;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const docs = [];
|
|
177
|
-
for (const fileName of fileNames) {
|
|
178
|
-
const fullPath = path.join(workspaceRoot, fileName);
|
|
179
|
-
try {
|
|
180
|
-
const content = await fs.readFile(fullPath, "utf8");
|
|
181
|
-
if (content.length <= MAX_BYTES) {
|
|
182
|
-
docs.push({ fileName, fullPath, content });
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
if (err.code !== "ENOENT") throw err;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return docs;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async readRecentWorkspaceMemory(agentId, limit = 5, workspaceOverride = null) {
|
|
193
|
-
const workspaceRoot = await this.resolveWorkspacePath(agentId, workspaceOverride);
|
|
194
|
-
const memoryDir = path.join(workspaceRoot, "memory");
|
|
195
|
-
const allFiles = (await listFilesRecursive(memoryDir)).filter((filePath) => filePath.endsWith(".md"));
|
|
196
|
-
const stats = await Promise.all(
|
|
197
|
-
allFiles.map(async (filePath) => {
|
|
198
|
-
const content = await readText(filePath, "");
|
|
199
|
-
const match = /(\d{4}-\d{2}-\d{2})/.exec(path.basename(filePath));
|
|
200
|
-
const dateHint = match ? new Date(`${match[1]}T00:00:00Z`).getTime() : 0;
|
|
201
|
-
return {
|
|
202
|
-
filePath,
|
|
203
|
-
content,
|
|
204
|
-
mtimeMs: dateHint
|
|
205
|
-
};
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
return sortNewestFirst(stats).slice(0, limit);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async buildWorkspaceSnapshot(agentId, options = {}) {
|
|
213
|
-
const workspaceOverride = options.workspaceOverride || null;
|
|
214
|
-
const canUseLiveAgent = (await this.listAgents()).some((agent) => agent.id === agentId);
|
|
215
|
-
const [docs, recentMemory, sessionIndex, authProfiles, models] = await Promise.all([
|
|
216
|
-
this.readWorkspaceDocs(agentId, workspaceOverride),
|
|
217
|
-
this.readRecentWorkspaceMemory(agentId, 6, workspaceOverride),
|
|
218
|
-
canUseLiveAgent ? this.loadSessionIndex(agentId) : {},
|
|
219
|
-
canUseLiveAgent ? this.loadAgentProfiles(agentId) : {},
|
|
220
|
-
canUseLiveAgent ? this.loadAgentModels(agentId) : {}
|
|
221
|
-
]);
|
|
222
|
-
|
|
223
|
-
const sessionEntries = Object.entries(sessionIndex || {}).map(([sessionKey, value]) => ({
|
|
224
|
-
sessionKey,
|
|
225
|
-
model: value.model || null,
|
|
226
|
-
modelProvider: value.modelProvider || null,
|
|
227
|
-
updatedAt: value.updatedAt || null,
|
|
228
|
-
origin: value.origin || null,
|
|
229
|
-
lastChannel: value.lastChannel || null
|
|
230
|
-
}));
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
agentId,
|
|
234
|
-
docs,
|
|
235
|
-
recentMemory,
|
|
236
|
-
sessionEntries,
|
|
237
|
-
authProfiles,
|
|
238
|
-
models
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async importWorkspaceSnapshot(agentId, memoryStore, policy, options = {}) {
|
|
243
|
-
const snapshot = await this.buildWorkspaceSnapshot(agentId, options);
|
|
244
|
-
const memoryImportLimit = options.memoryImportLimit ?? 5;
|
|
245
|
-
const memorySnippetChars = options.memorySnippetChars ?? 2000;
|
|
246
|
-
let writesThisRun = 0;
|
|
247
|
-
let importedFacts = 0;
|
|
248
|
-
let skippedFacts = 0;
|
|
249
|
-
|
|
250
|
-
await memoryStore.writeSummary(agentId, {
|
|
251
|
-
title: `shadow snapshot ${agentId}`,
|
|
252
|
-
summary: `Imported ${snapshot.docs.length} workspace docs and ${snapshot.recentMemory.length} recent memory files from live OpenClaw in read-only mode.`,
|
|
253
|
-
content: snapshot.docs.map((doc) => `${doc.fileName}: ${doc.fullPath}`).join("\n"),
|
|
254
|
-
category: "artifact_summary",
|
|
255
|
-
sourceKind: "shadow_snapshot",
|
|
256
|
-
salience: 0.75,
|
|
257
|
-
sourceRefs: snapshot.docs.map((doc) => doc.fullPath)
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
for (const entry of snapshot.recentMemory.slice(0, memoryImportLimit)) {
|
|
261
|
-
const result = await memoryStore.writeFact(
|
|
262
|
-
agentId,
|
|
263
|
-
{
|
|
264
|
-
title: path.basename(entry.filePath),
|
|
265
|
-
content: entry.content.slice(0, memorySnippetChars),
|
|
266
|
-
category: "artifact_summary",
|
|
267
|
-
sourceKind: "shadow_snapshot",
|
|
268
|
-
reason: "Shadow import of recent workspace memory for retrieval and continuity.",
|
|
269
|
-
sourceRefs: [entry.filePath]
|
|
270
|
-
},
|
|
271
|
-
policy,
|
|
272
|
-
{
|
|
273
|
-
writesThisRun
|
|
274
|
-
}
|
|
275
|
-
);
|
|
276
|
-
if (result.accepted && !result.skipped) {
|
|
277
|
-
writesThisRun += 1;
|
|
278
|
-
importedFacts += 1;
|
|
279
|
-
} else if (result.skipped) {
|
|
280
|
-
skippedFacts += 1;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
await memoryStore.appendEvent(agentId, {
|
|
285
|
-
title: `shadow import complete:${agentId}`,
|
|
286
|
-
content: `Imported live workspace snapshot for ${agentId} without modifying source files.`,
|
|
287
|
-
category: "shadow_import",
|
|
288
|
-
salience: 0.66,
|
|
289
|
-
dedupeKey: `shadow_import:${agentId}`
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
...snapshot,
|
|
294
|
-
importStats: {
|
|
295
|
-
importedFacts,
|
|
296
|
-
skippedFacts,
|
|
297
|
-
writesThisRun,
|
|
298
|
-
memoryImportLimit,
|
|
299
|
-
memorySnippetChars
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { listFilesRecursive, readJson, readJsonLines, readText } from "../utils/fs.js";
|
|
4
|
+
|
|
5
|
+
function sortNewestFirst(items) {
|
|
6
|
+
return [...items].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class OpenClawShadowBridge {
|
|
10
|
+
constructor({ liveRoot }) {
|
|
11
|
+
this.liveRoot = liveRoot;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get available() {
|
|
15
|
+
return Boolean(this.liveRoot);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async loadConfig() {
|
|
19
|
+
if (!this.available) return {};
|
|
20
|
+
return readJson(path.join(this.liveRoot, "openclaw.json"), {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loadCronJobs() {
|
|
24
|
+
if (!this.available) return [];
|
|
25
|
+
const data = await readJson(path.join(this.liveRoot, "cron", "jobs.json"), { jobs: [] });
|
|
26
|
+
return data.jobs || [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadCronRunHistory(jobId, limit = 3) {
|
|
30
|
+
if (!this.available) return [];
|
|
31
|
+
const filePath = path.join(this.liveRoot, "cron", "runs", `${jobId}.jsonl`);
|
|
32
|
+
const entries = await readJsonLines(filePath);
|
|
33
|
+
return entries.slice(-limit).reverse();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async listAgents() {
|
|
37
|
+
const config = await this.loadConfig();
|
|
38
|
+
return (config.agents?.list || []).map((agent) => ({
|
|
39
|
+
id: agent.id,
|
|
40
|
+
name: agent.name,
|
|
41
|
+
workspace: agent.workspace,
|
|
42
|
+
agentDir: agent.agentDir || path.join(this.liveRoot, "agents", agent.id, "agent"),
|
|
43
|
+
primaryModel: agent.model?.primary || null,
|
|
44
|
+
fallbackModels: agent.model?.fallbacks || [],
|
|
45
|
+
deniedTools: agent.tools?.deny || [],
|
|
46
|
+
skills: agent.skills || []
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async loadAgentProfiles(agentId) {
|
|
51
|
+
if (!this.available) return {};
|
|
52
|
+
return readJson(path.join(this.liveRoot, "agents", agentId, "agent", "auth-profiles.json"), {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async loadAgentModels(agentId) {
|
|
56
|
+
if (!this.available) return {};
|
|
57
|
+
return readJson(path.join(this.liveRoot, "agents", agentId, "agent", "models.json"), {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async loadSessionIndex(agentId) {
|
|
61
|
+
if (!this.available) return {};
|
|
62
|
+
return readJson(path.join(this.liveRoot, "agents", agentId, "sessions", "sessions.json"), {});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async resolveWorkspacePath(agentId, workspaceOverride = null) {
|
|
66
|
+
const agents = await this.listAgents();
|
|
67
|
+
const agent = agents.find((item) => item.id === agentId);
|
|
68
|
+
if (!agent) {
|
|
69
|
+
if (workspaceOverride) return workspaceOverride;
|
|
70
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
71
|
+
}
|
|
72
|
+
return agent.workspace;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read an identity file (SOUL.md, IDENTITY.md, etc.) with priority:
|
|
77
|
+
* 1. agent.workspace/fileName
|
|
78
|
+
* 2. agent.agentDir/fileName
|
|
79
|
+
* 3. null (caller falls back to stub)
|
|
80
|
+
*
|
|
81
|
+
* Whitespace-only files are treated as absent (same as missing).
|
|
82
|
+
*/
|
|
83
|
+
async readIdentityFile(agent, fileName) {
|
|
84
|
+
// Priority 1: workspace dir
|
|
85
|
+
const wsPath = path.join(agent.workspace, fileName);
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(wsPath, "utf8");
|
|
88
|
+
if (content.trim()) return content;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err.code !== "ENOENT") throw err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Priority 2: agent dir
|
|
94
|
+
const agentDirPath = path.join(
|
|
95
|
+
agent.agentDir || path.join(this.liveRoot, "agents", agent.id, "agent"),
|
|
96
|
+
fileName
|
|
97
|
+
);
|
|
98
|
+
try {
|
|
99
|
+
const content = await fs.readFile(agentDirPath, "utf8");
|
|
100
|
+
if (content.trim()) return content;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err.code !== "ENOENT") throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read session messages from OpenClaw JSONL files for FTS5 indexing.
|
|
110
|
+
* Only reads active sessions (skips *.reset.jsonl, *.deleted.jsonl).
|
|
111
|
+
* Returns up to `limit` message events (message_in / message_out).
|
|
112
|
+
*
|
|
113
|
+
* @param {string} agentId
|
|
114
|
+
* @param {number} limit Max messages to return (default 1000)
|
|
115
|
+
* Note: each session file is fully loaded before line filtering; for large
|
|
116
|
+
* installations with many messages, memory usage scales with the largest file.
|
|
117
|
+
*/
|
|
118
|
+
async readSessionMessages(agentId, limit = 1000) {
|
|
119
|
+
if (!this.available) return [];
|
|
120
|
+
const sessionsDir = path.join(this.liveRoot, "agents", agentId, "sessions");
|
|
121
|
+
let fileNames;
|
|
122
|
+
try {
|
|
123
|
+
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
124
|
+
fileNames = entries
|
|
125
|
+
.filter(e => e.isFile() && e.name.endsWith(".jsonl")
|
|
126
|
+
&& !e.name.endsWith(".reset.jsonl")
|
|
127
|
+
&& !e.name.endsWith(".deleted.jsonl"))
|
|
128
|
+
.map(e => e.name);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const messages = [];
|
|
135
|
+
for (const fileName of fileNames) {
|
|
136
|
+
if (messages.length >= limit) break;
|
|
137
|
+
const filePath = path.join(sessionsDir, fileName);
|
|
138
|
+
const lines = await readJsonLines(filePath);
|
|
139
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
140
|
+
if (messages.length >= limit) break;
|
|
141
|
+
const line = lines[lineIdx];
|
|
142
|
+
// Accept events with kind message_in/message_out (Nemoris format)
|
|
143
|
+
// or role user/assistant (OpenClaw legacy format)
|
|
144
|
+
const kind = line.kind || (line.role === "user" ? "message_in" : line.role === "assistant" ? "message_out" : null);
|
|
145
|
+
if (kind !== "message_in" && kind !== "message_out") continue;
|
|
146
|
+
messages.push({
|
|
147
|
+
id: line.id || `oc-${path.basename(fileName, ".jsonl")}-${lineIdx}`,
|
|
148
|
+
session_id: line.session_id || path.basename(fileName, ".jsonl"),
|
|
149
|
+
kind,
|
|
150
|
+
ts: line.ts || 0,
|
|
151
|
+
payload_json: line.payload_json || JSON.stringify({ content: line.content || "" }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return messages;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async readWorkspaceDocs(agentId, workspaceOverride = null) {
|
|
159
|
+
const workspaceRoot = await this.resolveWorkspacePath(agentId, workspaceOverride);
|
|
160
|
+
const MAX_FILES = 50;
|
|
161
|
+
const MAX_BYTES = 100 * 1024;
|
|
162
|
+
|
|
163
|
+
let fileNames;
|
|
164
|
+
try {
|
|
165
|
+
const entries = await fs.readdir(workspaceRoot, { withFileTypes: true });
|
|
166
|
+
const IDENTITY_FILES = new Set(["SOUL.md", "IDENTITY.md"]);
|
|
167
|
+
fileNames = entries
|
|
168
|
+
.filter(e => e.isFile() && e.name.endsWith(".md") && !IDENTITY_FILES.has(e.name))
|
|
169
|
+
.map(e => e.name)
|
|
170
|
+
.slice(0, MAX_FILES);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const docs = [];
|
|
177
|
+
for (const fileName of fileNames) {
|
|
178
|
+
const fullPath = path.join(workspaceRoot, fileName);
|
|
179
|
+
try {
|
|
180
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
181
|
+
if (content.length <= MAX_BYTES) {
|
|
182
|
+
docs.push({ fileName, fullPath, content });
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err.code !== "ENOENT") throw err;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return docs;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async readRecentWorkspaceMemory(agentId, limit = 5, workspaceOverride = null) {
|
|
193
|
+
const workspaceRoot = await this.resolveWorkspacePath(agentId, workspaceOverride);
|
|
194
|
+
const memoryDir = path.join(workspaceRoot, "memory");
|
|
195
|
+
const allFiles = (await listFilesRecursive(memoryDir)).filter((filePath) => filePath.endsWith(".md"));
|
|
196
|
+
const stats = await Promise.all(
|
|
197
|
+
allFiles.map(async (filePath) => {
|
|
198
|
+
const content = await readText(filePath, "");
|
|
199
|
+
const match = /(\d{4}-\d{2}-\d{2})/.exec(path.basename(filePath));
|
|
200
|
+
const dateHint = match ? new Date(`${match[1]}T00:00:00Z`).getTime() : 0;
|
|
201
|
+
return {
|
|
202
|
+
filePath,
|
|
203
|
+
content,
|
|
204
|
+
mtimeMs: dateHint
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return sortNewestFirst(stats).slice(0, limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async buildWorkspaceSnapshot(agentId, options = {}) {
|
|
213
|
+
const workspaceOverride = options.workspaceOverride || null;
|
|
214
|
+
const canUseLiveAgent = (await this.listAgents()).some((agent) => agent.id === agentId);
|
|
215
|
+
const [docs, recentMemory, sessionIndex, authProfiles, models] = await Promise.all([
|
|
216
|
+
this.readWorkspaceDocs(agentId, workspaceOverride),
|
|
217
|
+
this.readRecentWorkspaceMemory(agentId, 6, workspaceOverride),
|
|
218
|
+
canUseLiveAgent ? this.loadSessionIndex(agentId) : {},
|
|
219
|
+
canUseLiveAgent ? this.loadAgentProfiles(agentId) : {},
|
|
220
|
+
canUseLiveAgent ? this.loadAgentModels(agentId) : {}
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const sessionEntries = Object.entries(sessionIndex || {}).map(([sessionKey, value]) => ({
|
|
224
|
+
sessionKey,
|
|
225
|
+
model: value.model || null,
|
|
226
|
+
modelProvider: value.modelProvider || null,
|
|
227
|
+
updatedAt: value.updatedAt || null,
|
|
228
|
+
origin: value.origin || null,
|
|
229
|
+
lastChannel: value.lastChannel || null
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
agentId,
|
|
234
|
+
docs,
|
|
235
|
+
recentMemory,
|
|
236
|
+
sessionEntries,
|
|
237
|
+
authProfiles,
|
|
238
|
+
models
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async importWorkspaceSnapshot(agentId, memoryStore, policy, options = {}) {
|
|
243
|
+
const snapshot = await this.buildWorkspaceSnapshot(agentId, options);
|
|
244
|
+
const memoryImportLimit = options.memoryImportLimit ?? 5;
|
|
245
|
+
const memorySnippetChars = options.memorySnippetChars ?? 2000;
|
|
246
|
+
let writesThisRun = 0;
|
|
247
|
+
let importedFacts = 0;
|
|
248
|
+
let skippedFacts = 0;
|
|
249
|
+
|
|
250
|
+
await memoryStore.writeSummary(agentId, {
|
|
251
|
+
title: `shadow snapshot ${agentId}`,
|
|
252
|
+
summary: `Imported ${snapshot.docs.length} workspace docs and ${snapshot.recentMemory.length} recent memory files from live OpenClaw in read-only mode.`,
|
|
253
|
+
content: snapshot.docs.map((doc) => `${doc.fileName}: ${doc.fullPath}`).join("\n"),
|
|
254
|
+
category: "artifact_summary",
|
|
255
|
+
sourceKind: "shadow_snapshot",
|
|
256
|
+
salience: 0.75,
|
|
257
|
+
sourceRefs: snapshot.docs.map((doc) => doc.fullPath)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
for (const entry of snapshot.recentMemory.slice(0, memoryImportLimit)) {
|
|
261
|
+
const result = await memoryStore.writeFact(
|
|
262
|
+
agentId,
|
|
263
|
+
{
|
|
264
|
+
title: path.basename(entry.filePath),
|
|
265
|
+
content: entry.content.slice(0, memorySnippetChars),
|
|
266
|
+
category: "artifact_summary",
|
|
267
|
+
sourceKind: "shadow_snapshot",
|
|
268
|
+
reason: "Shadow import of recent workspace memory for retrieval and continuity.",
|
|
269
|
+
sourceRefs: [entry.filePath]
|
|
270
|
+
},
|
|
271
|
+
policy,
|
|
272
|
+
{
|
|
273
|
+
writesThisRun
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
if (result.accepted && !result.skipped) {
|
|
277
|
+
writesThisRun += 1;
|
|
278
|
+
importedFacts += 1;
|
|
279
|
+
} else if (result.skipped) {
|
|
280
|
+
skippedFacts += 1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await memoryStore.appendEvent(agentId, {
|
|
285
|
+
title: `shadow import complete:${agentId}`,
|
|
286
|
+
content: `Imported live workspace snapshot for ${agentId} without modifying source files.`,
|
|
287
|
+
category: "shadow_import",
|
|
288
|
+
salience: 0.66,
|
|
289
|
+
dedupeKey: `shadow_import:${agentId}`
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
...snapshot,
|
|
294
|
+
importStats: {
|
|
295
|
+
importedFacts,
|
|
296
|
+
skippedFacts,
|
|
297
|
+
writesThisRun,
|
|
298
|
+
memoryImportLimit,
|
|
299
|
+
memorySnippetChars
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|