lazyclaw 4.3.0 → 5.0.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.ko.md +44 -0
- package/README.md +172 -508
- package/channels/handoff.mjs +41 -0
- package/channels/loader.mjs +124 -0
- package/channels/threads.mjs +116 -0
- package/cli.mjs +398 -6
- package/daemon.mjs +13 -0
- package/mas/agent_turn.mjs +28 -0
- package/mas/confidence.mjs +108 -0
- package/mas/index_db.mjs +242 -0
- package/mas/nudge.mjs +97 -0
- package/mas/prompt_stack.mjs +80 -0
- package/mas/skill_synth.mjs +124 -25
- package/mas/tool_runner.mjs +10 -61
- package/mas/tools/browser.mjs +77 -0
- package/mas/tools/clarify.mjs +36 -0
- package/mas/tools/coding.mjs +109 -0
- package/mas/tools/delegation.mjs +53 -0
- package/mas/tools/edit.mjs +36 -0
- package/mas/tools/git.mjs +110 -0
- package/mas/tools/ha.mjs +34 -0
- package/mas/tools/learning.mjs +168 -0
- package/mas/tools/media.mjs +105 -0
- package/mas/tools/os.mjs +152 -0
- package/mas/tools/patch.mjs +91 -0
- package/mas/tools/recall.mjs +103 -0
- package/mas/tools/registry.mjs +93 -0
- package/mas/tools/scheduling.mjs +62 -0
- package/mas/tools/web.mjs +137 -0
- package/mas/toolsets.mjs +64 -0
- package/mas/trajectory_export.mjs +169 -0
- package/mas/trajectory_store.mjs +179 -0
- package/mas/user_modeler.mjs +108 -0
- package/package.json +20 -3
- package/providers/codex_cli.mjs +200 -0
- package/providers/gemini_cli.mjs +179 -0
- package/providers/registry.mjs +61 -1
- package/sandbox/base.mjs +82 -0
- package/sandbox/confiners/bubblewrap.mjs +21 -0
- package/sandbox/confiners/firejail.mjs +16 -0
- package/sandbox/confiners/landlock.mjs +14 -0
- package/sandbox/confiners/seatbelt.mjs +28 -0
- package/sandbox/daytona.mjs +37 -0
- package/sandbox/docker.mjs +91 -0
- package/sandbox/index.mjs +67 -0
- package/sandbox/local.mjs +59 -0
- package/sandbox/modal.mjs +53 -0
- package/sandbox/singularity.mjs +39 -0
- package/sandbox/ssh.mjs +56 -0
- package/sandbox.mjs +11 -127
- package/scripts/hermes-import.mjs +111 -0
- package/scripts/migrate-v5.mjs +342 -0
- package/scripts/openclaw-import.mjs +71 -0
- package/sessions.mjs +20 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/migrate-v5.mjs — Phase A baseline migration (spec §10).
|
|
3
|
+
//
|
|
4
|
+
// Steps (each is idempotent — a second run is a no-op):
|
|
5
|
+
// 1. Backup <configDir> to <configDir>/backup-v4-<ts>/ (only when no
|
|
6
|
+
// prior backup exists for the current schema version).
|
|
7
|
+
// 2. Rewrite config.json: ensure trainer.provider defaults to "auto"
|
|
8
|
+
// when omitted (canonical C9). Existing trainer blocks are left
|
|
9
|
+
// alone.
|
|
10
|
+
// 3. Walk skills/*.md and add missing frontmatter fields per
|
|
11
|
+
// canonical decisions:
|
|
12
|
+
// - group: filename-hyphen-prefix or 'legacy' (C5)
|
|
13
|
+
// - trained_by: 'legacy' for pre-v5 skills (C4)
|
|
14
|
+
// Existing fields are never overwritten.
|
|
15
|
+
// 4. Rebuild index.db from on-disk sessions, skills, memory.
|
|
16
|
+
//
|
|
17
|
+
// Phases B+ extend this script with user-modeler import, persona
|
|
18
|
+
// promotion, and trajectory backfill from the v4 recent.jsonl.
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import { openIndex, rebuild, indexSessionTurn, indexSkill, indexMemory } from '../mas/index_db.mjs';
|
|
24
|
+
import { parseFrontmatter } from '../skills.mjs';
|
|
25
|
+
|
|
26
|
+
function defaultConfigDir() {
|
|
27
|
+
return process.env.LAZYCLAW_CONFIG_DIR || path.join(os.homedir(), '.lazyclaw');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tsStamp() {
|
|
31
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function backupOnce(configDir) {
|
|
35
|
+
const entries = fs.readdirSync(configDir, { withFileTypes: true });
|
|
36
|
+
const hasBackup = entries.some(e => e.isDirectory() && e.name.startsWith('backup-v4-'));
|
|
37
|
+
if (hasBackup) return { skipped: true };
|
|
38
|
+
const backupDir = path.join(configDir, `backup-v4-${tsStamp()}`);
|
|
39
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
if (e.name === 'index.db' || e.name.startsWith('backup-v4-')) continue;
|
|
42
|
+
const src = path.join(configDir, e.name);
|
|
43
|
+
const dst = path.join(backupDir, e.name);
|
|
44
|
+
fs.cpSync(src, dst, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
return { backupDir };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rewriteConfig(configDir) {
|
|
50
|
+
const cfgPath = path.join(configDir, 'config.json');
|
|
51
|
+
let cfg = {};
|
|
52
|
+
if (fs.existsSync(cfgPath)) {
|
|
53
|
+
try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch { cfg = {}; }
|
|
54
|
+
}
|
|
55
|
+
if (!cfg.trainer || !cfg.trainer.provider) {
|
|
56
|
+
cfg.trainer = { provider: 'auto', ...(cfg.trainer || {}) };
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
59
|
+
return cfg;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function escapeYaml(s) {
|
|
63
|
+
const str = String(s ?? '');
|
|
64
|
+
if (!/[":\n]/.test(str)) return str;
|
|
65
|
+
return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function upgradeSkillFrontmatter(filePath) {
|
|
69
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
70
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
71
|
+
const baseName = path.basename(filePath, '.md');
|
|
72
|
+
const want = {
|
|
73
|
+
group: meta.group || (baseName.includes('-') ? baseName.split('-')[0] : 'legacy'),
|
|
74
|
+
trained_by: meta.trained_by || 'legacy',
|
|
75
|
+
};
|
|
76
|
+
const before = JSON.stringify(meta);
|
|
77
|
+
const next = { ...meta, ...want };
|
|
78
|
+
if (JSON.stringify(next) === before) return false; // no change
|
|
79
|
+
const lines = ['---'];
|
|
80
|
+
for (const [k, v] of Object.entries(next)) {
|
|
81
|
+
lines.push(`${k}: ${escapeYaml(v)}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push('---', '', body.replace(/^\n+/, ''));
|
|
84
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function upgradeAllSkills(configDir) {
|
|
89
|
+
const dir = path.join(configDir, 'skills');
|
|
90
|
+
if (!fs.existsSync(dir)) return { upgraded: 0 };
|
|
91
|
+
let n = 0;
|
|
92
|
+
for (const name of fs.readdirSync(dir)) {
|
|
93
|
+
if (!name.endsWith('.md')) continue;
|
|
94
|
+
if (upgradeSkillFrontmatter(path.join(dir, name))) n++;
|
|
95
|
+
}
|
|
96
|
+
return { upgraded: n };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rebuildIndex(configDir) {
|
|
100
|
+
rebuild(configDir);
|
|
101
|
+
openIndex(configDir);
|
|
102
|
+
|
|
103
|
+
// Sessions.
|
|
104
|
+
const sessDir = path.join(configDir, 'sessions');
|
|
105
|
+
if (fs.existsSync(sessDir)) {
|
|
106
|
+
for (const f of fs.readdirSync(sessDir)) {
|
|
107
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
108
|
+
const id = f.slice(0, -'.jsonl'.length);
|
|
109
|
+
const raw = fs.readFileSync(path.join(sessDir, f), 'utf8');
|
|
110
|
+
let idx = 0;
|
|
111
|
+
for (const line of raw.split('\n')) {
|
|
112
|
+
if (!line) continue;
|
|
113
|
+
try {
|
|
114
|
+
const obj = JSON.parse(line);
|
|
115
|
+
indexSessionTurn({
|
|
116
|
+
session_id: id, turn_idx: idx++, role: obj.role || 'user',
|
|
117
|
+
ts: obj.ts || 0, content: obj.content || '',
|
|
118
|
+
}, configDir);
|
|
119
|
+
} catch { /* skip malformed */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Skills.
|
|
125
|
+
const skillsDir = path.join(configDir, 'skills');
|
|
126
|
+
if (fs.existsSync(skillsDir)) {
|
|
127
|
+
for (const f of fs.readdirSync(skillsDir)) {
|
|
128
|
+
if (!f.endsWith('.md')) continue;
|
|
129
|
+
const name = f.slice(0, -'.md'.length);
|
|
130
|
+
const raw = fs.readFileSync(path.join(skillsDir, f), 'utf8');
|
|
131
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
132
|
+
indexSkill({
|
|
133
|
+
skill_name: name,
|
|
134
|
+
trained_by: meta.trained_by || 'legacy',
|
|
135
|
+
group_name: meta.group || (name.includes('-') ? name.split('-')[0] : 'legacy'),
|
|
136
|
+
content: body,
|
|
137
|
+
}, configDir);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Memory (core + episodic).
|
|
142
|
+
const memDir = path.join(configDir, 'memory');
|
|
143
|
+
if (fs.existsSync(memDir)) {
|
|
144
|
+
const corePath = path.join(memDir, 'core.md');
|
|
145
|
+
if (fs.existsSync(corePath)) {
|
|
146
|
+
indexMemory({ topic: 'core', kind: 'core',
|
|
147
|
+
content: fs.readFileSync(corePath, 'utf8') }, configDir);
|
|
148
|
+
}
|
|
149
|
+
const epi = path.join(memDir, 'episodic');
|
|
150
|
+
if (fs.existsSync(epi)) {
|
|
151
|
+
for (const f of fs.readdirSync(epi)) {
|
|
152
|
+
if (!f.endsWith('.md')) continue;
|
|
153
|
+
indexMemory({
|
|
154
|
+
topic: f.slice(0, -'.md'.length), kind: 'episodic',
|
|
155
|
+
content: fs.readFileSync(path.join(epi, f), 'utf8'),
|
|
156
|
+
}, configDir);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function migrateV5(opts = {}) {
|
|
163
|
+
const configDir = opts.configDir || defaultConfigDir();
|
|
164
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
165
|
+
const backup = backupOnce(configDir);
|
|
166
|
+
const config = rewriteConfig(configDir);
|
|
167
|
+
const skills = upgradeAllSkills(configDir);
|
|
168
|
+
rebuildIndex(configDir);
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
configDir,
|
|
172
|
+
backupDir: backup.backupDir || null,
|
|
173
|
+
backupSkipped: !!backup.skipped,
|
|
174
|
+
trainerProvider: config.trainer?.provider,
|
|
175
|
+
skillsUpgraded: skills.upgraded,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Phase G: full v4→v5 migration (spec §1.7, §10) ---------------------
|
|
180
|
+
// Phase A's migrateV5() keeps its original behaviour (backup inside
|
|
181
|
+
// cfgDir, rebuild index). Phase G adds a parallel migrate()/rollback()
|
|
182
|
+
// pair the user-facing CLI calls. Differences from Phase A:
|
|
183
|
+
// * Backup goes to `<cfgDir>.v4.backup/<ISO-ts>/` (peer dir, not under
|
|
184
|
+
// cfgDir) so rollback can wipe + restore cleanly.
|
|
185
|
+
// * Config rewrites add `orchestrator → orchestra` (C3, §3.9) and
|
|
186
|
+
// `sandbox: "docker"` string → `sandbox: {backend: "docker"}` object
|
|
187
|
+
// (C8) on top of the trainer auto default (C9).
|
|
188
|
+
// * Skill frontmatter gains `confidence: 0.5` alongside the C4/C5
|
|
189
|
+
// fields Phase A already injects.
|
|
190
|
+
// * `personalities/`, `memory/`, `workspaces/` directories are
|
|
191
|
+
// created so the Phase-G CLI surfaces work right after migration.
|
|
192
|
+
|
|
193
|
+
function isoTs() {
|
|
194
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function copyTree(src, dst) {
|
|
198
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
199
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
200
|
+
const s = path.join(src, entry.name);
|
|
201
|
+
const d = path.join(dst, entry.name);
|
|
202
|
+
if (entry.isDirectory()) copyTree(s, d);
|
|
203
|
+
else fs.copyFileSync(s, d);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function removeTree(p) {
|
|
208
|
+
if (!fs.existsSync(p)) return;
|
|
209
|
+
const st = fs.lstatSync(p);
|
|
210
|
+
if (!st.isDirectory()) { fs.unlinkSync(p); return; }
|
|
211
|
+
for (const entry of fs.readdirSync(p, { withFileTypes: true })) {
|
|
212
|
+
const c = path.join(p, entry.name);
|
|
213
|
+
if (entry.isDirectory()) removeTree(c);
|
|
214
|
+
else fs.unlinkSync(c);
|
|
215
|
+
}
|
|
216
|
+
fs.rmdirSync(p);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function backupSnapshot(cfgDir) {
|
|
220
|
+
const root = `${cfgDir}.v4.backup`;
|
|
221
|
+
fs.mkdirSync(root, { recursive: true });
|
|
222
|
+
const dst = path.join(root, isoTs());
|
|
223
|
+
copyTree(cfgDir, dst);
|
|
224
|
+
return dst;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function rewriteConfigPhaseG(cfgDir) {
|
|
228
|
+
const p = path.join(cfgDir, 'config.json');
|
|
229
|
+
if (!fs.existsSync(p)) return;
|
|
230
|
+
let cfg;
|
|
231
|
+
try { cfg = JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
232
|
+
catch (e) { throw new Error(`migrate: config.json is not valid JSON: ${e.message}`); }
|
|
233
|
+
|
|
234
|
+
// orchestrator → orchestra (C3 + spec §3.9)
|
|
235
|
+
if (cfg.orchestrator && !cfg.orchestra) {
|
|
236
|
+
cfg.orchestra = cfg.orchestrator;
|
|
237
|
+
delete cfg.orchestrator;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// sandbox: "docker" → sandbox: { backend: "docker" } (C8)
|
|
241
|
+
if (typeof cfg.sandbox === 'string') {
|
|
242
|
+
cfg.sandbox = { backend: cfg.sandbox };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Default trainer (auto) when absent (C9)
|
|
246
|
+
if (!cfg.trainer) {
|
|
247
|
+
cfg.trainer = { provider: 'auto', schedule: 'nightly' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function upgradeSkillPhaseG(filePath) {
|
|
254
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
255
|
+
// Frontmatter detection mirrors skills.mjs::parseFrontmatter
|
|
256
|
+
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) return;
|
|
257
|
+
const after = raw.slice(4);
|
|
258
|
+
const closeRe = /\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
259
|
+
const m = closeRe.exec(after);
|
|
260
|
+
if (!m) return;
|
|
261
|
+
const block = after.slice(0, m.index);
|
|
262
|
+
const body = after.slice(m.index + m[0].length);
|
|
263
|
+
const keys = {};
|
|
264
|
+
for (const line of block.split(/\r?\n/)) {
|
|
265
|
+
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
266
|
+
if (kv) keys[kv[1]] = kv[2];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const fname = path.basename(filePath, '.md');
|
|
270
|
+
const hyphenPrefix = fname.includes('-') ? fname.split('-')[0] : null;
|
|
271
|
+
|
|
272
|
+
let mutated = false;
|
|
273
|
+
if (!keys.group) {
|
|
274
|
+
keys.group = hyphenPrefix || 'legacy'; // C5
|
|
275
|
+
mutated = true;
|
|
276
|
+
}
|
|
277
|
+
if (!keys.confidence) { keys.confidence = '0.5'; mutated = true; }
|
|
278
|
+
if (!keys.trained_by) { keys.trained_by = 'legacy'; mutated = true; } // C4
|
|
279
|
+
|
|
280
|
+
if (!mutated) return;
|
|
281
|
+
// Emit in a stable order: keep originals first, append new ones.
|
|
282
|
+
const orderedKeys = [];
|
|
283
|
+
for (const line of block.split(/\r?\n/)) {
|
|
284
|
+
const kv = /^([A-Za-z0-9_-]+)\s*:/.exec(line.trim());
|
|
285
|
+
if (kv && !orderedKeys.includes(kv[1])) orderedKeys.push(kv[1]);
|
|
286
|
+
}
|
|
287
|
+
for (const k of ['group', 'confidence', 'trained_by']) {
|
|
288
|
+
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
|
289
|
+
}
|
|
290
|
+
const newBlock = orderedKeys.map(k => `${k}: ${keys[k]}`).join('\n');
|
|
291
|
+
fs.writeFileSync(filePath, `---\n${newBlock}\n---\n${body}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function upgradeAllSkillsPhaseG(cfgDir) {
|
|
295
|
+
const dir = path.join(cfgDir, 'skills');
|
|
296
|
+
if (!fs.existsSync(dir)) return;
|
|
297
|
+
for (const f of fs.readdirSync(dir)) {
|
|
298
|
+
if (f.endsWith('.md')) upgradeSkillPhaseG(path.join(dir, f));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ensureDirs(cfgDir) {
|
|
303
|
+
fs.mkdirSync(path.join(cfgDir, 'personalities'), { recursive: true }); // C7
|
|
304
|
+
fs.mkdirSync(path.join(cfgDir, 'memory'), { recursive: true });
|
|
305
|
+
fs.mkdirSync(path.join(cfgDir, 'workspaces'), { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function migrate({ cfgDir } = {}) {
|
|
309
|
+
const dir = cfgDir || defaultConfigDir();
|
|
310
|
+
if (!fs.existsSync(dir)) throw new Error(`config dir not found: ${dir}`);
|
|
311
|
+
const backupDir = backupSnapshot(dir);
|
|
312
|
+
rewriteConfigPhaseG(dir);
|
|
313
|
+
upgradeAllSkillsPhaseG(dir);
|
|
314
|
+
ensureDirs(dir);
|
|
315
|
+
return { backupDir };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function rollback({ cfgDir } = {}) {
|
|
319
|
+
const dir = cfgDir || defaultConfigDir();
|
|
320
|
+
const root = `${dir}.v4.backup`;
|
|
321
|
+
if (!fs.existsSync(root)) throw new Error(`no backup found at ${root}`);
|
|
322
|
+
const stamps = fs.readdirSync(root).sort();
|
|
323
|
+
if (!stamps.length) throw new Error(`no backup snapshots in ${root}`);
|
|
324
|
+
const latest = path.join(root, stamps[stamps.length - 1]);
|
|
325
|
+
// Wipe the current cfgDir contents, restore the latest snapshot.
|
|
326
|
+
for (const entry of fs.readdirSync(dir)) removeTree(path.join(dir, entry));
|
|
327
|
+
copyTree(latest, dir);
|
|
328
|
+
return { restoredFrom: latest };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// CLI entry — `npm run migrate:v5` or `node scripts/migrate-v5.mjs`.
|
|
332
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
333
|
+
migrateV5().then(r => {
|
|
334
|
+
// eslint-disable-next-line no-console
|
|
335
|
+
console.log(JSON.stringify(r, null, 2));
|
|
336
|
+
process.exit(r.ok ? 0 : 1);
|
|
337
|
+
}).catch(err => {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.error('[migrate-v5] failed:', err.stack || err.message);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// scripts/openclaw-import.mjs
|
|
2
|
+
// Detect ~/.openclaw (or --from <dir>) and import into lazyclaw,
|
|
3
|
+
// matching Hermes `claw migrate` coverage. Tags every skill
|
|
4
|
+
// trained_by: openclaw-import (canonical C4).
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
export function defaultOpenclawDir() { return path.join(os.homedir(), '.openclaw'); }
|
|
10
|
+
export function defaultCfgDir() {
|
|
11
|
+
return process.env.LAZYCLAW_CONFIG_DIR || path.join(os.homedir(), '.lazyclaw');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function injectTrainedBy(content, value) {
|
|
15
|
+
if (!content.startsWith('---')) return `---\ntrained_by: ${value}\n---\n${content}`;
|
|
16
|
+
const closeRe = /\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
17
|
+
const m = closeRe.exec(content.slice(3));
|
|
18
|
+
if (!m) return content;
|
|
19
|
+
const block = content.slice(4, 3 + m.index);
|
|
20
|
+
const rest = content.slice(3 + m.index + m[0].length);
|
|
21
|
+
if (/^trained_by:/m.test(block)) {
|
|
22
|
+
return `---\n${block.replace(/^trained_by:.*$/m, `trained_by: ${value}`)}\n---\n${rest}`;
|
|
23
|
+
}
|
|
24
|
+
return `---\n${block}\ntrained_by: ${value}\n---\n${rest}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyIfPresent(srcFile, dstFile, transform = (s) => s) {
|
|
28
|
+
if (!fs.existsSync(srcFile)) return false;
|
|
29
|
+
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
|
30
|
+
fs.writeFileSync(dstFile, transform(fs.readFileSync(srcFile, 'utf8')));
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mergeJson(srcFile, cfgKey, cfgDir) {
|
|
35
|
+
if (!fs.existsSync(srcFile)) return;
|
|
36
|
+
let incoming; try { incoming = JSON.parse(fs.readFileSync(srcFile, 'utf8')); } catch { return; }
|
|
37
|
+
const cfgPath = path.join(cfgDir, 'config.json');
|
|
38
|
+
let cfg = {}; try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch {}
|
|
39
|
+
cfg[cfgKey] = { ...(cfg[cfgKey] || {}), ...incoming };
|
|
40
|
+
fs.mkdirSync(cfgDir, { recursive: true });
|
|
41
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function importOpenclaw({ from, cfgDir } = {}) {
|
|
45
|
+
const src = from || defaultOpenclawDir();
|
|
46
|
+
const dst = cfgDir || defaultCfgDir();
|
|
47
|
+
if (!fs.existsSync(src)) throw new Error(`openclaw source not found: ${src}`);
|
|
48
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
49
|
+
|
|
50
|
+
copyIfPresent(path.join(src, 'SOUL.md'), path.join(dst, 'SOUL.md'));
|
|
51
|
+
copyIfPresent(path.join(src, 'USER.md'), path.join(dst, 'memory', 'USER.md'));
|
|
52
|
+
copyIfPresent(path.join(src, 'MEMORY.md'), path.join(dst, 'memory', 'core.md'));
|
|
53
|
+
|
|
54
|
+
const skillsSrc = path.join(src, 'skills');
|
|
55
|
+
let nSkills = 0;
|
|
56
|
+
if (fs.existsSync(skillsSrc)) {
|
|
57
|
+
const skillsDst = path.join(dst, 'skills');
|
|
58
|
+
fs.mkdirSync(skillsDst, { recursive: true });
|
|
59
|
+
for (const f of fs.readdirSync(skillsSrc)) {
|
|
60
|
+
if (!f.endsWith('.md')) continue;
|
|
61
|
+
const content = fs.readFileSync(path.join(skillsSrc, f), 'utf8');
|
|
62
|
+
fs.writeFileSync(path.join(skillsDst, f), injectTrainedBy(content, 'openclaw-import'));
|
|
63
|
+
nSkills++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mergeJson(path.join(src, 'allowlist.json'), 'allowlist', dst);
|
|
68
|
+
mergeJson(path.join(src, 'messaging.json'), 'channels', dst);
|
|
69
|
+
|
|
70
|
+
return { src, dst, counts: { skills: nSkills } };
|
|
71
|
+
}
|
package/sessions.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import fs from 'node:fs';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import { appendRecent as _memoryAppendRecent } from './memory.mjs';
|
|
21
|
+
import { indexSessionTurn as _indexSessionTurn } from './mas/index_db.mjs';
|
|
21
22
|
|
|
22
23
|
const SESSIONS_DIRNAME = 'sessions';
|
|
23
24
|
|
|
@@ -73,12 +74,30 @@ export function appendTurn(id, role, content, configDir = defaultConfigDir()) {
|
|
|
73
74
|
}
|
|
74
75
|
const p = sessionPath(id, configDir);
|
|
75
76
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
76
|
-
const
|
|
77
|
+
const ts = Date.now();
|
|
78
|
+
const line = JSON.stringify({ role, content: String(content ?? ''), ts }) + '\n';
|
|
79
|
+
// Compute turn_idx BEFORE the append so the index row aligns with the
|
|
80
|
+
// JSONL row about to land on disk.
|
|
81
|
+
let turnIdx = 0;
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(p)) {
|
|
84
|
+
const existing = fs.readFileSync(p, 'utf8');
|
|
85
|
+
turnIdx = existing ? existing.split('\n').filter(Boolean).length : 0;
|
|
86
|
+
}
|
|
87
|
+
} catch { /* ignore — turn_idx defaults to 0 */ }
|
|
77
88
|
fs.appendFileSync(p, line);
|
|
78
89
|
// Write-through to the memory recency log. Best-effort; failures
|
|
79
90
|
// never propagate up — a missing or broken memory store must not
|
|
80
91
|
// break the session-write path.
|
|
81
92
|
_memoryAppendRecent(id, role, content, configDir);
|
|
93
|
+
// Phase A: FTS5 mirror (spec §4.4). Errors are swallowed inside
|
|
94
|
+
// indexSessionTurn but we wrap again here so a missing module (e.g.
|
|
95
|
+
// index_db not present in a tree shaped before Phase A) can't break
|
|
96
|
+
// this hot path either.
|
|
97
|
+
try {
|
|
98
|
+
_indexSessionTurn({ session_id: id, turn_idx: turnIdx, role, ts,
|
|
99
|
+
content: String(content ?? '') }, configDir);
|
|
100
|
+
} catch { /* swallow */ }
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
export function clearSession(id, configDir = defaultConfigDir()) {
|