@geravant/sinain 1.0.19 → 1.2.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/README.md +10 -1
- package/cli.js +176 -0
- package/index.ts +4 -2
- package/install.js +89 -14
- package/launcher.js +622 -0
- package/openclaw.plugin.json +4 -0
- package/pack-prepare.js +48 -0
- package/package.json +24 -5
- package/sense_client/README.md +82 -0
- package/sense_client/__init__.py +1 -0
- package/sense_client/__main__.py +462 -0
- package/sense_client/app_detector.py +54 -0
- package/sense_client/app_detector_win.py +83 -0
- package/sense_client/capture.py +215 -0
- package/sense_client/capture_win.py +88 -0
- package/sense_client/change_detector.py +86 -0
- package/sense_client/config.py +64 -0
- package/sense_client/gate.py +145 -0
- package/sense_client/ocr.py +347 -0
- package/sense_client/privacy.py +65 -0
- package/sense_client/requirements.txt +13 -0
- package/sense_client/roi_extractor.py +84 -0
- package/sense_client/sender.py +173 -0
- package/sense_client/tests/__init__.py +0 -0
- package/sense_client/tests/test_stream1_optimizations.py +234 -0
- package/setup-overlay.js +82 -0
- package/sinain-agent/.env.example +17 -0
- package/sinain-agent/CLAUDE.md +87 -0
- package/sinain-agent/mcp-config.json +12 -0
- package/sinain-agent/run.sh +248 -0
- package/sinain-core/.env.example +93 -0
- package/sinain-core/package-lock.json +552 -0
- package/sinain-core/package.json +21 -0
- package/sinain-core/src/agent/analyzer.ts +366 -0
- package/sinain-core/src/agent/context-window.ts +172 -0
- package/sinain-core/src/agent/loop.ts +404 -0
- package/sinain-core/src/agent/situation-writer.ts +187 -0
- package/sinain-core/src/agent/traits.ts +520 -0
- package/sinain-core/src/audio/capture-spawner-macos.ts +44 -0
- package/sinain-core/src/audio/capture-spawner-win.ts +37 -0
- package/sinain-core/src/audio/capture-spawner.ts +14 -0
- package/sinain-core/src/audio/pipeline.ts +335 -0
- package/sinain-core/src/audio/transcription-local.ts +141 -0
- package/sinain-core/src/audio/transcription.ts +278 -0
- package/sinain-core/src/buffers/feed-buffer.ts +71 -0
- package/sinain-core/src/buffers/sense-buffer.ts +425 -0
- package/sinain-core/src/config.ts +245 -0
- package/sinain-core/src/escalation/escalation-slot.ts +136 -0
- package/sinain-core/src/escalation/escalator.ts +828 -0
- package/sinain-core/src/escalation/message-builder.ts +370 -0
- package/sinain-core/src/escalation/openclaw-ws.ts +726 -0
- package/sinain-core/src/escalation/scorer.ts +166 -0
- package/sinain-core/src/index.ts +537 -0
- package/sinain-core/src/learning/feedback-store.ts +253 -0
- package/sinain-core/src/learning/signal-collector.ts +218 -0
- package/sinain-core/src/log.ts +24 -0
- package/sinain-core/src/overlay/commands.ts +126 -0
- package/sinain-core/src/overlay/ws-handler.ts +267 -0
- package/sinain-core/src/privacy/index.ts +18 -0
- package/sinain-core/src/privacy/presets.ts +40 -0
- package/sinain-core/src/privacy/redact.ts +92 -0
- package/sinain-core/src/profiler.ts +181 -0
- package/sinain-core/src/recorder.ts +186 -0
- package/sinain-core/src/server.ts +456 -0
- package/sinain-core/src/trace/trace-store.ts +73 -0
- package/sinain-core/src/trace/tracer.ts +94 -0
- package/sinain-core/src/types.ts +427 -0
- package/sinain-core/src/util/dedup.ts +48 -0
- package/sinain-core/src/util/task-store.ts +84 -0
- package/sinain-core/tsconfig.json +18 -0
- package/sinain-knowledge/curation/engine.ts +137 -24
- package/sinain-knowledge/data/git-store.ts +26 -0
- package/sinain-knowledge/data/store.ts +117 -0
- package/sinain-mcp-server/index.ts +417 -0
- package/sinain-mcp-server/package.json +19 -0
- package/sinain-mcp-server/tsconfig.json +15 -0
- package/sinain-memory/graph_query.py +185 -0
- package/sinain-memory/knowledge_integrator.py +450 -0
- package/sinain-memory/memory-config.json +3 -1
- package/sinain-memory/session_distiller.py +162 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Configuration
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const SINAIN_CORE_URL = process.env.SINAIN_CORE_URL || "http://localhost:9500";
|
|
15
|
+
const WORKSPACE = (process.env.SINAIN_WORKSPACE || "~/.openclaw/workspace").replace(/^~/, os.homedir());
|
|
16
|
+
const MEMORY_DIR = resolve(WORKSPACE, "memory");
|
|
17
|
+
const MODULES_DIR = resolve(WORKSPACE, "modules");
|
|
18
|
+
|
|
19
|
+
const SCRIPTS_CANDIDATES = [
|
|
20
|
+
resolve(WORKSPACE, "sinain-memory"),
|
|
21
|
+
resolve(import.meta.dirname || ".", "..", "sinain-hud-plugin", "sinain-memory"),
|
|
22
|
+
];
|
|
23
|
+
const SCRIPTS_DIR = SCRIPTS_CANDIDATES.find((d) => existsSync(d)) || SCRIPTS_CANDIDATES[0];
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
function stripPrivateTags(text: string): string {
|
|
30
|
+
return text.replace(/<private>[\s\S]*?<\/private>/g, "[REDACTED]");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function coreRequest(method: string, path: string, body?: unknown): Promise<any> {
|
|
34
|
+
const url = `${SINAIN_CORE_URL}${path}`;
|
|
35
|
+
const opts: RequestInit = {
|
|
36
|
+
method,
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
};
|
|
39
|
+
if (body) opts.body = JSON.stringify(body);
|
|
40
|
+
const res = await fetch(url, opts);
|
|
41
|
+
const json = await res.json();
|
|
42
|
+
return json;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runScript(args: string[], timeoutMs = 30_000): Promise<string> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
execFile(
|
|
48
|
+
"python3",
|
|
49
|
+
args,
|
|
50
|
+
{
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
53
|
+
env: { ...process.env },
|
|
54
|
+
},
|
|
55
|
+
(err, stdout, stderr) => {
|
|
56
|
+
if (err) reject(new Error(`Script failed: ${err.message}\n${stderr}`));
|
|
57
|
+
else resolve(stdout);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function textResult(text: string) {
|
|
64
|
+
return { content: [{ type: "text" as const, text }] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Server
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: "sinain-mcp-server",
|
|
73
|
+
version: "0.1.0",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 1. sinain_get_escalation
|
|
77
|
+
server.tool(
|
|
78
|
+
"sinain_get_escalation",
|
|
79
|
+
"Get the current pending escalation from sinain-core",
|
|
80
|
+
{},
|
|
81
|
+
async () => {
|
|
82
|
+
try {
|
|
83
|
+
const data = await coreRequest("GET", "/escalation/pending");
|
|
84
|
+
if (!data || (data.status && data.status === "none")) {
|
|
85
|
+
return textResult("No pending escalation");
|
|
86
|
+
}
|
|
87
|
+
return textResult(stripPrivateTags(JSON.stringify(data, null, 2)));
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
return textResult(`Error fetching escalation: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// 2. sinain_respond
|
|
95
|
+
server.tool(
|
|
96
|
+
"sinain_respond",
|
|
97
|
+
"Respond to a pending escalation",
|
|
98
|
+
{ id: z.string(), response: z.string() },
|
|
99
|
+
async ({ id, response }) => {
|
|
100
|
+
try {
|
|
101
|
+
const data = await coreRequest("POST", "/escalation/respond", { id, response });
|
|
102
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
return textResult(`Error responding to escalation: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 3. sinain_get_context
|
|
110
|
+
server.tool(
|
|
111
|
+
"sinain_get_context",
|
|
112
|
+
"Get the current agent context window from sinain-core (screen + audio + feed)",
|
|
113
|
+
{},
|
|
114
|
+
async () => {
|
|
115
|
+
try {
|
|
116
|
+
const data = await coreRequest("GET", "/agent/context");
|
|
117
|
+
return textResult(stripPrivateTags(JSON.stringify(data, null, 2)));
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
return textResult(`Error fetching context: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 4. sinain_get_digest
|
|
125
|
+
server.tool(
|
|
126
|
+
"sinain_get_digest",
|
|
127
|
+
"Get the latest agent digest from sinain-core",
|
|
128
|
+
{},
|
|
129
|
+
async () => {
|
|
130
|
+
try {
|
|
131
|
+
const data = await coreRequest("GET", "/agent/digest");
|
|
132
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
return textResult(`Error fetching digest: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// 5. sinain_get_feedback
|
|
140
|
+
server.tool(
|
|
141
|
+
"sinain_get_feedback",
|
|
142
|
+
"Get recent learning feedback entries",
|
|
143
|
+
{ limit: z.number().optional().default(20) },
|
|
144
|
+
async ({ limit }) => {
|
|
145
|
+
try {
|
|
146
|
+
const data = await coreRequest("GET", `/learning/feedback?limit=${limit}`);
|
|
147
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
148
|
+
} catch (err: any) {
|
|
149
|
+
return textResult(`Error fetching feedback: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// 6. sinain_post_feed
|
|
155
|
+
server.tool(
|
|
156
|
+
"sinain_post_feed",
|
|
157
|
+
"Post a message to the sinain-core HUD feed",
|
|
158
|
+
{
|
|
159
|
+
text: z.string(),
|
|
160
|
+
priority: z.enum(["normal", "high", "urgent"]).optional().default("normal"),
|
|
161
|
+
},
|
|
162
|
+
async ({ text, priority }) => {
|
|
163
|
+
try {
|
|
164
|
+
const data = await coreRequest("POST", "/feed", { text, priority });
|
|
165
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
return textResult(`Error posting to feed: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 7. sinain_health
|
|
173
|
+
server.tool(
|
|
174
|
+
"sinain_health",
|
|
175
|
+
"Check sinain-core health status",
|
|
176
|
+
{},
|
|
177
|
+
async () => {
|
|
178
|
+
try {
|
|
179
|
+
const data = await coreRequest("GET", "/health");
|
|
180
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
return textResult(`Error checking health: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// 8. sinain_get_knowledge
|
|
188
|
+
server.tool(
|
|
189
|
+
"sinain_get_knowledge",
|
|
190
|
+
"Get the portable knowledge document (playbook + long-term facts + recent sessions)",
|
|
191
|
+
{},
|
|
192
|
+
async () => {
|
|
193
|
+
try {
|
|
194
|
+
// Read pre-rendered knowledge doc (fast, no subprocess)
|
|
195
|
+
const docPath = resolve(MEMORY_DIR, "sinain-knowledge.md");
|
|
196
|
+
if (existsSync(docPath)) {
|
|
197
|
+
const content = readFileSync(docPath, "utf-8");
|
|
198
|
+
return textResult(stripPrivateTags(content));
|
|
199
|
+
}
|
|
200
|
+
// Fallback: read playbook directly
|
|
201
|
+
const playbookPath = resolve(MEMORY_DIR, "sinain-playbook.md");
|
|
202
|
+
if (existsSync(playbookPath)) {
|
|
203
|
+
return textResult(stripPrivateTags(readFileSync(playbookPath, "utf-8")));
|
|
204
|
+
}
|
|
205
|
+
return textResult("No knowledge document available yet");
|
|
206
|
+
} catch (err: any) {
|
|
207
|
+
return textResult(`Error reading knowledge: ${err.message}`);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 8b. sinain_knowledge_query (graph query — entity-based lookup)
|
|
213
|
+
server.tool(
|
|
214
|
+
"sinain_knowledge_query",
|
|
215
|
+
"Query the knowledge graph for facts about specific entities/domains",
|
|
216
|
+
{
|
|
217
|
+
entities: z.array(z.string()).optional().default([]),
|
|
218
|
+
max_facts: z.number().optional().default(5),
|
|
219
|
+
},
|
|
220
|
+
async ({ entities, max_facts }) => {
|
|
221
|
+
try {
|
|
222
|
+
const dbPath = resolve(MEMORY_DIR, "knowledge-graph.db");
|
|
223
|
+
const scriptPath = resolve(SCRIPTS_DIR, "graph_query.py");
|
|
224
|
+
const args = [scriptPath, "--db", dbPath, "--max-facts", String(max_facts)];
|
|
225
|
+
if (entities.length > 0) {
|
|
226
|
+
args.push("--entities", JSON.stringify(entities));
|
|
227
|
+
}
|
|
228
|
+
const output = await runScript(args);
|
|
229
|
+
return textResult(stripPrivateTags(output));
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
return textResult(`Error querying graph: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// 8c. sinain_distill_session
|
|
237
|
+
server.tool(
|
|
238
|
+
"sinain_distill_session",
|
|
239
|
+
"Distill the current session into knowledge (playbook updates + graph facts)",
|
|
240
|
+
{
|
|
241
|
+
session_summary: z.string().optional().default("Bare agent session distillation"),
|
|
242
|
+
},
|
|
243
|
+
async ({ session_summary }) => {
|
|
244
|
+
const results: string[] = [];
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// Fetch feed items from sinain-core
|
|
248
|
+
const coreUrl = process.env.SINAIN_CORE_URL || "http://localhost:9500";
|
|
249
|
+
const feedResp = await fetch(`${coreUrl}/feed?after=0`).then(r => r.json());
|
|
250
|
+
const historyResp = await fetch(`${coreUrl}/agent/history?limit=10`).then(r => r.json());
|
|
251
|
+
|
|
252
|
+
const feedItems = (feedResp as any).messages ?? [];
|
|
253
|
+
const agentHistory = (historyResp as any).results ?? [];
|
|
254
|
+
|
|
255
|
+
if (feedItems.length < 3) {
|
|
256
|
+
return textResult("Not enough feed items to distill (need >3)");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Step 1: Distill
|
|
260
|
+
const transcript = JSON.stringify([...feedItems, ...agentHistory].slice(0, 100));
|
|
261
|
+
const meta = JSON.stringify({ ts: new Date().toISOString(), sessionKey: session_summary });
|
|
262
|
+
|
|
263
|
+
const distillOutput = await runScript([
|
|
264
|
+
resolve(SCRIPTS_DIR, "session_distiller.py"),
|
|
265
|
+
"--memory-dir", MEMORY_DIR,
|
|
266
|
+
"--transcript", transcript,
|
|
267
|
+
"--session-meta", meta,
|
|
268
|
+
], 30_000);
|
|
269
|
+
results.push(`[session_distiller] ${distillOutput.trim().slice(0, 500)}`);
|
|
270
|
+
|
|
271
|
+
const digest = JSON.parse(distillOutput.trim());
|
|
272
|
+
if (digest.isEmpty || digest.error) {
|
|
273
|
+
return textResult(`Distillation skipped: ${digest.error || "empty session"}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 2: Integrate
|
|
277
|
+
const integrateOutput = await runScript([
|
|
278
|
+
resolve(SCRIPTS_DIR, "knowledge_integrator.py"),
|
|
279
|
+
"--memory-dir", MEMORY_DIR,
|
|
280
|
+
"--digest", JSON.stringify(digest),
|
|
281
|
+
], 60_000);
|
|
282
|
+
results.push(`[knowledge_integrator] ${integrateOutput.trim().slice(0, 500)}`);
|
|
283
|
+
|
|
284
|
+
return textResult(stripPrivateTags(results.join("\n\n")));
|
|
285
|
+
} catch (err: any) {
|
|
286
|
+
return textResult(`Distillation error: ${err.message}`);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// 9. sinain_heartbeat_tick
|
|
292
|
+
server.tool(
|
|
293
|
+
"sinain_heartbeat_tick",
|
|
294
|
+
"Run the full heartbeat knowledge pipeline (signal analysis, insight synthesis, memory mining, playbook curation)",
|
|
295
|
+
{
|
|
296
|
+
session_summary: z.string().optional().default("Bare agent heartbeat tick"),
|
|
297
|
+
},
|
|
298
|
+
async ({ session_summary }) => {
|
|
299
|
+
const results: string[] = [];
|
|
300
|
+
const now = new Date().toISOString();
|
|
301
|
+
|
|
302
|
+
// Step 1: git_backup.sh
|
|
303
|
+
const gitBackupPath = resolve(SCRIPTS_DIR, "git_backup.sh");
|
|
304
|
+
if (existsSync(gitBackupPath)) {
|
|
305
|
+
try {
|
|
306
|
+
const out = await new Promise<string>((res, rej) => {
|
|
307
|
+
execFile("bash", [gitBackupPath, MEMORY_DIR], { timeout: 30_000 }, (err, stdout, stderr) => {
|
|
308
|
+
if (err) rej(new Error(`git_backup failed: ${err.message}\n${stderr}`));
|
|
309
|
+
else res(stdout);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
results.push(`[git_backup] ${out.trim() || "OK"}`);
|
|
313
|
+
} catch (err: any) {
|
|
314
|
+
results.push(`[git_backup] FAILED: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Step 2: signal_analyzer.py
|
|
319
|
+
try {
|
|
320
|
+
const out = await runScript([
|
|
321
|
+
resolve(SCRIPTS_DIR, "signal_analyzer.py"),
|
|
322
|
+
"--memory-dir", MEMORY_DIR,
|
|
323
|
+
"--session-summary", session_summary,
|
|
324
|
+
"--current-time", now,
|
|
325
|
+
]);
|
|
326
|
+
results.push(`[signal_analyzer] ${out.trim() || "OK"}`);
|
|
327
|
+
} catch (err: any) {
|
|
328
|
+
results.push(`[signal_analyzer] FAILED: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Step 3: insight_synthesizer.py
|
|
332
|
+
try {
|
|
333
|
+
const out = await runScript([
|
|
334
|
+
resolve(SCRIPTS_DIR, "insight_synthesizer.py"),
|
|
335
|
+
"--memory-dir", MEMORY_DIR,
|
|
336
|
+
"--session-summary", session_summary,
|
|
337
|
+
]);
|
|
338
|
+
results.push(`[insight_synthesizer] ${out.trim() || "OK"}`);
|
|
339
|
+
} catch (err: any) {
|
|
340
|
+
results.push(`[insight_synthesizer] FAILED: ${err.message}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Step 4: memory_miner.py
|
|
344
|
+
try {
|
|
345
|
+
const out = await runScript([
|
|
346
|
+
resolve(SCRIPTS_DIR, "memory_miner.py"),
|
|
347
|
+
"--memory-dir", MEMORY_DIR,
|
|
348
|
+
]);
|
|
349
|
+
results.push(`[memory_miner] ${out.trim() || "OK"}`);
|
|
350
|
+
} catch (err: any) {
|
|
351
|
+
results.push(`[memory_miner] FAILED: ${err.message}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Step 5: playbook_curator.py
|
|
355
|
+
try {
|
|
356
|
+
const out = await runScript([
|
|
357
|
+
resolve(SCRIPTS_DIR, "playbook_curator.py"),
|
|
358
|
+
"--memory-dir", MEMORY_DIR,
|
|
359
|
+
"--session-summary", session_summary,
|
|
360
|
+
]);
|
|
361
|
+
results.push(`[playbook_curator] ${out.trim() || "OK"}`);
|
|
362
|
+
} catch (err: any) {
|
|
363
|
+
results.push(`[playbook_curator] FAILED: ${err.message}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return textResult(stripPrivateTags(results.join("\n\n")));
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// 10. sinain_module_guidance
|
|
371
|
+
server.tool(
|
|
372
|
+
"sinain_module_guidance",
|
|
373
|
+
"Read guidance from all active modules in the workspace",
|
|
374
|
+
{},
|
|
375
|
+
async () => {
|
|
376
|
+
try {
|
|
377
|
+
const registryPath = resolve(MODULES_DIR, "module-registry.json");
|
|
378
|
+
if (!existsSync(registryPath)) {
|
|
379
|
+
return textResult("No modules configured");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
383
|
+
const modules: Array<{ name: string; active?: boolean }> = Array.isArray(registry)
|
|
384
|
+
? registry
|
|
385
|
+
: registry.modules || [];
|
|
386
|
+
|
|
387
|
+
const parts: string[] = [];
|
|
388
|
+
for (const mod of modules) {
|
|
389
|
+
if (mod.active === false) continue;
|
|
390
|
+
const guidancePath = resolve(MODULES_DIR, mod.name, "guidance.md");
|
|
391
|
+
if (existsSync(guidancePath)) {
|
|
392
|
+
const content = readFileSync(guidancePath, "utf-8");
|
|
393
|
+
parts.push(`## ${mod.name}\n\n${content}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (parts.length === 0) {
|
|
398
|
+
return textResult("No module guidance files found");
|
|
399
|
+
}
|
|
400
|
+
return textResult(stripPrivateTags(parts.join("\n\n---\n\n")));
|
|
401
|
+
} catch (err: any) {
|
|
402
|
+
return textResult(`Error reading module guidance: ${err.message}`);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Startup
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
async function main() {
|
|
412
|
+
const transport = new StdioServerTransport();
|
|
413
|
+
await server.connect(transport);
|
|
414
|
+
console.error(`sinain-mcp-server started (core=${SINAIN_CORE_URL}, workspace=${WORKSPACE})`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sinain-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for sinain-hud bare agent integration",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "npx tsx index.ts",
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^22.19.7",
|
|
16
|
+
"tsx": "^4.21.0",
|
|
17
|
+
"typescript": "^5.9.3"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "node16",
|
|
5
|
+
"moduleResolution": "node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["*.ts"],
|
|
14
|
+
"exclude": ["dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Graph Query — entity-based lookup of knowledge graph facts.
|
|
3
|
+
|
|
4
|
+
Thin wrapper around triplestore.py for querying facts by entity/domain.
|
|
5
|
+
Used by sinain-core (via HTTP endpoint) and sinain-mcp-server (via subprocess).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 graph_query.py --db memory/knowledge-graph.db \
|
|
9
|
+
--entities '["react-native", "metro-bundler"]' \
|
|
10
|
+
[--max-facts 5] [--format text|json]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def query_facts_by_entities(
|
|
20
|
+
db_path: str,
|
|
21
|
+
entities: list[str],
|
|
22
|
+
max_facts: int = 5,
|
|
23
|
+
) -> list[dict]:
|
|
24
|
+
"""Query knowledge graph for facts related to specified entities/domains."""
|
|
25
|
+
if not Path(db_path).exists():
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from triplestore import TripleStore
|
|
30
|
+
store = TripleStore(db_path)
|
|
31
|
+
|
|
32
|
+
# Find fact entity_ids that match the requested domains or entity names
|
|
33
|
+
placeholders = ",".join(["?" for _ in entities])
|
|
34
|
+
# Match by domain attribute OR by entity name substring
|
|
35
|
+
like_clauses = " OR ".join([f"entity_id LIKE ?" for _ in entities])
|
|
36
|
+
entity_likes = [f"fact:{e}%" for e in entities]
|
|
37
|
+
|
|
38
|
+
rows = store._conn.execute(
|
|
39
|
+
f"""SELECT DISTINCT entity_id FROM triples
|
|
40
|
+
WHERE NOT retracted AND (
|
|
41
|
+
(attribute = 'domain' AND value IN ({placeholders}))
|
|
42
|
+
OR ({like_clauses})
|
|
43
|
+
)
|
|
44
|
+
LIMIT ?""",
|
|
45
|
+
(*entities, *entity_likes, max_facts * 3),
|
|
46
|
+
).fetchall()
|
|
47
|
+
|
|
48
|
+
fact_ids = [r["entity_id"] for r in rows]
|
|
49
|
+
|
|
50
|
+
# Load full attributes for each fact, sorted by confidence
|
|
51
|
+
facts = []
|
|
52
|
+
for fid in fact_ids:
|
|
53
|
+
attrs = store.entity(fid)
|
|
54
|
+
if not attrs:
|
|
55
|
+
continue
|
|
56
|
+
fact = {"entityId": fid}
|
|
57
|
+
for a in attrs:
|
|
58
|
+
fact[a["attribute"]] = a["value"]
|
|
59
|
+
facts.append(fact)
|
|
60
|
+
|
|
61
|
+
# Sort by confidence descending
|
|
62
|
+
facts.sort(key=lambda f: float(f.get("confidence", "0")), reverse=True)
|
|
63
|
+
store.close()
|
|
64
|
+
return facts[:max_facts]
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"[warn] Graph query failed: {e}", file=sys.stderr)
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def query_top_facts(db_path: str, limit: int = 30) -> list[dict]:
|
|
71
|
+
"""Query top-N facts by confidence for knowledge doc rendering."""
|
|
72
|
+
if not Path(db_path).exists():
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
from triplestore import TripleStore
|
|
77
|
+
store = TripleStore(db_path)
|
|
78
|
+
|
|
79
|
+
rows = store._conn.execute(
|
|
80
|
+
"""SELECT entity_id, CAST(value AS REAL) as conf
|
|
81
|
+
FROM triples
|
|
82
|
+
WHERE attribute = 'confidence' AND NOT retracted
|
|
83
|
+
AND entity_id LIKE 'fact:%'
|
|
84
|
+
ORDER BY conf DESC
|
|
85
|
+
LIMIT ?""",
|
|
86
|
+
(limit,),
|
|
87
|
+
).fetchall()
|
|
88
|
+
|
|
89
|
+
facts = []
|
|
90
|
+
for row in rows:
|
|
91
|
+
fid = row["entity_id"]
|
|
92
|
+
attrs = store.entity(fid)
|
|
93
|
+
if not attrs:
|
|
94
|
+
continue
|
|
95
|
+
fact = {"entityId": fid}
|
|
96
|
+
for a in attrs:
|
|
97
|
+
fact[a["attribute"]] = a["value"]
|
|
98
|
+
facts.append(fact)
|
|
99
|
+
|
|
100
|
+
store.close()
|
|
101
|
+
return facts
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"[warn] Graph top-facts query failed: {e}", file=sys.stderr)
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def format_facts_text(facts: list[dict], max_chars: int = 500) -> str:
|
|
108
|
+
"""Format facts as human-readable text for escalation message injection."""
|
|
109
|
+
if not facts:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
lines = []
|
|
113
|
+
total = 0
|
|
114
|
+
for f in facts:
|
|
115
|
+
value = f.get("value", "")
|
|
116
|
+
conf = f.get("confidence", "?")
|
|
117
|
+
count = f.get("reinforce_count", "1")
|
|
118
|
+
domain = f.get("domain", "")
|
|
119
|
+
|
|
120
|
+
line = f"- {value} (confidence: {conf}, confirmed {count}x)"
|
|
121
|
+
if domain:
|
|
122
|
+
line = f"- [{domain}] {value} (confidence: {conf}, confirmed {count}x)"
|
|
123
|
+
|
|
124
|
+
if total + len(line) > max_chars:
|
|
125
|
+
break
|
|
126
|
+
lines.append(line)
|
|
127
|
+
total += len(line)
|
|
128
|
+
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def domain_fact_counts(db_path: str) -> dict[str, int]:
|
|
133
|
+
"""Count facts per domain for module emergence detection."""
|
|
134
|
+
if not Path(db_path).exists():
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
from triplestore import TripleStore
|
|
139
|
+
store = TripleStore(db_path)
|
|
140
|
+
|
|
141
|
+
rows = store._conn.execute(
|
|
142
|
+
"""SELECT value, COUNT(DISTINCT entity_id) as cnt
|
|
143
|
+
FROM triples
|
|
144
|
+
WHERE attribute = 'domain' AND NOT retracted
|
|
145
|
+
GROUP BY value
|
|
146
|
+
ORDER BY cnt DESC""",
|
|
147
|
+
).fetchall()
|
|
148
|
+
|
|
149
|
+
store.close()
|
|
150
|
+
return {r["value"]: r["cnt"] for r in rows}
|
|
151
|
+
except Exception:
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main() -> None:
|
|
156
|
+
parser = argparse.ArgumentParser(description="Graph Query")
|
|
157
|
+
parser.add_argument("--db", required=True, help="Path to knowledge-graph.db")
|
|
158
|
+
parser.add_argument("--entities", default=None, help="JSON array of entity/domain names")
|
|
159
|
+
parser.add_argument("--top", type=int, default=None, help="Query top-N facts by confidence")
|
|
160
|
+
parser.add_argument("--domain-counts", action="store_true", help="Show fact counts per domain")
|
|
161
|
+
parser.add_argument("--max-facts", type=int, default=5, help="Maximum facts to return")
|
|
162
|
+
parser.add_argument("--format", choices=["text", "json"], default="json", help="Output format")
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
if args.domain_counts:
|
|
166
|
+
counts = domain_fact_counts(args.db)
|
|
167
|
+
print(json.dumps(counts, indent=2))
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if args.top is not None:
|
|
171
|
+
facts = query_top_facts(args.db, limit=args.top)
|
|
172
|
+
elif args.entities:
|
|
173
|
+
entities = json.loads(args.entities)
|
|
174
|
+
facts = query_facts_by_entities(args.db, entities, max_facts=args.max_facts)
|
|
175
|
+
else:
|
|
176
|
+
facts = query_top_facts(args.db, limit=args.max_facts)
|
|
177
|
+
|
|
178
|
+
if args.format == "text":
|
|
179
|
+
print(format_facts_text(facts))
|
|
180
|
+
else:
|
|
181
|
+
print(json.dumps({"facts": facts, "count": len(facts)}, indent=2, ensure_ascii=False))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|