lazyclaw 4.2.2 → 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 -353
- package/agents.mjs +19 -3
- package/channels/handoff.mjs +41 -0
- package/channels/loader.mjs +124 -0
- package/channels/matrix.mjs +417 -0
- package/channels/telegram.mjs +362 -0
- package/channels/threads.mjs +116 -0
- package/cli.mjs +730 -27
- package/daemon.mjs +111 -0
- package/gateway/device_auth.mjs +664 -0
- package/gateway/http_gateway.mjs +304 -0
- package/mas/agent_memory.mjs +35 -34
- package/mas/agent_turn.mjs +30 -1
- package/mas/confidence.mjs +108 -0
- package/mas/index_db.mjs +242 -0
- package/mas/mention_router.mjs +75 -4
- package/mas/nudge.mjs +97 -0
- package/mas/prompt_stack.mjs +80 -0
- package/mas/provider_adapters.mjs +83 -0
- package/mas/redact.mjs +46 -0
- package/mas/skill_synth.mjs +331 -0
- package/mas/tool_runner.mjs +19 -48
- 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/skill_view.mjs +43 -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 +22 -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
- package/skills.mjs +101 -8
- package/skills_curator.mjs +323 -0
- package/workspace.mjs +18 -3
|
@@ -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()) {
|
package/skills.mjs
CHANGED
|
@@ -38,24 +38,79 @@ export function listSkills(configDir = defaultConfigDir()) {
|
|
|
38
38
|
.map(name => {
|
|
39
39
|
const full = path.join(dir, name);
|
|
40
40
|
const stat = fs.statSync(full);
|
|
41
|
-
|
|
41
|
+
let content = '';
|
|
42
|
+
try { content = fs.readFileSync(full, 'utf8'); } catch { /* unreadable → empty */ }
|
|
43
|
+
const { meta, body } = parseFrontmatter(content);
|
|
44
|
+
// Prefer the agent-/author-supplied frontmatter description; fall
|
|
45
|
+
// back to the first markdown heading for legacy frontmatter-less
|
|
46
|
+
// skills so the index still reads sensibly.
|
|
47
|
+
const summary = (meta.description || firstHeading(body) || '').slice(0, 120);
|
|
42
48
|
return {
|
|
43
49
|
name: name.slice(0, -SKILL_EXT.length),
|
|
44
50
|
path: full,
|
|
45
51
|
bytes: stat.size,
|
|
46
52
|
mtimeMs: stat.mtimeMs,
|
|
47
|
-
summary
|
|
53
|
+
summary,
|
|
54
|
+
description: meta.description || '',
|
|
55
|
+
createdBy: meta.created_by || '',
|
|
56
|
+
version: meta.version || '',
|
|
48
57
|
};
|
|
49
58
|
})
|
|
50
59
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
// Parse a leading YAML frontmatter block (--- … ---). Only the flat
|
|
63
|
+
// `key: value` shape skills use is supported — no nested YAML — which
|
|
64
|
+
// keeps us dependency-free. Returns { meta, body }; when no frontmatter
|
|
65
|
+
// is present meta is {} and body is the untouched content.
|
|
66
|
+
export function parseFrontmatter(content) {
|
|
67
|
+
const text = String(content ?? '');
|
|
68
|
+
if (!text.startsWith('---')) return { meta: {}, body: text };
|
|
69
|
+
// The opening fence must be its own line.
|
|
70
|
+
const afterOpen = text.slice(3);
|
|
71
|
+
if (!/^\r?\n/.test(afterOpen)) return { meta: {}, body: text };
|
|
72
|
+
const closeRe = /\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
73
|
+
const m = closeRe.exec(afterOpen);
|
|
74
|
+
if (!m) return { meta: {}, body: text };
|
|
75
|
+
const block = afterOpen.slice(0, m.index);
|
|
76
|
+
// Drop blank lines between the closing fence and the first body line
|
|
77
|
+
// so callers can rely on body starting at real content.
|
|
78
|
+
const body = afterOpen.slice(m.index + m[0].length).replace(/^(?:\r?\n)+/, '');
|
|
79
|
+
const meta = {};
|
|
80
|
+
for (const line of block.split(/\r?\n/)) {
|
|
81
|
+
const mm = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
82
|
+
if (!mm) continue;
|
|
83
|
+
let val = mm[2].trim();
|
|
84
|
+
if (val.length >= 2 && val.startsWith('"') && val.endsWith('"')) {
|
|
85
|
+
// Symmetric with skill_synth's escapeYaml double-quote escaping.
|
|
86
|
+
val = val.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
87
|
+
} else if (val.length >= 2 && val.startsWith("'") && val.endsWith("'")) {
|
|
88
|
+
val = val.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
meta[mm[1]] = val;
|
|
91
|
+
}
|
|
92
|
+
return { meta, body };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function firstHeading(body) {
|
|
96
|
+
for (const line of String(body || '').split(/\r?\n/)) {
|
|
97
|
+
const t = line.trim();
|
|
98
|
+
if (!t) continue;
|
|
99
|
+
return t.replace(/^#+\s*/, '');
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compact "Level 0" recall index: one `- <name>: <summary>` line per
|
|
105
|
+
// installed skill, sorted by name. Returns '' when no skills exist so
|
|
106
|
+
// callers can inject it conditionally. This is what gets dropped into
|
|
107
|
+
// the system prompt so the model knows which skills exist without
|
|
108
|
+
// paying for their full bodies (progressive disclosure — the model
|
|
109
|
+
// pulls a full skill on demand via the skill_view tool).
|
|
110
|
+
export function skillsIndex(configDir = defaultConfigDir()) {
|
|
111
|
+
const skills = listSkills(configDir);
|
|
112
|
+
if (!skills.length) return '';
|
|
113
|
+
return skills.map((s) => `- ${s.name}: ${s.summary}`.trimEnd()).join('\n');
|
|
59
114
|
}
|
|
60
115
|
|
|
61
116
|
export function loadSkill(name, configDir = defaultConfigDir()) {
|
|
@@ -76,6 +131,44 @@ export function removeSkill(name, configDir = defaultConfigDir()) {
|
|
|
76
131
|
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
77
132
|
}
|
|
78
133
|
|
|
134
|
+
export function skillExists(name, configDir = defaultConfigDir()) {
|
|
135
|
+
try { return fs.existsSync(skillPath(name, configDir)); }
|
|
136
|
+
catch { return false; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Read the integer `version` from a skill's frontmatter (0 when the
|
|
140
|
+
// skill is missing or carries no version).
|
|
141
|
+
export function skillVersion(name, configDir = defaultConfigDir()) {
|
|
142
|
+
try {
|
|
143
|
+
const { meta } = parseFrontmatter(fs.readFileSync(skillPath(name, configDir), 'utf8'));
|
|
144
|
+
return parseInt(meta.version, 10) || 0;
|
|
145
|
+
} catch { return 0; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reserve a target name for an agent-synthesised skill that NEVER
|
|
149
|
+
// clobbers a human-authored skill. If the slug is free, use it. If a
|
|
150
|
+
// skill with that slug already exists AND it was itself agent-authored
|
|
151
|
+
// (created_by: agent), reuse it — that's the self-improvement update
|
|
152
|
+
// path. Otherwise the slug belongs to a human/curated skill, so we
|
|
153
|
+
// append a numeric suffix and try again. This is the security boundary
|
|
154
|
+
// that stops LLM-chosen slugs from overwriting trusted skills.
|
|
155
|
+
export function reserveSynthName(name, configDir = defaultConfigDir()) {
|
|
156
|
+
const base = (name && String(name).trim()) || 'skill';
|
|
157
|
+
let candidate = base;
|
|
158
|
+
for (let i = 1; i < 1000; i++) {
|
|
159
|
+
let p;
|
|
160
|
+
try { p = skillPath(candidate, configDir); }
|
|
161
|
+
catch { return base; }
|
|
162
|
+
if (!fs.existsSync(p)) return candidate;
|
|
163
|
+
let createdBy = '';
|
|
164
|
+
try { createdBy = parseFrontmatter(fs.readFileSync(p, 'utf8')).meta.created_by || ''; }
|
|
165
|
+
catch { /* unreadable → treat as occupied */ }
|
|
166
|
+
if (createdBy === 'agent') return candidate; // overwrite our own = improve
|
|
167
|
+
candidate = `${base}-${i}`;
|
|
168
|
+
}
|
|
169
|
+
return candidate;
|
|
170
|
+
}
|
|
171
|
+
|
|
79
172
|
/**
|
|
80
173
|
* Compose the system prompt for a chat/agent invocation. Concatenates each
|
|
81
174
|
* named skill's contents with a separator, in the order given. Returns null
|