@wipcomputer/wip-ldm-os 0.4.65 → 0.4.67
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/SKILL.md +1 -1
- package/bin/scaffold.sh +5 -2
- package/dist/bridge/{chunk-XVIE3HLZ.js → chunk-LF7EMFBY.js} +179 -10
- package/dist/bridge/cli.js +1 -1
- package/dist/bridge/core.d.ts +64 -3
- package/dist/bridge/core.js +15 -3
- package/dist/bridge/mcp-server.js +64 -14
- package/package.json +1 -1
- package/src/boot/boot-hook.mjs +40 -13
- package/src/bridge/core.ts +280 -12
- package/src/bridge/mcp-server.ts +87 -14
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ license: MIT
|
|
|
9
9
|
compatibility: Requires git, npm, node. Node.js 18+.
|
|
10
10
|
metadata:
|
|
11
11
|
display-name: "LDM OS"
|
|
12
|
-
version: "0.4.
|
|
12
|
+
version: "0.4.67"
|
|
13
13
|
homepage: "https://github.com/wipcomputer/wip-ldm-os"
|
|
14
14
|
author: "Parker Todd Brooks"
|
|
15
15
|
category: infrastructure
|
package/bin/scaffold.sh
CHANGED
|
@@ -10,8 +10,11 @@ CC_HOME="${LDM_HOME}/agents/cc"
|
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
11
|
TEMPLATES="${SCRIPT_DIR}/templates/cc"
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
|
|
13
|
+
# Resolve workspace from LDM config or default
|
|
14
|
+
WORKSPACE=$(python3 -c "import json; print(json.load(open('$HOME/.ldm/config.json')).get('workspace','$HOME/wipcomputerinc'))" 2>/dev/null || echo "$HOME/wipcomputerinc")
|
|
15
|
+
|
|
16
|
+
# Existing soul files (now under workspace/team/cc-mini/)
|
|
17
|
+
CC_DOCS="${WORKSPACE}/team/cc-mini/documents"
|
|
15
18
|
CC_SOUL="${CC_DOCS}/cc-soul"
|
|
16
19
|
|
|
17
20
|
echo "=== LDM OS Scaffold ==="
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// core.ts
|
|
2
2
|
import { execSync, exec } from "child_process";
|
|
3
|
-
import { readdirSync, readFileSync, existsSync, statSync } from "fs";
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
4
4
|
import { join, relative, resolve } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
7
8
|
var execAsync = promisify(exec);
|
|
8
9
|
var HOME = process.env.HOME || homedir();
|
|
9
10
|
var LDM_ROOT = process.env.LDM_ROOT || join(HOME, ".ldm");
|
|
@@ -82,18 +83,180 @@ function resolveGatewayConfig(openclawDir) {
|
|
|
82
83
|
cachedGatewayConfig = { token, port };
|
|
83
84
|
return cachedGatewayConfig;
|
|
84
85
|
}
|
|
85
|
-
var
|
|
86
|
+
var MESSAGES_DIR = join(LDM_ROOT, "messages");
|
|
87
|
+
var PROCESSED_DIR = join(MESSAGES_DIR, "_processed");
|
|
88
|
+
var _sessionAgentId = "cc-mini";
|
|
89
|
+
var _sessionName = process.env.LDM_SESSION_NAME || "default";
|
|
90
|
+
function setSessionIdentity(agentId, sessionName) {
|
|
91
|
+
_sessionAgentId = agentId;
|
|
92
|
+
_sessionName = sessionName;
|
|
93
|
+
}
|
|
94
|
+
function getSessionIdentity() {
|
|
95
|
+
return { agentId: _sessionAgentId, sessionName: _sessionName };
|
|
96
|
+
}
|
|
97
|
+
function parseTarget(to) {
|
|
98
|
+
if (to === "*") return { agent: "*", session: "*" };
|
|
99
|
+
const colonIdx = to.indexOf(":");
|
|
100
|
+
if (colonIdx === -1) return { agent: to, session: "default" };
|
|
101
|
+
return { agent: to.slice(0, colonIdx), session: to.slice(colonIdx + 1) };
|
|
102
|
+
}
|
|
103
|
+
function messageMatchesSession(msgTo, agentId, sessionName) {
|
|
104
|
+
if (msgTo === "*" || msgTo === "all") return true;
|
|
105
|
+
const target = parseTarget(msgTo);
|
|
106
|
+
if (target.agent !== "*" && target.agent !== agentId) return false;
|
|
107
|
+
if (target.session === "*") return true;
|
|
108
|
+
return target.session === sessionName;
|
|
109
|
+
}
|
|
86
110
|
function pushInbox(msg) {
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
try {
|
|
112
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
113
|
+
const id = randomUUID();
|
|
114
|
+
const data = {
|
|
115
|
+
id,
|
|
116
|
+
type: msg.type || "chat",
|
|
117
|
+
from: msg.from || "unknown",
|
|
118
|
+
to: msg.to || `${_sessionAgentId}:${_sessionName}`,
|
|
119
|
+
body: msg.body || msg.message || "",
|
|
120
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
121
|
+
read: false
|
|
122
|
+
};
|
|
123
|
+
writeFileSync(join(MESSAGES_DIR, `${id}.json`), JSON.stringify(data, null, 2) + "\n");
|
|
124
|
+
return inboxCount();
|
|
125
|
+
} catch {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
89
128
|
}
|
|
90
129
|
function drainInbox() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
try {
|
|
131
|
+
if (!existsSync(MESSAGES_DIR)) return [];
|
|
132
|
+
const files = readdirSync(MESSAGES_DIR).filter((f) => f.endsWith(".json"));
|
|
133
|
+
const messages = [];
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const filePath = join(MESSAGES_DIR, file);
|
|
136
|
+
try {
|
|
137
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
138
|
+
if (!messageMatchesSession(data.to, _sessionAgentId, _sessionName)) continue;
|
|
139
|
+
if (!data.body && data.message) data.body = data.message;
|
|
140
|
+
messages.push(data);
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(PROCESSED_DIR, { recursive: true });
|
|
143
|
+
renameSync(filePath, join(PROCESSED_DIR, file));
|
|
144
|
+
} catch {
|
|
145
|
+
try {
|
|
146
|
+
unlinkSync(filePath);
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
messages.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
|
|
154
|
+
return messages;
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
94
158
|
}
|
|
95
159
|
function inboxCount() {
|
|
96
|
-
|
|
160
|
+
try {
|
|
161
|
+
if (!existsSync(MESSAGES_DIR)) return 0;
|
|
162
|
+
const files = readdirSync(MESSAGES_DIR).filter((f) => f.endsWith(".json"));
|
|
163
|
+
let count = 0;
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(readFileSync(join(MESSAGES_DIR, file), "utf-8"));
|
|
167
|
+
if (messageMatchesSession(data.to, _sessionAgentId, _sessionName)) count++;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return count;
|
|
172
|
+
} catch {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function inboxCountBySession() {
|
|
177
|
+
const counts = {};
|
|
178
|
+
try {
|
|
179
|
+
if (!existsSync(MESSAGES_DIR)) return counts;
|
|
180
|
+
const files = readdirSync(MESSAGES_DIR).filter((f) => f.endsWith(".json"));
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
try {
|
|
183
|
+
const data = JSON.parse(readFileSync(join(MESSAGES_DIR, file), "utf-8"));
|
|
184
|
+
const to = data.to || "unknown";
|
|
185
|
+
counts[to] = (counts[to] || 0) + 1;
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
return counts;
|
|
192
|
+
}
|
|
193
|
+
function sendLdmMessage(opts) {
|
|
194
|
+
try {
|
|
195
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
196
|
+
const id = randomUUID();
|
|
197
|
+
const data = {
|
|
198
|
+
id,
|
|
199
|
+
type: opts.type || "chat",
|
|
200
|
+
from: opts.from || `${_sessionAgentId}:${_sessionName}`,
|
|
201
|
+
to: opts.to,
|
|
202
|
+
body: opts.body,
|
|
203
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
204
|
+
read: false
|
|
205
|
+
};
|
|
206
|
+
writeFileSync(join(MESSAGES_DIR, `${id}.json`), JSON.stringify(data, null, 2) + "\n");
|
|
207
|
+
return id;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
var SESSIONS_DIR = join(LDM_ROOT, "sessions");
|
|
213
|
+
function registerBridgeSession() {
|
|
214
|
+
try {
|
|
215
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
216
|
+
const fileName = `${_sessionAgentId}--${_sessionName}.json`;
|
|
217
|
+
const data = {
|
|
218
|
+
name: _sessionName,
|
|
219
|
+
agentId: _sessionAgentId,
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
cwd: process.cwd(),
|
|
223
|
+
alive: true
|
|
224
|
+
};
|
|
225
|
+
writeFileSync(join(SESSIONS_DIR, fileName), JSON.stringify(data, null, 2) + "\n");
|
|
226
|
+
return data;
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function listActiveSessions(agentFilter) {
|
|
232
|
+
try {
|
|
233
|
+
if (!existsSync(SESSIONS_DIR)) return [];
|
|
234
|
+
const files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
235
|
+
const sessions = [];
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
try {
|
|
238
|
+
const filePath = join(SESSIONS_DIR, file);
|
|
239
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
240
|
+
let alive = false;
|
|
241
|
+
try {
|
|
242
|
+
process.kill(data.pid, 0);
|
|
243
|
+
alive = true;
|
|
244
|
+
} catch {
|
|
245
|
+
try {
|
|
246
|
+
unlinkSync(filePath);
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (agentFilter && data.agentId !== agentFilter) continue;
|
|
252
|
+
sessions.push({ ...data, alive });
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return sessions;
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
97
260
|
}
|
|
98
261
|
async function sendMessage(openclawDir, message, options) {
|
|
99
262
|
const { token, port } = resolveGatewayConfig(openclawDir);
|
|
@@ -104,11 +267,11 @@ async function sendMessage(openclawDir, message, options) {
|
|
|
104
267
|
headers: {
|
|
105
268
|
Authorization: `Bearer ${token}`,
|
|
106
269
|
"Content-Type": "application/json",
|
|
107
|
-
"x-openclaw-scopes": "operator.read,operator.write"
|
|
270
|
+
"x-openclaw-scopes": "operator.read,operator.write",
|
|
271
|
+
"x-openclaw-session-key": `agent:${agentId}:main`
|
|
108
272
|
},
|
|
109
273
|
body: JSON.stringify({
|
|
110
274
|
model: `openclaw/${agentId}`,
|
|
111
|
-
user: "main",
|
|
112
275
|
messages: [
|
|
113
276
|
{
|
|
114
277
|
role: "user",
|
|
@@ -409,9 +572,15 @@ export {
|
|
|
409
572
|
resolveConfigMulti,
|
|
410
573
|
resolveApiKey,
|
|
411
574
|
resolveGatewayConfig,
|
|
575
|
+
setSessionIdentity,
|
|
576
|
+
getSessionIdentity,
|
|
412
577
|
pushInbox,
|
|
413
578
|
drainInbox,
|
|
414
579
|
inboxCount,
|
|
580
|
+
inboxCountBySession,
|
|
581
|
+
sendLdmMessage,
|
|
582
|
+
registerBridgeSession,
|
|
583
|
+
listActiveSessions,
|
|
415
584
|
sendMessage,
|
|
416
585
|
getQueryEmbedding,
|
|
417
586
|
blobToEmbedding,
|
package/dist/bridge/cli.js
CHANGED
package/dist/bridge/core.d.ts
CHANGED
|
@@ -12,9 +12,14 @@ interface GatewayConfig {
|
|
|
12
12
|
port: number;
|
|
13
13
|
}
|
|
14
14
|
interface InboxMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
type: string;
|
|
15
17
|
from: string;
|
|
16
|
-
|
|
18
|
+
to: string;
|
|
19
|
+
body: string;
|
|
20
|
+
message?: string;
|
|
17
21
|
timestamp: string;
|
|
22
|
+
read: boolean;
|
|
18
23
|
}
|
|
19
24
|
interface ConversationResult {
|
|
20
25
|
text: string;
|
|
@@ -39,9 +44,65 @@ declare function resolveConfig(overrides?: Partial<BridgeConfig>): BridgeConfig;
|
|
|
39
44
|
declare function resolveConfigMulti(overrides?: Partial<BridgeConfig>): BridgeConfig;
|
|
40
45
|
declare function resolveApiKey(openclawDir: string): string | null;
|
|
41
46
|
declare function resolveGatewayConfig(openclawDir: string): GatewayConfig;
|
|
42
|
-
declare function
|
|
47
|
+
declare function setSessionIdentity(agentId: string, sessionName: string): void;
|
|
48
|
+
declare function getSessionIdentity(): {
|
|
49
|
+
agentId: string;
|
|
50
|
+
sessionName: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Write a message to the file-based inbox.
|
|
54
|
+
* Creates a JSON file at ~/.ldm/messages/{uuid}.json.
|
|
55
|
+
*/
|
|
56
|
+
declare function pushInbox(msg: {
|
|
57
|
+
from: string;
|
|
58
|
+
message?: string;
|
|
59
|
+
body?: string;
|
|
60
|
+
to?: string;
|
|
61
|
+
type?: string;
|
|
62
|
+
}): number;
|
|
63
|
+
/**
|
|
64
|
+
* Read and drain all messages for this session from the inbox.
|
|
65
|
+
* Moves processed messages to ~/.ldm/messages/_processed/.
|
|
66
|
+
*/
|
|
43
67
|
declare function drainInbox(): InboxMessage[];
|
|
68
|
+
/**
|
|
69
|
+
* Count pending messages for this session without draining.
|
|
70
|
+
*/
|
|
44
71
|
declare function inboxCount(): number;
|
|
72
|
+
/**
|
|
73
|
+
* Get pending message counts broken down by session.
|
|
74
|
+
* Used by GET /status to show per-session counts.
|
|
75
|
+
*/
|
|
76
|
+
declare function inboxCountBySession(): Record<string, number>;
|
|
77
|
+
/**
|
|
78
|
+
* Send a message to another agent or session via the file-based inbox.
|
|
79
|
+
* Phase 4: Cross-agent messaging. Works for any agent, any session.
|
|
80
|
+
* This is the file-based path. For OpenClaw agents, use sendMessage() (gateway).
|
|
81
|
+
*/
|
|
82
|
+
declare function sendLdmMessage(opts: {
|
|
83
|
+
from?: string;
|
|
84
|
+
to: string;
|
|
85
|
+
body: string;
|
|
86
|
+
type?: string;
|
|
87
|
+
}): string | null;
|
|
88
|
+
interface SessionInfo {
|
|
89
|
+
name: string;
|
|
90
|
+
agentId: string;
|
|
91
|
+
pid: number;
|
|
92
|
+
startTime: string;
|
|
93
|
+
cwd: string;
|
|
94
|
+
alive: boolean;
|
|
95
|
+
meta?: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register this bridge session in ~/.ldm/sessions/.
|
|
99
|
+
* Uses the agent--session naming convention.
|
|
100
|
+
*/
|
|
101
|
+
declare function registerBridgeSession(): SessionInfo | null;
|
|
102
|
+
/**
|
|
103
|
+
* List active sessions. Validates PID liveness and cleans stale entries.
|
|
104
|
+
*/
|
|
105
|
+
declare function listActiveSessions(agentFilter?: string): SessionInfo[];
|
|
45
106
|
declare function sendMessage(openclawDir: string, message: string, options?: {
|
|
46
107
|
agentId?: string;
|
|
47
108
|
user?: string;
|
|
@@ -71,4 +132,4 @@ declare function discoverSkills(openclawDir: string): SkillInfo[];
|
|
|
71
132
|
declare function executeSkillScript(skillDir: string, scripts: string[], scriptName: string | undefined, args: string): Promise<string>;
|
|
72
133
|
declare function readWorkspaceFile(workspaceDir: string, filePath: string): WorkspaceFileResult;
|
|
73
134
|
|
|
74
|
-
export { type BridgeConfig, type ConversationResult, type GatewayConfig, type InboxMessage, LDM_ROOT, type SkillInfo, type WorkspaceFileResult, type WorkspaceSearchResult, blobToEmbedding, cosineSimilarity, discoverSkills, drainInbox, executeSkillScript, findMarkdownFiles, getQueryEmbedding, inboxCount, pushInbox, readWorkspaceFile, resolveApiKey, resolveConfig, resolveConfigMulti, resolveGatewayConfig, searchConversations, searchWorkspace, sendMessage };
|
|
135
|
+
export { type BridgeConfig, type ConversationResult, type GatewayConfig, type InboxMessage, LDM_ROOT, type SessionInfo, type SkillInfo, type WorkspaceFileResult, type WorkspaceSearchResult, blobToEmbedding, cosineSimilarity, discoverSkills, drainInbox, executeSkillScript, findMarkdownFiles, getQueryEmbedding, getSessionIdentity, inboxCount, inboxCountBySession, listActiveSessions, pushInbox, readWorkspaceFile, registerBridgeSession, resolveApiKey, resolveConfig, resolveConfigMulti, resolveGatewayConfig, searchConversations, searchWorkspace, sendLdmMessage, sendMessage, setSessionIdentity };
|
package/dist/bridge/core.js
CHANGED
|
@@ -7,17 +7,23 @@ import {
|
|
|
7
7
|
executeSkillScript,
|
|
8
8
|
findMarkdownFiles,
|
|
9
9
|
getQueryEmbedding,
|
|
10
|
+
getSessionIdentity,
|
|
10
11
|
inboxCount,
|
|
12
|
+
inboxCountBySession,
|
|
13
|
+
listActiveSessions,
|
|
11
14
|
pushInbox,
|
|
12
15
|
readWorkspaceFile,
|
|
16
|
+
registerBridgeSession,
|
|
13
17
|
resolveApiKey,
|
|
14
18
|
resolveConfig,
|
|
15
19
|
resolveConfigMulti,
|
|
16
20
|
resolveGatewayConfig,
|
|
17
21
|
searchConversations,
|
|
18
22
|
searchWorkspace,
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
sendLdmMessage,
|
|
24
|
+
sendMessage,
|
|
25
|
+
setSessionIdentity
|
|
26
|
+
} from "./chunk-LF7EMFBY.js";
|
|
21
27
|
export {
|
|
22
28
|
LDM_ROOT,
|
|
23
29
|
blobToEmbedding,
|
|
@@ -27,14 +33,20 @@ export {
|
|
|
27
33
|
executeSkillScript,
|
|
28
34
|
findMarkdownFiles,
|
|
29
35
|
getQueryEmbedding,
|
|
36
|
+
getSessionIdentity,
|
|
30
37
|
inboxCount,
|
|
38
|
+
inboxCountBySession,
|
|
39
|
+
listActiveSessions,
|
|
31
40
|
pushInbox,
|
|
32
41
|
readWorkspaceFile,
|
|
42
|
+
registerBridgeSession,
|
|
33
43
|
resolveApiKey,
|
|
34
44
|
resolveConfig,
|
|
35
45
|
resolveConfigMulti,
|
|
36
46
|
resolveGatewayConfig,
|
|
37
47
|
searchConversations,
|
|
38
48
|
searchWorkspace,
|
|
39
|
-
|
|
49
|
+
sendLdmMessage,
|
|
50
|
+
sendMessage,
|
|
51
|
+
setSessionIdentity
|
|
40
52
|
};
|
|
@@ -2,14 +2,20 @@ import {
|
|
|
2
2
|
discoverSkills,
|
|
3
3
|
drainInbox,
|
|
4
4
|
executeSkillScript,
|
|
5
|
+
getSessionIdentity,
|
|
5
6
|
inboxCount,
|
|
7
|
+
inboxCountBySession,
|
|
8
|
+
listActiveSessions,
|
|
6
9
|
pushInbox,
|
|
7
10
|
readWorkspaceFile,
|
|
11
|
+
registerBridgeSession,
|
|
8
12
|
resolveConfig,
|
|
9
13
|
searchConversations,
|
|
10
14
|
searchWorkspace,
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
sendLdmMessage,
|
|
16
|
+
sendMessage,
|
|
17
|
+
setSessionIdentity
|
|
18
|
+
} from "./chunk-LF7EMFBY.js";
|
|
13
19
|
|
|
14
20
|
// mcp-server.ts
|
|
15
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -17,9 +23,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
23
|
import { createServer } from "http";
|
|
18
24
|
import { appendFileSync, mkdirSync } from "fs";
|
|
19
25
|
import { join } from "path";
|
|
26
|
+
import { homedir } from "os";
|
|
20
27
|
import { z } from "zod";
|
|
21
28
|
var config = resolveConfig();
|
|
22
|
-
var METRICS_DIR = join(process.env.HOME ||
|
|
29
|
+
var METRICS_DIR = join(process.env.HOME || homedir(), ".openclaw", "memory");
|
|
23
30
|
var METRICS_PATH = join(METRICS_DIR, "search-metrics.jsonl");
|
|
24
31
|
function logSearchMetric(tool, query, resultCount) {
|
|
25
32
|
try {
|
|
@@ -53,18 +60,20 @@ function startInboxServer(cfg) {
|
|
|
53
60
|
if (req.method === "POST" && req.url === "/message") {
|
|
54
61
|
try {
|
|
55
62
|
const body = JSON.parse(await readBody(req));
|
|
56
|
-
const
|
|
63
|
+
const { agentId, sessionName } = getSessionIdentity();
|
|
64
|
+
const queued = pushInbox({
|
|
57
65
|
from: body.from || "agent",
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
body: body.body || body.message || "",
|
|
67
|
+
to: body.to || `${agentId}:${sessionName}`,
|
|
68
|
+
type: body.type || "chat"
|
|
69
|
+
});
|
|
70
|
+
const messageBody = body.body || body.message || "";
|
|
71
|
+
console.error(`wip-bridge inbox: message from ${body.from || "agent"} to ${body.to || "default"}`);
|
|
63
72
|
try {
|
|
64
73
|
server.sendLoggingMessage({
|
|
65
74
|
level: "info",
|
|
66
75
|
logger: "wip-bridge",
|
|
67
|
-
data: `[
|
|
76
|
+
data: `[inbox] ${body.from || "agent"}: ${messageBody}`
|
|
68
77
|
});
|
|
69
78
|
} catch {
|
|
70
79
|
}
|
|
@@ -77,8 +86,16 @@ function startInboxServer(cfg) {
|
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
79
88
|
if (req.method === "GET" && req.url === "/status") {
|
|
89
|
+
const pending = inboxCount();
|
|
90
|
+
const bySession = inboxCountBySession();
|
|
80
91
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
-
res.end(JSON.stringify({ ok: true, pending
|
|
92
|
+
res.end(JSON.stringify({ ok: true, pending, bySession }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (req.method === "GET" && req.url === "/sessions") {
|
|
96
|
+
const sessions = listActiveSessions();
|
|
97
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({ ok: true, sessions }));
|
|
82
99
|
return;
|
|
83
100
|
}
|
|
84
101
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -191,7 +208,7 @@ server.registerTool(
|
|
|
191
208
|
server.registerTool(
|
|
192
209
|
"lesa_check_inbox",
|
|
193
210
|
{
|
|
194
|
-
description: "Check for pending messages
|
|
211
|
+
description: "Check for pending messages in the file-based inbox (~/.ldm/messages/). Messages can come from OpenClaw agents, other Claude Code sessions, or CLI. Returns all pending messages for this session and marks them as read.",
|
|
195
212
|
inputSchema: {}
|
|
196
213
|
},
|
|
197
214
|
async () => {
|
|
@@ -199,13 +216,38 @@ server.registerTool(
|
|
|
199
216
|
if (messages.length === 0) {
|
|
200
217
|
return { content: [{ type: "text", text: "No pending messages." }] };
|
|
201
218
|
}
|
|
202
|
-
const text = messages.map((m) => `**${m.from}** (${m.timestamp}):
|
|
203
|
-
${m.message}`).join("\n\n---\n\n");
|
|
219
|
+
const text = messages.map((m) => `**${m.from}** [${m.type}] (${m.timestamp}):
|
|
220
|
+
${m.body || m.message}`).join("\n\n---\n\n");
|
|
204
221
|
return { content: [{ type: "text", text: `${messages.length} message(s):
|
|
205
222
|
|
|
206
223
|
${text}` }] };
|
|
207
224
|
}
|
|
208
225
|
);
|
|
226
|
+
server.registerTool(
|
|
227
|
+
"ldm_send_message",
|
|
228
|
+
{
|
|
229
|
+
description: "Send a message to any agent or session via the file-based inbox (~/.ldm/messages/). Works for agent-to-agent communication. For OpenClaw agents (like Lesa), use lesa_send_message instead (goes through the gateway). This tool writes directly to the shared inbox.\n\nTarget formats:\n 'cc-mini' ... default session\n 'cc-mini:brainstorm' ... named session\n 'cc-mini:*' ... broadcast to all sessions of that agent\n '*' ... broadcast to all agents",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
to: z.string().describe("Target: 'agent', 'agent:session', 'agent:*', or '*'"),
|
|
232
|
+
message: z.string().describe("Message body"),
|
|
233
|
+
type: z.string().optional().default("chat").describe("Message type: chat, system, task (default: chat)")
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async ({ to, message, type }) => {
|
|
237
|
+
const { agentId, sessionName } = getSessionIdentity();
|
|
238
|
+
const id = sendLdmMessage({
|
|
239
|
+
from: `${agentId}:${sessionName}`,
|
|
240
|
+
to,
|
|
241
|
+
body: message,
|
|
242
|
+
type
|
|
243
|
+
});
|
|
244
|
+
if (id) {
|
|
245
|
+
return { content: [{ type: "text", text: `Message sent (id: ${id}) to ${to}` }] };
|
|
246
|
+
} else {
|
|
247
|
+
return { content: [{ type: "text", text: "Failed to send message." }], isError: true };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
);
|
|
209
251
|
function registerSkillTools(skills) {
|
|
210
252
|
const executableSkills = skills.filter((s) => s.hasScripts);
|
|
211
253
|
const toolNameMap = /* @__PURE__ */ new Map();
|
|
@@ -267,6 +309,14 @@ ${lines.join("\n")}` }] };
|
|
|
267
309
|
console.error(`wip-bridge: registered ${executableSkills.length} skill tools + oc_skills_list (${skills.length} total skills)`);
|
|
268
310
|
}
|
|
269
311
|
async function main() {
|
|
312
|
+
const agentId = process.env.LDM_AGENT_ID || "cc-mini";
|
|
313
|
+
const sessionName = process.env.LDM_SESSION_NAME || "default";
|
|
314
|
+
setSessionIdentity(agentId, sessionName);
|
|
315
|
+
console.error(`wip-bridge: session identity: ${agentId}:${sessionName}`);
|
|
316
|
+
const session = registerBridgeSession();
|
|
317
|
+
if (session) {
|
|
318
|
+
console.error(`wip-bridge: registered session ${agentId}--${sessionName} (pid ${session.pid})`);
|
|
319
|
+
}
|
|
270
320
|
startInboxServer(config);
|
|
271
321
|
try {
|
|
272
322
|
const skills = discoverSkills(config.openclawDir);
|
package/package.json
CHANGED
package/src/boot/boot-hook.mjs
CHANGED
|
@@ -101,9 +101,9 @@ function getDefaultConfig() {
|
|
|
101
101
|
maxTotalLines: 2000,
|
|
102
102
|
steps: {
|
|
103
103
|
sharedContext: { path: '~/.openclaw/workspace/SHARED-CONTEXT.md', label: 'SHARED-CONTEXT.md', stepNumber: 2, critical: true },
|
|
104
|
-
journals: { dir: '
|
|
104
|
+
journals: { dir: '~/.ldm/agents/cc-mini/memory/journals', label: 'Most Recent CC Journal (Legacy)', stepNumber: 3, maxLines: 80, strategy: 'most-recent' },
|
|
105
105
|
workspaceDailyLogs: { dir: '~/.openclaw/workspace/memory', label: 'Workspace Daily Logs', stepNumber: 4, maxLines: 40, strategy: 'daily-logs', days: ['today', 'yesterday'] },
|
|
106
|
-
fullHistory: { label: 'Full History', stepNumber: 5, reminder: 'Read on cold start:
|
|
106
|
+
fullHistory: { label: 'Full History', stepNumber: 5, reminder: 'Read on cold start: team/cc-mini/documents/cc-full-history.md' },
|
|
107
107
|
context: { path: '~/.ldm/agents/cc-mini/CONTEXT.md', label: 'CC CONTEXT.md', stepNumber: 6, critical: true },
|
|
108
108
|
soul: { path: '~/.ldm/agents/cc-mini/SOUL.md', label: 'CC SOUL.md', stepNumber: 7 },
|
|
109
109
|
ccJournals: { dir: '~/.ldm/agents/cc-mini/memory/journals', label: 'Most Recent CC Journal', stepNumber: 8, maxLines: 80, strategy: 'most-recent' },
|
|
@@ -212,26 +212,53 @@ async function main() {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
// ── Register session (fire-and-forget) ──
|
|
215
|
+
// ── Register session (fire-and-forget, Phase 2) ──
|
|
216
216
|
try {
|
|
217
217
|
const { registerSession } = await import('../../lib/sessions.mjs');
|
|
218
|
-
const
|
|
218
|
+
const agentId = config?.agentId || 'unknown';
|
|
219
|
+
const sessionName = process.env.LDM_SESSION_NAME || process.env.CLAUDE_SESSION_NAME || basename(input?.cwd || process.cwd()) || 'default';
|
|
220
|
+
// Register with agent--session naming convention
|
|
219
221
|
registerSession({
|
|
220
|
-
name
|
|
221
|
-
agentId
|
|
222
|
+
name: `${agentId}--${sessionName}`,
|
|
223
|
+
agentId,
|
|
222
224
|
pid: process.ppid || process.pid,
|
|
223
|
-
meta: { cwd: input?.cwd },
|
|
225
|
+
meta: { cwd: input?.cwd, sessionName },
|
|
224
226
|
});
|
|
225
227
|
} catch {}
|
|
226
228
|
|
|
227
|
-
// ── Check pending messages ──
|
|
229
|
+
// ── Check pending messages (Phase 3: boot hook delivery) ──
|
|
230
|
+
// Scans ~/.ldm/messages/ for messages addressed to this agent.
|
|
231
|
+
// Supports targeting: "cc-mini", "cc-mini:session", "cc-mini:*", "*", "all".
|
|
232
|
+
// Does NOT mark as read. The MCP check_inbox tool handles that.
|
|
228
233
|
try {
|
|
229
234
|
const { readMessages } = await import('../../lib/messages.mjs');
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
const agentId = config?.agentId || 'cc-mini';
|
|
236
|
+
const sessionName = process.env.LDM_SESSION_NAME || process.env.CLAUDE_SESSION_NAME || basename(input?.cwd || process.cwd()) || 'default';
|
|
237
|
+
|
|
238
|
+
// Read messages using the existing lib/messages.mjs.
|
|
239
|
+
// It filters by exact sessionName or "all" broadcast.
|
|
240
|
+
// We also need to check for agent-level targeting (e.g. "cc-mini", "cc-mini:*").
|
|
241
|
+
const directMessages = readMessages(sessionName, { markRead: false });
|
|
242
|
+
const agentMessages = readMessages(agentId, { markRead: false });
|
|
243
|
+
const agentSessionMessages = readMessages(`${agentId}:${sessionName}`, { markRead: false });
|
|
244
|
+
const agentBroadcast = readMessages(`${agentId}:*`, { markRead: false });
|
|
245
|
+
const globalBroadcast = readMessages('*', { markRead: false });
|
|
246
|
+
|
|
247
|
+
// Deduplicate by message ID
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
const allPending = [];
|
|
250
|
+
for (const msg of [...directMessages, ...agentMessages, ...agentSessionMessages, ...agentBroadcast, ...globalBroadcast]) {
|
|
251
|
+
if (msg.id && seen.has(msg.id)) continue;
|
|
252
|
+
if (msg.id) seen.add(msg.id);
|
|
253
|
+
allPending.push(msg);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Sort by timestamp
|
|
257
|
+
allPending.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
258
|
+
|
|
259
|
+
if (allPending.length > 0) {
|
|
260
|
+
const msgLines = allPending.map(m => ` [${m.type || 'chat'}] from ${m.from}: ${m.body}`).join('\n');
|
|
261
|
+
sections.push(`== Pending Messages (${allPending.length}) ==\nYou have ${allPending.length} pending message(s). Use check_inbox to read and acknowledge them.\n${msgLines}`);
|
|
235
262
|
}
|
|
236
263
|
} catch {}
|
|
237
264
|
|
package/src/bridge/core.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Handles messaging, memory search, and workspace access for OpenClaw agents.
|
|
3
3
|
|
|
4
4
|
import { execSync, exec } from "node:child_process";
|
|
5
|
-
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
5
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
|
6
6
|
import { join, relative, resolve } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
9
10
|
|
|
10
11
|
const execAsync = promisify(exec);
|
|
11
12
|
|
|
@@ -31,9 +32,14 @@ export interface GatewayConfig {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface InboxMessage {
|
|
35
|
+
id: string;
|
|
36
|
+
type: string;
|
|
34
37
|
from: string;
|
|
35
|
-
|
|
38
|
+
to: string;
|
|
39
|
+
body: string;
|
|
40
|
+
message?: string; // legacy compat: alias for body
|
|
36
41
|
timestamp: string;
|
|
42
|
+
read: boolean;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export interface ConversationResult {
|
|
@@ -158,23 +164,285 @@ export function resolveGatewayConfig(openclawDir: string): GatewayConfig {
|
|
|
158
164
|
return cachedGatewayConfig;
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
// ── Inbox
|
|
167
|
+
// ── Inbox (file-based via ~/.ldm/messages/) ─────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// Phase 1: Replaces the in-memory queue with JSON files on disk.
|
|
170
|
+
// Phase 2: Adds session targeting (agent:session format).
|
|
171
|
+
// Phase 4: Cross-agent delivery to any agent via the same directory.
|
|
172
|
+
//
|
|
173
|
+
// Uses the existing lib/messages.mjs format. Each message is a JSON file
|
|
174
|
+
// at ~/.ldm/messages/{uuid}.json. Read means move to _processed/.
|
|
175
|
+
|
|
176
|
+
const MESSAGES_DIR = join(LDM_ROOT, "messages");
|
|
177
|
+
const PROCESSED_DIR = join(MESSAGES_DIR, "_processed");
|
|
178
|
+
|
|
179
|
+
// Session identity for this bridge process.
|
|
180
|
+
// Set via LDM_SESSION_NAME env or defaults to "default".
|
|
181
|
+
let _sessionAgentId = "cc-mini";
|
|
182
|
+
let _sessionName = process.env.LDM_SESSION_NAME || "default";
|
|
183
|
+
|
|
184
|
+
export function setSessionIdentity(agentId: string, sessionName: string): void {
|
|
185
|
+
_sessionAgentId = agentId;
|
|
186
|
+
_sessionName = sessionName;
|
|
187
|
+
}
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
export function getSessionIdentity(): { agentId: string; sessionName: string } {
|
|
190
|
+
return { agentId: _sessionAgentId, sessionName: _sessionName };
|
|
191
|
+
}
|
|
164
192
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Parse a "to" field into agent and session parts.
|
|
195
|
+
* Formats: "cc-mini" (default session), "cc-mini:brainstorm" (named),
|
|
196
|
+
* "cc-mini:*" (broadcast to all sessions of agent), "*" (all)
|
|
197
|
+
*/
|
|
198
|
+
function parseTarget(to: string): { agent: string; session: string } {
|
|
199
|
+
if (to === "*") return { agent: "*", session: "*" };
|
|
200
|
+
const colonIdx = to.indexOf(":");
|
|
201
|
+
if (colonIdx === -1) return { agent: to, session: "default" };
|
|
202
|
+
return { agent: to.slice(0, colonIdx), session: to.slice(colonIdx + 1) };
|
|
168
203
|
}
|
|
169
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Check if a message's "to" field matches this session.
|
|
207
|
+
* Matches: exact agent + session, agent broadcast (agent:*),
|
|
208
|
+
* global broadcast (*), or agent with default session.
|
|
209
|
+
*/
|
|
210
|
+
function messageMatchesSession(msgTo: string, agentId: string, sessionName: string): boolean {
|
|
211
|
+
// Global broadcast
|
|
212
|
+
if (msgTo === "*" || msgTo === "all") return true;
|
|
213
|
+
|
|
214
|
+
const target = parseTarget(msgTo);
|
|
215
|
+
|
|
216
|
+
// Different agent entirely
|
|
217
|
+
if (target.agent !== "*" && target.agent !== agentId) return false;
|
|
218
|
+
|
|
219
|
+
// Agent broadcast (agent:*)
|
|
220
|
+
if (target.session === "*") return true;
|
|
221
|
+
|
|
222
|
+
// Exact session match
|
|
223
|
+
return target.session === sessionName;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Write a message to the file-based inbox.
|
|
228
|
+
* Creates a JSON file at ~/.ldm/messages/{uuid}.json.
|
|
229
|
+
*/
|
|
230
|
+
export function pushInbox(msg: { from: string; message?: string; body?: string; to?: string; type?: string }): number {
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
233
|
+
const id = randomUUID();
|
|
234
|
+
const data: InboxMessage = {
|
|
235
|
+
id,
|
|
236
|
+
type: msg.type || "chat",
|
|
237
|
+
from: msg.from || "unknown",
|
|
238
|
+
to: msg.to || `${_sessionAgentId}:${_sessionName}`,
|
|
239
|
+
body: msg.body || msg.message || "",
|
|
240
|
+
timestamp: new Date().toISOString(),
|
|
241
|
+
read: false,
|
|
242
|
+
};
|
|
243
|
+
writeFileSync(join(MESSAGES_DIR, `${id}.json`), JSON.stringify(data, null, 2) + "\n");
|
|
244
|
+
|
|
245
|
+
// Return count of pending messages for this session
|
|
246
|
+
return inboxCount();
|
|
247
|
+
} catch {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Read and drain all messages for this session from the inbox.
|
|
254
|
+
* Moves processed messages to ~/.ldm/messages/_processed/.
|
|
255
|
+
*/
|
|
170
256
|
export function drainInbox(): InboxMessage[] {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
257
|
+
try {
|
|
258
|
+
if (!existsSync(MESSAGES_DIR)) return [];
|
|
259
|
+
|
|
260
|
+
const files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith(".json"));
|
|
261
|
+
const messages: InboxMessage[] = [];
|
|
262
|
+
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
const filePath = join(MESSAGES_DIR, file);
|
|
265
|
+
try {
|
|
266
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8")) as InboxMessage;
|
|
267
|
+
|
|
268
|
+
// Check if this message is addressed to us
|
|
269
|
+
if (!messageMatchesSession(data.to, _sessionAgentId, _sessionName)) continue;
|
|
270
|
+
|
|
271
|
+
// Normalize: ensure body is populated (legacy compat)
|
|
272
|
+
if (!data.body && data.message) data.body = data.message;
|
|
273
|
+
|
|
274
|
+
messages.push(data);
|
|
275
|
+
|
|
276
|
+
// Move to processed
|
|
277
|
+
try {
|
|
278
|
+
mkdirSync(PROCESSED_DIR, { recursive: true });
|
|
279
|
+
renameSync(filePath, join(PROCESSED_DIR, file));
|
|
280
|
+
} catch {
|
|
281
|
+
// If rename fails, try to delete
|
|
282
|
+
try { unlinkSync(filePath); } catch {}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Skip malformed files
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Sort by timestamp (oldest first)
|
|
290
|
+
messages.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
|
|
291
|
+
return messages;
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
174
295
|
}
|
|
175
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Count pending messages for this session without draining.
|
|
299
|
+
*/
|
|
176
300
|
export function inboxCount(): number {
|
|
177
|
-
|
|
301
|
+
try {
|
|
302
|
+
if (!existsSync(MESSAGES_DIR)) return 0;
|
|
303
|
+
|
|
304
|
+
const files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith(".json"));
|
|
305
|
+
let count = 0;
|
|
306
|
+
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
try {
|
|
309
|
+
const data = JSON.parse(readFileSync(join(MESSAGES_DIR, file), "utf-8"));
|
|
310
|
+
if (messageMatchesSession(data.to, _sessionAgentId, _sessionName)) count++;
|
|
311
|
+
} catch {
|
|
312
|
+
// Skip malformed
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return count;
|
|
317
|
+
} catch {
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get pending message counts broken down by session.
|
|
324
|
+
* Used by GET /status to show per-session counts.
|
|
325
|
+
*/
|
|
326
|
+
export function inboxCountBySession(): Record<string, number> {
|
|
327
|
+
const counts: Record<string, number> = {};
|
|
328
|
+
try {
|
|
329
|
+
if (!existsSync(MESSAGES_DIR)) return counts;
|
|
330
|
+
|
|
331
|
+
const files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith(".json"));
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
try {
|
|
334
|
+
const data = JSON.parse(readFileSync(join(MESSAGES_DIR, file), "utf-8"));
|
|
335
|
+
const to = data.to || "unknown";
|
|
336
|
+
counts[to] = (counts[to] || 0) + 1;
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
return counts;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Send a message to another agent or session via the file-based inbox.
|
|
345
|
+
* Phase 4: Cross-agent messaging. Works for any agent, any session.
|
|
346
|
+
* This is the file-based path. For OpenClaw agents, use sendMessage() (gateway).
|
|
347
|
+
*/
|
|
348
|
+
export function sendLdmMessage(opts: {
|
|
349
|
+
from?: string;
|
|
350
|
+
to: string;
|
|
351
|
+
body: string;
|
|
352
|
+
type?: string;
|
|
353
|
+
}): string | null {
|
|
354
|
+
try {
|
|
355
|
+
mkdirSync(MESSAGES_DIR, { recursive: true });
|
|
356
|
+
const id = randomUUID();
|
|
357
|
+
const data: InboxMessage = {
|
|
358
|
+
id,
|
|
359
|
+
type: opts.type || "chat",
|
|
360
|
+
from: opts.from || `${_sessionAgentId}:${_sessionName}`,
|
|
361
|
+
to: opts.to,
|
|
362
|
+
body: opts.body,
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
read: false,
|
|
365
|
+
};
|
|
366
|
+
writeFileSync(join(MESSAGES_DIR, `${id}.json`), JSON.stringify(data, null, 2) + "\n");
|
|
367
|
+
return id;
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Session management (Phase 2) ────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
const SESSIONS_DIR = join(LDM_ROOT, "sessions");
|
|
376
|
+
|
|
377
|
+
export interface SessionInfo {
|
|
378
|
+
name: string;
|
|
379
|
+
agentId: string;
|
|
380
|
+
pid: number;
|
|
381
|
+
startTime: string;
|
|
382
|
+
cwd: string;
|
|
383
|
+
alive: boolean;
|
|
384
|
+
meta?: Record<string, unknown>;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Register this bridge session in ~/.ldm/sessions/.
|
|
389
|
+
* Uses the agent--session naming convention.
|
|
390
|
+
*/
|
|
391
|
+
export function registerBridgeSession(): SessionInfo | null {
|
|
392
|
+
try {
|
|
393
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
394
|
+
const fileName = `${_sessionAgentId}--${_sessionName}.json`;
|
|
395
|
+
const data: SessionInfo = {
|
|
396
|
+
name: _sessionName,
|
|
397
|
+
agentId: _sessionAgentId,
|
|
398
|
+
pid: process.pid,
|
|
399
|
+
startTime: new Date().toISOString(),
|
|
400
|
+
cwd: process.cwd(),
|
|
401
|
+
alive: true,
|
|
402
|
+
};
|
|
403
|
+
writeFileSync(join(SESSIONS_DIR, fileName), JSON.stringify(data, null, 2) + "\n");
|
|
404
|
+
return data;
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* List active sessions. Validates PID liveness and cleans stale entries.
|
|
412
|
+
*/
|
|
413
|
+
export function listActiveSessions(agentFilter?: string): SessionInfo[] {
|
|
414
|
+
try {
|
|
415
|
+
if (!existsSync(SESSIONS_DIR)) return [];
|
|
416
|
+
|
|
417
|
+
const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith(".json"));
|
|
418
|
+
const sessions: SessionInfo[] = [];
|
|
419
|
+
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
try {
|
|
422
|
+
const filePath = join(SESSIONS_DIR, file);
|
|
423
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8")) as SessionInfo;
|
|
424
|
+
|
|
425
|
+
// PID liveness check
|
|
426
|
+
let alive = false;
|
|
427
|
+
try {
|
|
428
|
+
process.kill(data.pid, 0);
|
|
429
|
+
alive = true;
|
|
430
|
+
} catch {
|
|
431
|
+
// Dead PID. Clean up.
|
|
432
|
+
try { unlinkSync(filePath); } catch {}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (agentFilter && data.agentId !== agentFilter) continue;
|
|
437
|
+
|
|
438
|
+
sessions.push({ ...data, alive });
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return sessions;
|
|
443
|
+
} catch {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
178
446
|
}
|
|
179
447
|
|
|
180
448
|
// ── Send message to OpenClaw agent ───────────────────────────────────
|
|
@@ -197,10 +465,10 @@ export async function sendMessage(
|
|
|
197
465
|
Authorization: `Bearer ${token}`,
|
|
198
466
|
"Content-Type": "application/json",
|
|
199
467
|
"x-openclaw-scopes": "operator.read,operator.write",
|
|
468
|
+
"x-openclaw-session-key": `agent:${agentId}:main`,
|
|
200
469
|
},
|
|
201
470
|
body: JSON.stringify({
|
|
202
471
|
model: `openclaw/${agentId}`,
|
|
203
|
-
user: "main",
|
|
204
472
|
messages: [
|
|
205
473
|
{
|
|
206
474
|
role: "user",
|
package/src/bridge/mcp-server.ts
CHANGED
|
@@ -6,14 +6,21 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
7
7
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
resolveConfig,
|
|
13
14
|
sendMessage,
|
|
15
|
+
sendLdmMessage,
|
|
14
16
|
drainInbox,
|
|
15
17
|
pushInbox,
|
|
16
18
|
inboxCount,
|
|
19
|
+
inboxCountBySession,
|
|
20
|
+
setSessionIdentity,
|
|
21
|
+
getSessionIdentity,
|
|
22
|
+
registerBridgeSession,
|
|
23
|
+
listActiveSessions,
|
|
17
24
|
searchConversations,
|
|
18
25
|
searchWorkspace,
|
|
19
26
|
readWorkspaceFile,
|
|
@@ -28,7 +35,7 @@ import {
|
|
|
28
35
|
|
|
29
36
|
const config = resolveConfig();
|
|
30
37
|
|
|
31
|
-
const METRICS_DIR = join(process.env.HOME ||
|
|
38
|
+
const METRICS_DIR = join(process.env.HOME || homedir(), '.openclaw', 'memory');
|
|
32
39
|
const METRICS_PATH = join(METRICS_DIR, 'search-metrics.jsonl');
|
|
33
40
|
|
|
34
41
|
function logSearchMetric(tool: string, query: string, resultCount: number) {
|
|
@@ -64,22 +71,27 @@ function startInboxServer(cfg: BridgeConfig): void {
|
|
|
64
71
|
return;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
// POST /message: Write to file-based inbox (Phase 1 + 2 + 4)
|
|
75
|
+
// Accepts: { from, message|body, to?, type? }
|
|
76
|
+
// The "to" field supports: "cc-mini", "cc-mini:brainstorm", "cc-mini:*", "*"
|
|
67
77
|
if (req.method === "POST" && req.url === "/message") {
|
|
68
78
|
try {
|
|
69
79
|
const body = JSON.parse(await readBody(req));
|
|
70
|
-
const
|
|
80
|
+
const { agentId, sessionName } = getSessionIdentity();
|
|
81
|
+
const queued = pushInbox({
|
|
71
82
|
from: body.from || "agent",
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
body: body.body || body.message || "",
|
|
84
|
+
to: body.to || `${agentId}:${sessionName}`,
|
|
85
|
+
type: body.type || "chat",
|
|
86
|
+
});
|
|
87
|
+
const messageBody = body.body || body.message || "";
|
|
88
|
+
console.error(`wip-bridge inbox: message from ${body.from || "agent"} to ${body.to || "default"}`);
|
|
77
89
|
|
|
78
90
|
try {
|
|
79
91
|
server.sendLoggingMessage({
|
|
80
92
|
level: "info",
|
|
81
93
|
logger: "wip-bridge",
|
|
82
|
-
data: `[
|
|
94
|
+
data: `[inbox] ${body.from || "agent"}: ${messageBody}`,
|
|
83
95
|
});
|
|
84
96
|
} catch {}
|
|
85
97
|
|
|
@@ -92,9 +104,20 @@ function startInboxServer(cfg: BridgeConfig): void {
|
|
|
92
104
|
return;
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
// GET /status: Pending message counts (Phase 1 + 2)
|
|
95
108
|
if (req.method === "GET" && req.url === "/status") {
|
|
109
|
+
const pending = inboxCount();
|
|
110
|
+
const bySession = inboxCountBySession();
|
|
96
111
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
97
|
-
res.end(JSON.stringify({ ok: true, pending
|
|
112
|
+
res.end(JSON.stringify({ ok: true, pending, bySession }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// GET /sessions: List active sessions (Phase 2)
|
|
117
|
+
if (req.method === "GET" && req.url === "/sessions") {
|
|
118
|
+
const sessions = listActiveSessions();
|
|
119
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
120
|
+
res.end(JSON.stringify({ ok: true, sessions }));
|
|
98
121
|
return;
|
|
99
122
|
}
|
|
100
123
|
|
|
@@ -107,6 +130,8 @@ function startInboxServer(cfg: BridgeConfig): void {
|
|
|
107
130
|
});
|
|
108
131
|
|
|
109
132
|
httpServer.on("error", (err: Error) => {
|
|
133
|
+
// Port already bound by another bridge process. That's fine.
|
|
134
|
+
// This process reads from filesystem directly via check_inbox.
|
|
110
135
|
console.error(`wip-bridge inbox server error: ${err.message}`);
|
|
111
136
|
});
|
|
112
137
|
}
|
|
@@ -240,14 +265,14 @@ server.registerTool(
|
|
|
240
265
|
}
|
|
241
266
|
);
|
|
242
267
|
|
|
243
|
-
// Tool 5: Check inbox for messages
|
|
268
|
+
// Tool 5: Check inbox for messages (file-based, Phase 1)
|
|
244
269
|
server.registerTool(
|
|
245
270
|
"lesa_check_inbox",
|
|
246
271
|
{
|
|
247
272
|
description:
|
|
248
|
-
"Check for pending messages
|
|
249
|
-
"
|
|
250
|
-
"
|
|
273
|
+
"Check for pending messages in the file-based inbox (~/.ldm/messages/). " +
|
|
274
|
+
"Messages can come from OpenClaw agents, other Claude Code sessions, or CLI. " +
|
|
275
|
+
"Returns all pending messages for this session and marks them as read.",
|
|
251
276
|
inputSchema: {},
|
|
252
277
|
},
|
|
253
278
|
async () => {
|
|
@@ -258,13 +283,49 @@ server.registerTool(
|
|
|
258
283
|
}
|
|
259
284
|
|
|
260
285
|
const text = messages
|
|
261
|
-
.map((m) => `**${m.from}** (${m.timestamp}):\n${m.message}`)
|
|
286
|
+
.map((m) => `**${m.from}** [${m.type}] (${m.timestamp}):\n${m.body || m.message}`)
|
|
262
287
|
.join("\n\n---\n\n");
|
|
263
288
|
|
|
264
289
|
return { content: [{ type: "text" as const, text: `${messages.length} message(s):\n\n${text}` }] };
|
|
265
290
|
}
|
|
266
291
|
);
|
|
267
292
|
|
|
293
|
+
// Tool 6: Send message to any agent via file-based inbox (Phase 4)
|
|
294
|
+
server.registerTool(
|
|
295
|
+
"ldm_send_message",
|
|
296
|
+
{
|
|
297
|
+
description:
|
|
298
|
+
"Send a message to any agent or session via the file-based inbox (~/.ldm/messages/). " +
|
|
299
|
+
"Works for agent-to-agent communication. For OpenClaw agents (like Lesa), use lesa_send_message " +
|
|
300
|
+
"instead (goes through the gateway). This tool writes directly to the shared inbox.\n\n" +
|
|
301
|
+
"Target formats:\n" +
|
|
302
|
+
" 'cc-mini' ... default session\n" +
|
|
303
|
+
" 'cc-mini:brainstorm' ... named session\n" +
|
|
304
|
+
" 'cc-mini:*' ... broadcast to all sessions of that agent\n" +
|
|
305
|
+
" '*' ... broadcast to all agents",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
to: z.string().describe("Target: 'agent', 'agent:session', 'agent:*', or '*'"),
|
|
308
|
+
message: z.string().describe("Message body"),
|
|
309
|
+
type: z.string().optional().default("chat").describe("Message type: chat, system, task (default: chat)"),
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
async ({ to, message, type }) => {
|
|
313
|
+
const { agentId, sessionName } = getSessionIdentity();
|
|
314
|
+
const id = sendLdmMessage({
|
|
315
|
+
from: `${agentId}:${sessionName}`,
|
|
316
|
+
to,
|
|
317
|
+
body: message,
|
|
318
|
+
type,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (id) {
|
|
322
|
+
return { content: [{ type: "text" as const, text: `Message sent (id: ${id}) to ${to}` }] };
|
|
323
|
+
} else {
|
|
324
|
+
return { content: [{ type: "text" as const, text: "Failed to send message." }], isError: true };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
|
|
268
329
|
// ── OpenClaw Skill Bridge ────────────────────────────────────────────
|
|
269
330
|
|
|
270
331
|
function registerSkillTools(skills: SkillInfo[]): void {
|
|
@@ -350,6 +411,18 @@ function registerSkillTools(skills: SkillInfo[]): void {
|
|
|
350
411
|
// ── Start ────────────────────────────────────────────────────────────
|
|
351
412
|
|
|
352
413
|
async function main() {
|
|
414
|
+
// Phase 2: Set session identity from env or defaults
|
|
415
|
+
const agentId = process.env.LDM_AGENT_ID || "cc-mini";
|
|
416
|
+
const sessionName = process.env.LDM_SESSION_NAME || "default";
|
|
417
|
+
setSessionIdentity(agentId, sessionName);
|
|
418
|
+
console.error(`wip-bridge: session identity: ${agentId}:${sessionName}`);
|
|
419
|
+
|
|
420
|
+
// Phase 2: Register session in ~/.ldm/sessions/
|
|
421
|
+
const session = registerBridgeSession();
|
|
422
|
+
if (session) {
|
|
423
|
+
console.error(`wip-bridge: registered session ${agentId}--${sessionName} (pid ${session.pid})`);
|
|
424
|
+
}
|
|
425
|
+
|
|
353
426
|
startInboxServer(config);
|
|
354
427
|
|
|
355
428
|
// Discover and register OpenClaw skills
|