@geravant/sinain 1.0.18 → 1.0.19
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/index.ts +163 -1257
- package/install.js +2 -1
- package/package.json +4 -2
- package/sinain-knowledge/adapters/generic/adapter.ts +103 -0
- package/sinain-knowledge/adapters/interface.ts +72 -0
- package/sinain-knowledge/adapters/openclaw/adapter.ts +223 -0
- package/sinain-knowledge/curation/engine.ts +493 -0
- package/sinain-knowledge/curation/resilience.ts +336 -0
- package/sinain-knowledge/data/git-store.ts +310 -0
- package/sinain-knowledge/data/schema.ts +89 -0
- package/sinain-knowledge/data/snapshot.ts +226 -0
- package/sinain-knowledge/data/store.ts +488 -0
- package/sinain-knowledge/deploy/cli.ts +214 -0
- package/sinain-knowledge/deploy/manifest.ts +80 -0
- package/sinain-knowledge/protocol/bindings/generic.md +5 -0
- package/sinain-knowledge/protocol/bindings/openclaw.md +5 -0
- package/sinain-knowledge/protocol/heartbeat.md +62 -0
- package/sinain-knowledge/protocol/renderer.ts +56 -0
- package/sinain-knowledge/protocol/skill.md +335 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sinain-knowledge — KnowledgeStore
|
|
3
|
+
*
|
|
4
|
+
* Wraps all file I/O for the knowledge system: workspace setup, file deployment,
|
|
5
|
+
* playbook management, module guidance, session summaries, eval logs, and SITUATION.md.
|
|
6
|
+
*
|
|
7
|
+
* Pure Node.js built-in imports only — no OpenClaw dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
statSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { join, dirname, extname } from "node:path";
|
|
21
|
+
|
|
22
|
+
import type { Logger, ModuleRegistry } from "./schema.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// KnowledgeStore
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export class KnowledgeStore {
|
|
29
|
+
constructor(
|
|
30
|
+
private workspaceDir: string,
|
|
31
|
+
private logger: Logger,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/** Update the workspace directory (e.g. after before_agent_start provides it). */
|
|
35
|
+
setWorkspaceDir(dir: string): void {
|
|
36
|
+
this.workspaceDir = dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getWorkspaceDir(): string {
|
|
40
|
+
return this.workspaceDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Workspace setup ─────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** Ensure all memory sub-directories exist and are writable. */
|
|
46
|
+
ensureMemoryDirs(): void {
|
|
47
|
+
for (const dir of [
|
|
48
|
+
"memory",
|
|
49
|
+
"memory/playbook-archive",
|
|
50
|
+
"memory/playbook-logs",
|
|
51
|
+
"memory/eval-logs",
|
|
52
|
+
"memory/eval-reports",
|
|
53
|
+
]) {
|
|
54
|
+
const fullPath = join(this.workspaceDir, dir);
|
|
55
|
+
if (!existsSync(fullPath)) {
|
|
56
|
+
mkdirSync(fullPath, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
chmodSync(fullPath, 0o755);
|
|
60
|
+
} catch {
|
|
61
|
+
// Best-effort — may fail if owned by another user
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── File deployment ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sync a single source file to the workspace, writing only if content changed.
|
|
70
|
+
* Returns true if the file was written.
|
|
71
|
+
*/
|
|
72
|
+
deployFile(sourcePath: string | undefined, targetName: string): boolean {
|
|
73
|
+
if (!sourcePath) return false;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(sourcePath, "utf-8");
|
|
77
|
+
const targetPath = join(this.workspaceDir, targetName);
|
|
78
|
+
const targetDir = dirname(targetPath);
|
|
79
|
+
|
|
80
|
+
if (!existsSync(targetDir)) {
|
|
81
|
+
mkdirSync(targetDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let existing = "";
|
|
85
|
+
try {
|
|
86
|
+
existing = readFileSync(targetPath, "utf-8");
|
|
87
|
+
} catch {
|
|
88
|
+
// File doesn't exist yet
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (existing !== content) {
|
|
92
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
93
|
+
this.logger.info(`sinain-hud: synced ${targetName} to workspace`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
this.logger.warn(`sinain-hud: failed to sync ${targetName}: ${String(err)}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recursively sync a source directory to the workspace with selective overwrite policy:
|
|
105
|
+
* - .json, .sh, .txt, .jsonl, .py — always overwritten (infra/config files we control)
|
|
106
|
+
* - others — deploy-once only (skip if already exists; bot owns these after first deploy)
|
|
107
|
+
* Skips __pycache__ and hidden directories.
|
|
108
|
+
*/
|
|
109
|
+
deployDir(sourceDir: string, targetDirName: string): number {
|
|
110
|
+
if (!existsSync(sourceDir)) return 0;
|
|
111
|
+
const targetDir = join(this.workspaceDir, targetDirName);
|
|
112
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
113
|
+
|
|
114
|
+
const ALWAYS_OVERWRITE = new Set([".json", ".sh", ".txt", ".jsonl", ".py"]);
|
|
115
|
+
let synced = 0;
|
|
116
|
+
|
|
117
|
+
const syncRecursive = (srcDir: string, dstDir: string): void => {
|
|
118
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
119
|
+
for (const entry of readdirSync(srcDir)) {
|
|
120
|
+
const srcPath = join(srcDir, entry);
|
|
121
|
+
const dstPath = join(dstDir, entry);
|
|
122
|
+
const stat = statSync(srcPath);
|
|
123
|
+
if (stat.isDirectory()) {
|
|
124
|
+
if (entry.startsWith("__") || entry.startsWith(".")) continue;
|
|
125
|
+
syncRecursive(srcPath, dstPath);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!stat.isFile()) continue;
|
|
129
|
+
const ext = extname(entry).toLowerCase();
|
|
130
|
+
if (!ALWAYS_OVERWRITE.has(ext) && existsSync(dstPath)) continue;
|
|
131
|
+
const content = readFileSync(srcPath, "utf-8");
|
|
132
|
+
let existing = "";
|
|
133
|
+
try {
|
|
134
|
+
existing = readFileSync(dstPath, "utf-8");
|
|
135
|
+
} catch {}
|
|
136
|
+
if (existing !== content) {
|
|
137
|
+
writeFileSync(dstPath, content, "utf-8");
|
|
138
|
+
synced++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
syncRecursive(sourceDir, targetDir);
|
|
144
|
+
if (synced > 0) this.logger.info(`sinain-hud: synced ${synced} files to ${targetDirName}/`);
|
|
145
|
+
return synced;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Recursively sync a modules/ source directory to workspace with selective deploy policy:
|
|
150
|
+
* - manifest.json → always overwrite (plugin controls schema)
|
|
151
|
+
* - module-registry.json, patterns.md, guidance.md → deploy-once (agent manages)
|
|
152
|
+
* - context/*.json → always overwrite
|
|
153
|
+
*/
|
|
154
|
+
deployModules(sourceDir: string): number {
|
|
155
|
+
if (!existsSync(sourceDir)) return 0;
|
|
156
|
+
const targetDir = join(this.workspaceDir, "modules");
|
|
157
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
158
|
+
|
|
159
|
+
const ALWAYS_OVERWRITE = new Set(["manifest.json"]);
|
|
160
|
+
const DEPLOY_ONCE = new Set(["module-registry.json", "patterns.md", "guidance.md"]);
|
|
161
|
+
let synced = 0;
|
|
162
|
+
|
|
163
|
+
const syncRecursive = (srcDir: string, dstDir: string): void => {
|
|
164
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
for (const entry of readdirSync(srcDir)) {
|
|
167
|
+
const srcPath = join(srcDir, entry);
|
|
168
|
+
const dstPath = join(dstDir, entry);
|
|
169
|
+
const stat = statSync(srcPath);
|
|
170
|
+
|
|
171
|
+
if (stat.isDirectory()) {
|
|
172
|
+
syncRecursive(srcPath, dstPath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!stat.isFile()) continue;
|
|
177
|
+
|
|
178
|
+
const fileName = entry;
|
|
179
|
+
const isAlwaysOverwrite = ALWAYS_OVERWRITE.has(fileName) || fileName.startsWith("context/");
|
|
180
|
+
const isDeployOnce = DEPLOY_ONCE.has(fileName);
|
|
181
|
+
|
|
182
|
+
if (isDeployOnce && existsSync(dstPath)) continue;
|
|
183
|
+
if (!isAlwaysOverwrite && !isDeployOnce && existsSync(dstPath)) continue;
|
|
184
|
+
|
|
185
|
+
const content = readFileSync(srcPath, "utf-8");
|
|
186
|
+
let existing = "";
|
|
187
|
+
try {
|
|
188
|
+
existing = readFileSync(dstPath, "utf-8");
|
|
189
|
+
} catch {}
|
|
190
|
+
if (existing !== content) {
|
|
191
|
+
writeFileSync(dstPath, content, "utf-8");
|
|
192
|
+
synced++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
syncRecursive(sourceDir, targetDir);
|
|
198
|
+
if (synced > 0) this.logger.info(`sinain-hud: synced ${synced} module files to modules/`);
|
|
199
|
+
return synced;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Playbook ────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
readPlaybook(): string | null {
|
|
205
|
+
const p = join(this.workspaceDir, "memory", "sinain-playbook.md");
|
|
206
|
+
try {
|
|
207
|
+
return readFileSync(p, "utf-8");
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
writePlaybook(content: string): void {
|
|
214
|
+
const dir = join(this.workspaceDir, "memory");
|
|
215
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
216
|
+
writeFileSync(join(dir, "sinain-playbook.md"), content, "utf-8");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
readEffectivePlaybook(): string | null {
|
|
220
|
+
const p = join(this.workspaceDir, "memory", "sinain-playbook-effective.md");
|
|
221
|
+
try {
|
|
222
|
+
return readFileSync(p, "utf-8");
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generate the merged effective playbook from active modules + base playbook.
|
|
230
|
+
* Reads module-registry.json, collects patterns.md from each active module
|
|
231
|
+
* (sorted by priority desc), reads the base sinain-playbook.md, and writes
|
|
232
|
+
* the merged result to memory/sinain-playbook-effective.md.
|
|
233
|
+
*/
|
|
234
|
+
generateEffectivePlaybook(): boolean {
|
|
235
|
+
const registryPath = join(this.workspaceDir, "modules", "module-registry.json");
|
|
236
|
+
if (!existsSync(registryPath)) {
|
|
237
|
+
this.logger.info("sinain-hud: no module-registry.json found, skipping effective playbook generation");
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let registry: ModuleRegistry;
|
|
242
|
+
try {
|
|
243
|
+
registry = JSON.parse(readFileSync(registryPath, "utf-8")) as ModuleRegistry;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
this.logger.warn(`sinain-hud: failed to parse module-registry.json: ${String(err)}`);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Collect active modules sorted by priority desc
|
|
250
|
+
const activeModules: Array<{ id: string; priority: number }> = [];
|
|
251
|
+
for (const [id, entry] of Object.entries(registry.modules)) {
|
|
252
|
+
if (entry.status === "active") {
|
|
253
|
+
activeModules.push({ id, priority: entry.priority });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
activeModules.sort((a, b) => b.priority - a.priority);
|
|
257
|
+
|
|
258
|
+
// Build module stack header
|
|
259
|
+
const stackLabel = activeModules.map((m) => `${m.id}(${m.priority})`).join(", ");
|
|
260
|
+
|
|
261
|
+
// Collect patterns from each active module
|
|
262
|
+
const sections: string[] = [];
|
|
263
|
+
sections.push(`<!-- module-stack: ${stackLabel} -->`);
|
|
264
|
+
sections.push("");
|
|
265
|
+
|
|
266
|
+
for (const mod of activeModules) {
|
|
267
|
+
const patternsPath = join(this.workspaceDir, "modules", mod.id, "patterns.md");
|
|
268
|
+
if (!existsSync(patternsPath)) continue;
|
|
269
|
+
try {
|
|
270
|
+
const patterns = readFileSync(patternsPath, "utf-8").trim();
|
|
271
|
+
if (patterns) {
|
|
272
|
+
sections.push(`<!-- module: ${mod.id} (priority ${mod.priority}) -->`);
|
|
273
|
+
const manifestPath = join(this.workspaceDir, "modules", mod.id, "manifest.json");
|
|
274
|
+
if (existsSync(manifestPath)) {
|
|
275
|
+
try {
|
|
276
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
277
|
+
if (manifest.importedAt) {
|
|
278
|
+
sections.push(`> *[Transferred knowledge: ${manifest.name || mod.id}]*`);
|
|
279
|
+
}
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
sections.push(patterns);
|
|
283
|
+
sections.push("");
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// Skip unreadable patterns
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Append base playbook
|
|
291
|
+
const basePlaybookPath = join(this.workspaceDir, "memory", "sinain-playbook.md");
|
|
292
|
+
if (existsSync(basePlaybookPath)) {
|
|
293
|
+
try {
|
|
294
|
+
const base = readFileSync(basePlaybookPath, "utf-8").trim();
|
|
295
|
+
if (base) {
|
|
296
|
+
sections.push("<!-- base-playbook -->");
|
|
297
|
+
sections.push(base);
|
|
298
|
+
sections.push("");
|
|
299
|
+
}
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Write effective playbook (always overwrite)
|
|
304
|
+
const effectivePath = join(this.workspaceDir, "memory", "sinain-playbook-effective.md");
|
|
305
|
+
const effectiveDir = dirname(effectivePath);
|
|
306
|
+
if (!existsSync(effectiveDir)) mkdirSync(effectiveDir, { recursive: true });
|
|
307
|
+
|
|
308
|
+
const content = sections.join("\n");
|
|
309
|
+
writeFileSync(effectivePath, content, "utf-8");
|
|
310
|
+
this.logger.info(`sinain-hud: generated effective playbook (${activeModules.length} active modules)`);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Update the effectiveness footer in the base playbook with fresh metrics.
|
|
316
|
+
*/
|
|
317
|
+
updateEffectivenessFooter(effectiveness: Record<string, unknown>): void {
|
|
318
|
+
const playbookPath = join(this.workspaceDir, "memory", "sinain-playbook.md");
|
|
319
|
+
if (!existsSync(playbookPath)) return;
|
|
320
|
+
let content = readFileSync(playbookPath, "utf-8");
|
|
321
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
322
|
+
const newFooter = `<!-- effectiveness: outputs=${effectiveness.outputs ?? 0}, positive=${effectiveness.positive ?? 0}, negative=${effectiveness.negative ?? 0}, neutral=${effectiveness.neutral ?? 0}, rate=${effectiveness.rate ?? 0}, updated=${today} -->`;
|
|
323
|
+
const footerRe = /<!--\s*effectiveness:[^>]+-->/;
|
|
324
|
+
if (footerRe.test(content)) {
|
|
325
|
+
content = content.replace(footerRe, newFooter);
|
|
326
|
+
} else {
|
|
327
|
+
content = content.trimEnd() + "\n\n" + newFooter + "\n";
|
|
328
|
+
}
|
|
329
|
+
writeFileSync(playbookPath, content, "utf-8");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Modules ─────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
readModuleRegistry(): ModuleRegistry | null {
|
|
335
|
+
const p = join(this.workspaceDir, "modules", "module-registry.json");
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(readFileSync(p, "utf-8")) as ModuleRegistry;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Collect behavioral guidance from all active modules for prependContext injection.
|
|
345
|
+
* Returns a formatted [MODULE GUIDANCE] block or empty string.
|
|
346
|
+
*/
|
|
347
|
+
getActiveModuleGuidance(): string {
|
|
348
|
+
const registryPath = join(this.workspaceDir, "modules", "module-registry.json");
|
|
349
|
+
if (!existsSync(registryPath)) return "";
|
|
350
|
+
|
|
351
|
+
let registry: ModuleRegistry;
|
|
352
|
+
try {
|
|
353
|
+
registry = JSON.parse(readFileSync(registryPath, "utf-8")) as ModuleRegistry;
|
|
354
|
+
} catch {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Active modules sorted by priority desc
|
|
359
|
+
const activeModules: Array<{ id: string; priority: number }> = [];
|
|
360
|
+
for (const [id, entry] of Object.entries(registry.modules)) {
|
|
361
|
+
if (entry.status === "active") {
|
|
362
|
+
activeModules.push({ id, priority: entry.priority });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
activeModules.sort((a, b) => b.priority - a.priority);
|
|
366
|
+
|
|
367
|
+
const guidanceSections: string[] = [];
|
|
368
|
+
let moduleCount = 0;
|
|
369
|
+
|
|
370
|
+
for (const mod of activeModules) {
|
|
371
|
+
const guidancePath = join(this.workspaceDir, "modules", mod.id, "guidance.md");
|
|
372
|
+
if (!existsSync(guidancePath)) continue;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const content = readFileSync(guidancePath, "utf-8").trim();
|
|
376
|
+
if (!content) continue;
|
|
377
|
+
|
|
378
|
+
let label = mod.id;
|
|
379
|
+
const manifestPath = join(this.workspaceDir, "modules", mod.id, "manifest.json");
|
|
380
|
+
if (existsSync(manifestPath)) {
|
|
381
|
+
try {
|
|
382
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
383
|
+
if (manifest.importedAt) {
|
|
384
|
+
label = `${manifest.name || mod.id} [transferred]`;
|
|
385
|
+
}
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
guidanceSections.push(`### ${label}\n${content}`);
|
|
390
|
+
moduleCount++;
|
|
391
|
+
} catch {
|
|
392
|
+
// Skip unreadable guidance
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (guidanceSections.length === 0) return "";
|
|
397
|
+
|
|
398
|
+
this.logger.info(`sinain-hud: injecting guidance from ${moduleCount} module(s)`);
|
|
399
|
+
return `[MODULE GUIDANCE]\n${guidanceSections.join("\n\n")}`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Session summaries ───────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
appendSessionSummary(summary: Record<string, unknown>): void {
|
|
405
|
+
const summaryPath = join(this.workspaceDir, "memory", "session-summaries.jsonl");
|
|
406
|
+
const dir = dirname(summaryPath);
|
|
407
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
408
|
+
writeFileSync(summaryPath, JSON.stringify(summary) + "\n", { flag: "a" });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Playbook logs ──────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
appendPlaybookLog(entry: Record<string, unknown>): void {
|
|
414
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
415
|
+
const logDir = join(this.workspaceDir, "memory", "playbook-logs");
|
|
416
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
417
|
+
writeFileSync(
|
|
418
|
+
join(logDir, `${dateStr}.jsonl`),
|
|
419
|
+
JSON.stringify(entry) + "\n",
|
|
420
|
+
{ flag: "a" },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
appendCurationLog(entry: Record<string, unknown>): void {
|
|
425
|
+
this.appendPlaybookLog(entry);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── Eval ────────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
readEvalConfig(): Record<string, unknown> | null {
|
|
431
|
+
const p = join(this.workspaceDir, "memory", "eval-config.json");
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
readLatestEvalReport(): string | null {
|
|
440
|
+
const reportsDir = join(this.workspaceDir, "memory", "eval-reports");
|
|
441
|
+
if (!existsSync(reportsDir)) return null;
|
|
442
|
+
const reports = readdirSync(reportsDir)
|
|
443
|
+
.filter((f: string) => f.endsWith(".md"))
|
|
444
|
+
.sort()
|
|
445
|
+
.reverse();
|
|
446
|
+
if (reports.length === 0) return null;
|
|
447
|
+
try {
|
|
448
|
+
return readFileSync(join(reportsDir, reports[0]), "utf-8");
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
readRecentEvalLogs(n: number): string[] {
|
|
455
|
+
const logsDir = join(this.workspaceDir, "memory", "eval-logs");
|
|
456
|
+
if (!existsSync(logsDir)) return [];
|
|
457
|
+
const logFiles = readdirSync(logsDir)
|
|
458
|
+
.filter((f: string) => f.endsWith(".jsonl"))
|
|
459
|
+
.sort()
|
|
460
|
+
.reverse();
|
|
461
|
+
if (logFiles.length === 0) return [];
|
|
462
|
+
try {
|
|
463
|
+
const content = readFileSync(join(logsDir, logFiles[0]), "utf-8");
|
|
464
|
+
return content.trim().split("\n").slice(-n);
|
|
465
|
+
} catch {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── SITUATION.md ────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
readSituation(): string | null {
|
|
473
|
+
const p = join(this.workspaceDir, "SITUATION.md");
|
|
474
|
+
try {
|
|
475
|
+
const content = readFileSync(p, "utf-8").trim();
|
|
476
|
+
return content || null;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
writeSituation(content: string): void {
|
|
483
|
+
const situationPath = join(this.workspaceDir, "SITUATION.md");
|
|
484
|
+
const tmpPath = situationPath + ".rpc.tmp";
|
|
485
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
486
|
+
renameSync(tmpPath, situationPath);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sinain-knowledge CLI
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* install [--backend openclaw|generic] — Deploy knowledge system to workspace
|
|
8
|
+
* snapshot export <file> — Export knowledge state to file
|
|
9
|
+
* snapshot import <file> — Import knowledge state from file
|
|
10
|
+
* protocol render --binding <name> — Render protocol templates with bindings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
14
|
+
import { resolve, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
|
|
20
|
+
async function main(): Promise<void> {
|
|
21
|
+
const command = args[0];
|
|
22
|
+
|
|
23
|
+
switch (command) {
|
|
24
|
+
case "install":
|
|
25
|
+
await cmdInstall();
|
|
26
|
+
break;
|
|
27
|
+
case "snapshot":
|
|
28
|
+
await cmdSnapshot();
|
|
29
|
+
break;
|
|
30
|
+
case "protocol":
|
|
31
|
+
await cmdProtocol();
|
|
32
|
+
break;
|
|
33
|
+
case "--help":
|
|
34
|
+
case "-h":
|
|
35
|
+
case undefined:
|
|
36
|
+
printUsage();
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
console.error(`Unknown command: ${command}`);
|
|
40
|
+
printUsage();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printUsage(): void {
|
|
46
|
+
console.log(`sinain-knowledge CLI
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
sinain-knowledge install [--backend openclaw|generic] [--workspace <path>]
|
|
50
|
+
sinain-knowledge snapshot export <file>
|
|
51
|
+
sinain-knowledge snapshot import <file> [--workspace <path>]
|
|
52
|
+
sinain-knowledge snapshot save [--workspace <path>] [--repo <path>]
|
|
53
|
+
sinain-knowledge snapshot list [--repo <path>] [--count <n>]
|
|
54
|
+
sinain-knowledge snapshot restore <ref> [--workspace <path>] [--repo <path>]
|
|
55
|
+
sinain-knowledge protocol render --protocol <heartbeat|skill> --binding <openclaw|generic>
|
|
56
|
+
sinain-knowledge protocol render --protocol <heartbeat|skill> --binding <openclaw|generic> --output <file>
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Install ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
async function cmdInstall(): Promise<void> {
|
|
63
|
+
const backend = getArg("--backend") ?? "openclaw";
|
|
64
|
+
const workspace = getArg("--workspace") ?? process.cwd();
|
|
65
|
+
|
|
66
|
+
console.log(`Installing sinain-knowledge (backend: ${backend}, workspace: ${workspace})`);
|
|
67
|
+
|
|
68
|
+
// Import renderer to generate HEARTBEAT.md and SKILL.md
|
|
69
|
+
const { render } = await import("../protocol/renderer.js");
|
|
70
|
+
const binding = backend === "openclaw" ? "openclaw" : "generic";
|
|
71
|
+
|
|
72
|
+
const heartbeat = render("heartbeat", binding as any);
|
|
73
|
+
const skill = render("skill", binding as any);
|
|
74
|
+
|
|
75
|
+
writeFileSync(resolve(workspace, "HEARTBEAT.md"), heartbeat, "utf-8");
|
|
76
|
+
writeFileSync(resolve(workspace, "SKILL.md"), skill, "utf-8");
|
|
77
|
+
|
|
78
|
+
console.log(` Written HEARTBEAT.md (${heartbeat.length} chars)`);
|
|
79
|
+
console.log(` Written SKILL.md (${skill.length} chars)`);
|
|
80
|
+
console.log("Done.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Snapshot ────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
async function cmdSnapshot(): Promise<void> {
|
|
86
|
+
const subcommand = args[1];
|
|
87
|
+
|
|
88
|
+
if (subcommand === "export") {
|
|
89
|
+
const file = args[2];
|
|
90
|
+
if (!file) {
|
|
91
|
+
console.error("Usage: sinain-knowledge snapshot export <file>");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const workspace = getArg("--workspace") ?? process.cwd();
|
|
96
|
+
const { KnowledgeStore } = await import("../data/store.js");
|
|
97
|
+
const { exportSnapshot } = await import("../data/snapshot.js");
|
|
98
|
+
|
|
99
|
+
const logger = { info: console.log, warn: console.warn };
|
|
100
|
+
const store = new KnowledgeStore(workspace, logger);
|
|
101
|
+
const snapshot = exportSnapshot(store);
|
|
102
|
+
|
|
103
|
+
writeFileSync(resolve(file), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
104
|
+
console.log(`Snapshot exported to ${file} (${JSON.stringify(snapshot).length} bytes)`);
|
|
105
|
+
} else if (subcommand === "import") {
|
|
106
|
+
const file = args[2];
|
|
107
|
+
if (!file) {
|
|
108
|
+
console.error("Usage: sinain-knowledge snapshot import <file>");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const workspace = getArg("--workspace") ?? process.cwd();
|
|
113
|
+
const { KnowledgeStore } = await import("../data/store.js");
|
|
114
|
+
const { importSnapshot } = await import("../data/snapshot.js");
|
|
115
|
+
|
|
116
|
+
const logger = { info: console.log, warn: console.warn };
|
|
117
|
+
const store = new KnowledgeStore(workspace, logger);
|
|
118
|
+
const snapshot = JSON.parse(readFileSync(resolve(file), "utf-8"));
|
|
119
|
+
|
|
120
|
+
importSnapshot(store, snapshot);
|
|
121
|
+
console.log(`Snapshot imported from ${file}`);
|
|
122
|
+
} else if (subcommand === "save") {
|
|
123
|
+
const workspace = getArg("--workspace") ?? process.cwd();
|
|
124
|
+
const repoPath = getArg("--repo");
|
|
125
|
+
|
|
126
|
+
const { KnowledgeStore } = await import("../data/store.js");
|
|
127
|
+
const { GitSnapshotStore } = await import("../data/git-store.js");
|
|
128
|
+
|
|
129
|
+
const logger = { info: console.log, warn: console.warn };
|
|
130
|
+
const store = new KnowledgeStore(workspace, logger);
|
|
131
|
+
const gitStore = new GitSnapshotStore(repoPath, logger);
|
|
132
|
+
|
|
133
|
+
const hash = await gitStore.save(store);
|
|
134
|
+
console.log(`Snapshot saved → ${hash} (repo: ${gitStore.getRepoPath()})`);
|
|
135
|
+
} else if (subcommand === "list") {
|
|
136
|
+
const repoPath = getArg("--repo");
|
|
137
|
+
const count = parseInt(getArg("--count") ?? "20", 10);
|
|
138
|
+
|
|
139
|
+
const { GitSnapshotStore } = await import("../data/git-store.js");
|
|
140
|
+
|
|
141
|
+
const logger = { info: () => {}, warn: console.warn };
|
|
142
|
+
const gitStore = new GitSnapshotStore(repoPath, logger);
|
|
143
|
+
const entries = await gitStore.list(count);
|
|
144
|
+
|
|
145
|
+
if (entries.length === 0) {
|
|
146
|
+
console.log("No snapshots found.");
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`${entries.length} snapshot(s) in ${gitStore.getRepoPath()}:\n`);
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
console.log(` ${e.hash} ${e.date} ${e.subject}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (subcommand === "restore") {
|
|
154
|
+
const ref = args[2];
|
|
155
|
+
if (!ref) {
|
|
156
|
+
console.error("Usage: sinain-knowledge snapshot restore <ref>");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const workspace = getArg("--workspace") ?? process.cwd();
|
|
161
|
+
const repoPath = getArg("--repo");
|
|
162
|
+
|
|
163
|
+
const { KnowledgeStore } = await import("../data/store.js");
|
|
164
|
+
const { GitSnapshotStore } = await import("../data/git-store.js");
|
|
165
|
+
|
|
166
|
+
const logger = { info: console.log, warn: console.warn };
|
|
167
|
+
const store = new KnowledgeStore(workspace, logger);
|
|
168
|
+
const gitStore = new GitSnapshotStore(repoPath, logger);
|
|
169
|
+
|
|
170
|
+
await gitStore.restore(store, ref);
|
|
171
|
+
console.log(`Snapshot ${ref} restored to ${workspace}`);
|
|
172
|
+
} else {
|
|
173
|
+
console.error("Usage: sinain-knowledge snapshot <export|import|save|list|restore>");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Protocol ────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
async function cmdProtocol(): Promise<void> {
|
|
181
|
+
const subcommand = args[1];
|
|
182
|
+
|
|
183
|
+
if (subcommand !== "render") {
|
|
184
|
+
console.error("Usage: sinain-knowledge protocol render --protocol <name> --binding <name>");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const protocol = getArg("--protocol") ?? "heartbeat";
|
|
189
|
+
const binding = getArg("--binding") ?? "openclaw";
|
|
190
|
+
const output = getArg("--output");
|
|
191
|
+
|
|
192
|
+
const { render } = await import("../protocol/renderer.js");
|
|
193
|
+
const result = render(protocol as any, binding as any);
|
|
194
|
+
|
|
195
|
+
if (output) {
|
|
196
|
+
writeFileSync(resolve(output), result, "utf-8");
|
|
197
|
+
console.log(`Rendered ${protocol} with ${binding} binding → ${output}`);
|
|
198
|
+
} else {
|
|
199
|
+
process.stdout.write(result);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function getArg(flag: string): string | undefined {
|
|
206
|
+
const idx = args.indexOf(flag);
|
|
207
|
+
if (idx < 0 || idx + 1 >= args.length) return undefined;
|
|
208
|
+
return args[idx + 1];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main().catch((err) => {
|
|
212
|
+
console.error(err);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|