@savestate/cli 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/dist/adapters/claude-code.d.ts +81 -0
  4. package/dist/adapters/claude-code.d.ts.map +1 -0
  5. package/dist/adapters/claude-code.js +540 -0
  6. package/dist/adapters/claude-code.js.map +1 -0
  7. package/dist/adapters/clawdbot.d.ts +93 -0
  8. package/dist/adapters/clawdbot.d.ts.map +1 -0
  9. package/dist/adapters/clawdbot.js +673 -0
  10. package/dist/adapters/clawdbot.js.map +1 -0
  11. package/dist/adapters/index.d.ts +9 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +8 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/interface.d.ts +8 -0
  16. package/dist/adapters/interface.d.ts.map +1 -0
  17. package/dist/adapters/interface.js +8 -0
  18. package/dist/adapters/interface.js.map +1 -0
  19. package/dist/adapters/openai-assistants.d.ts +51 -0
  20. package/dist/adapters/openai-assistants.d.ts.map +1 -0
  21. package/dist/adapters/openai-assistants.js +114 -0
  22. package/dist/adapters/openai-assistants.js.map +1 -0
  23. package/dist/adapters/registry.d.ts +38 -0
  24. package/dist/adapters/registry.d.ts.map +1 -0
  25. package/dist/adapters/registry.js +79 -0
  26. package/dist/adapters/registry.js.map +1 -0
  27. package/dist/cli.d.ts +18 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +81 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/commands/adapters.d.ts +5 -0
  32. package/dist/commands/adapters.d.ts.map +1 -0
  33. package/dist/commands/adapters.js +49 -0
  34. package/dist/commands/adapters.js.map +1 -0
  35. package/dist/commands/config.d.ts +10 -0
  36. package/dist/commands/config.d.ts.map +1 -0
  37. package/dist/commands/config.js +60 -0
  38. package/dist/commands/config.js.map +1 -0
  39. package/dist/commands/diff.d.ts +5 -0
  40. package/dist/commands/diff.d.ts.map +1 -0
  41. package/dist/commands/diff.js +51 -0
  42. package/dist/commands/diff.js.map +1 -0
  43. package/dist/commands/index.d.ts +12 -0
  44. package/dist/commands/index.d.ts.map +1 -0
  45. package/dist/commands/index.js +12 -0
  46. package/dist/commands/index.js.map +1 -0
  47. package/dist/commands/init.d.ts +5 -0
  48. package/dist/commands/init.d.ts.map +1 -0
  49. package/dist/commands/init.js +71 -0
  50. package/dist/commands/init.js.map +1 -0
  51. package/dist/commands/list.d.ts +10 -0
  52. package/dist/commands/list.d.ts.map +1 -0
  53. package/dist/commands/list.js +89 -0
  54. package/dist/commands/list.js.map +1 -0
  55. package/dist/commands/restore.d.ts +11 -0
  56. package/dist/commands/restore.d.ts.map +1 -0
  57. package/dist/commands/restore.js +86 -0
  58. package/dist/commands/restore.js.map +1 -0
  59. package/dist/commands/search.d.ts +11 -0
  60. package/dist/commands/search.d.ts.map +1 -0
  61. package/dist/commands/search.js +50 -0
  62. package/dist/commands/search.js.map +1 -0
  63. package/dist/commands/snapshot.d.ts +12 -0
  64. package/dist/commands/snapshot.d.ts.map +1 -0
  65. package/dist/commands/snapshot.js +84 -0
  66. package/dist/commands/snapshot.js.map +1 -0
  67. package/dist/config.d.ts +44 -0
  68. package/dist/config.d.ts.map +1 -0
  69. package/dist/config.js +83 -0
  70. package/dist/config.js.map +1 -0
  71. package/dist/encryption.d.ts +40 -0
  72. package/dist/encryption.d.ts.map +1 -0
  73. package/dist/encryption.js +120 -0
  74. package/dist/encryption.js.map +1 -0
  75. package/dist/format.d.ts +47 -0
  76. package/dist/format.d.ts.map +1 -0
  77. package/dist/format.js +198 -0
  78. package/dist/format.js.map +1 -0
  79. package/dist/index-file.d.ts +42 -0
  80. package/dist/index-file.d.ts.map +1 -0
  81. package/dist/index-file.js +68 -0
  82. package/dist/index-file.js.map +1 -0
  83. package/dist/index.d.ts +15 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +22 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/passphrase.d.ts +23 -0
  88. package/dist/passphrase.d.ts.map +1 -0
  89. package/dist/passphrase.js +82 -0
  90. package/dist/passphrase.js.map +1 -0
  91. package/dist/restore.d.ts +46 -0
  92. package/dist/restore.d.ts.map +1 -0
  93. package/dist/restore.js +113 -0
  94. package/dist/restore.js.map +1 -0
  95. package/dist/search.d.ts +35 -0
  96. package/dist/search.d.ts.map +1 -0
  97. package/dist/search.js +59 -0
  98. package/dist/search.js.map +1 -0
  99. package/dist/snapshot.d.ts +43 -0
  100. package/dist/snapshot.d.ts.map +1 -0
  101. package/dist/snapshot.js +95 -0
  102. package/dist/snapshot.js.map +1 -0
  103. package/dist/storage/index.d.ts +7 -0
  104. package/dist/storage/index.d.ts.map +1 -0
  105. package/dist/storage/index.js +6 -0
  106. package/dist/storage/index.js.map +1 -0
  107. package/dist/storage/interface.d.ts +8 -0
  108. package/dist/storage/interface.d.ts.map +1 -0
  109. package/dist/storage/interface.js +8 -0
  110. package/dist/storage/interface.js.map +1 -0
  111. package/dist/storage/local.d.ts +22 -0
  112. package/dist/storage/local.d.ts.map +1 -0
  113. package/dist/storage/local.js +63 -0
  114. package/dist/storage/local.js.map +1 -0
  115. package/dist/storage/resolve.d.ts +11 -0
  116. package/dist/storage/resolve.d.ts.map +1 -0
  117. package/dist/storage/resolve.js +21 -0
  118. package/dist/storage/resolve.js.map +1 -0
  119. package/dist/types.d.ts +273 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +8 -0
  122. package/dist/types.js.map +1 -0
  123. package/package.json +61 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Clawdbot Adapter
3
+ *
4
+ * First-party adapter for Clawdbot / Moltbot workspaces.
5
+ * Reads SOUL.md, MEMORY.md, memory/, USER.md, TOOLS.md,
6
+ * skills/, personal-scripts/, extensions/, conversation logs,
7
+ * cron wrappers, and config files.
8
+ *
9
+ * This is the dogfood adapter — SaveState eats its own cooking.
10
+ */
11
+ import { readFile, writeFile, readdir, stat, rename, mkdir } from 'node:fs/promises';
12
+ import { existsSync } from 'node:fs';
13
+ import { join, dirname, extname, relative } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { SAF_VERSION, generateSnapshotId, computeChecksum } from '../format.js';
16
+ /** Files that constitute the agent's identity */
17
+ const IDENTITY_FILES = ['SOUL.md', 'USER.md', 'AGENTS.md', 'TOOLS.md'];
18
+ /** Directories containing memory data */
19
+ const MEMORY_DIRS = ['memory'];
20
+ /** Files containing memory data */
21
+ const MEMORY_FILES = ['memory.md', 'MEMORY.md'];
22
+ /** Config files to capture at workspace root */
23
+ const CONFIG_FILES = ['.env', 'config.json', 'config.yaml', 'config.yml', '.savestate/config.json'];
24
+ /** File extensions to skip as binary */
25
+ const BINARY_EXTENSIONS = new Set([
26
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg', '.bmp', '.tiff',
27
+ '.mp3', '.mp4', '.wav', '.ogg', '.flac', '.m4a', '.aac', '.webm', '.avi', '.mov', '.mkv',
28
+ '.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar',
29
+ '.exe', '.dll', '.so', '.dylib', '.o', '.a',
30
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
31
+ '.woff', '.woff2', '.ttf', '.otf', '.eot',
32
+ '.db', '.sqlite', '.sqlite3',
33
+ '.DS_Store',
34
+ ]);
35
+ /** Maximum file size to capture (1MB) */
36
+ const MAX_FILE_SIZE = 1024 * 1024;
37
+ /** Separator used in concatenated personality */
38
+ const FILE_SEPARATOR_PREFIX = '--- ';
39
+ const FILE_SEPARATOR_SUFFIX = ' ---';
40
+ /** Directories to skip when scanning */
41
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv']);
42
+ export class ClawdbotAdapter {
43
+ id = 'clawdbot';
44
+ name = 'Clawdbot';
45
+ platform = 'clawdbot';
46
+ version = '0.2.0';
47
+ workspaceDir;
48
+ warnings = [];
49
+ constructor(workspaceDir) {
50
+ this.workspaceDir = workspaceDir ?? process.cwd();
51
+ }
52
+ async detect() {
53
+ // Detect by looking for characteristic files
54
+ const markers = ['SOUL.md', 'memory.md', 'AGENTS.md', 'memory/'];
55
+ for (const marker of markers) {
56
+ if (existsSync(join(this.workspaceDir, marker))) {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+ async extract() {
63
+ this.warnings = [];
64
+ const personality = await this.readIdentity();
65
+ const memoryEntries = await this.readMemory();
66
+ const conversations = await this.readConversations();
67
+ const skills = await this.readSkills();
68
+ const scripts = await this.readScripts();
69
+ const extensions = await this.readExtensions();
70
+ const configEntries = await this.readConfigFiles();
71
+ const knowledge = await this.buildKnowledgeIndex(skills, scripts);
72
+ const snapshotId = generateSnapshotId();
73
+ const now = new Date().toISOString();
74
+ // Log warnings
75
+ if (this.warnings.length > 0) {
76
+ for (const w of this.warnings) {
77
+ console.warn(` ⚠ ${w}`);
78
+ }
79
+ }
80
+ const snapshot = {
81
+ manifest: {
82
+ version: SAF_VERSION,
83
+ timestamp: now,
84
+ id: snapshotId,
85
+ platform: this.platform,
86
+ adapter: this.id,
87
+ checksum: '', // Computed during packing
88
+ size: 0, // Computed during packing
89
+ },
90
+ identity: {
91
+ personality,
92
+ config: configEntries,
93
+ tools: [],
94
+ skills,
95
+ scripts,
96
+ extensions,
97
+ },
98
+ memory: {
99
+ core: memoryEntries,
100
+ knowledge,
101
+ },
102
+ conversations: {
103
+ total: conversations.length,
104
+ conversations,
105
+ },
106
+ platform: await this.identify(),
107
+ chain: {
108
+ current: snapshotId,
109
+ ancestors: [],
110
+ },
111
+ restoreHints: {
112
+ platform: this.platform,
113
+ steps: [
114
+ {
115
+ type: 'file',
116
+ description: 'Restore SOUL.md and identity files',
117
+ target: 'identity/',
118
+ },
119
+ {
120
+ type: 'file',
121
+ description: 'Restore memory files',
122
+ target: 'memory/',
123
+ },
124
+ {
125
+ type: 'file',
126
+ description: 'Restore skills directory',
127
+ target: 'skills/',
128
+ },
129
+ {
130
+ type: 'file',
131
+ description: 'Restore personal scripts',
132
+ target: 'personal-scripts/',
133
+ },
134
+ {
135
+ type: 'file',
136
+ description: 'Restore extension configs',
137
+ target: 'extensions/',
138
+ },
139
+ ],
140
+ },
141
+ };
142
+ return snapshot;
143
+ }
144
+ async restore(snapshot) {
145
+ // Restore identity files from concatenated personality
146
+ if (snapshot.identity.personality) {
147
+ await this.restoreIdentity(snapshot.identity.personality);
148
+ }
149
+ // Restore memory files
150
+ await this.restoreMemory(snapshot.memory.core);
151
+ // Restore skills
152
+ if (snapshot.identity.skills?.length) {
153
+ await this.restoreSkills(snapshot.identity.skills);
154
+ }
155
+ // Restore scripts
156
+ if (snapshot.identity.scripts?.length) {
157
+ await this.restoreScripts(snapshot.identity.scripts);
158
+ }
159
+ // Restore extensions
160
+ if (snapshot.identity.extensions?.length) {
161
+ await this.restoreExtensions(snapshot.identity.extensions);
162
+ }
163
+ // Restore config files
164
+ if (snapshot.identity.config) {
165
+ await this.restoreConfigFiles(snapshot.identity.config);
166
+ }
167
+ }
168
+ async identify() {
169
+ // Try to read version from package.json in the workspace
170
+ let version = this.version;
171
+ try {
172
+ const pkg = await readFile(join(this.workspaceDir, 'package.json'), 'utf-8');
173
+ const parsed = JSON.parse(pkg);
174
+ if (typeof parsed.version === 'string') {
175
+ version = parsed.version;
176
+ }
177
+ }
178
+ catch { /* ignore */ }
179
+ return {
180
+ name: 'Clawdbot',
181
+ version,
182
+ exportMethod: 'direct-file-access',
183
+ };
184
+ }
185
+ // ─── Private helpers ─────────────────────────────────────
186
+ /**
187
+ * Check if a file should be skipped (binary or too large).
188
+ */
189
+ isBinary(filePath) {
190
+ const ext = extname(filePath).toLowerCase();
191
+ const base = filePath.split('/').pop() ?? '';
192
+ return BINARY_EXTENSIONS.has(ext) || BINARY_EXTENSIONS.has(`.${base}`);
193
+ }
194
+ async checkFileSize(filePath) {
195
+ try {
196
+ const s = await stat(filePath);
197
+ if (s.size > MAX_FILE_SIZE) {
198
+ this.warnings.push(`Skipped ${filePath} (${(s.size / 1024 / 1024).toFixed(1)}MB > 1MB limit)`);
199
+ return false;
200
+ }
201
+ return true;
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ async safeReadFile(filePath) {
208
+ if (this.isBinary(filePath)) {
209
+ return null;
210
+ }
211
+ if (!(await this.checkFileSize(filePath))) {
212
+ return null;
213
+ }
214
+ try {
215
+ return await readFile(filePath, 'utf-8');
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ async readIdentity() {
222
+ const parts = [];
223
+ for (const file of IDENTITY_FILES) {
224
+ const path = join(this.workspaceDir, file);
225
+ if (existsSync(path)) {
226
+ const content = await this.safeReadFile(path);
227
+ if (content !== null) {
228
+ parts.push(`--- ${file} ---\n${content}`);
229
+ }
230
+ }
231
+ }
232
+ return parts.join('\n\n');
233
+ }
234
+ async readMemory() {
235
+ const entries = [];
236
+ // Read standalone memory files
237
+ for (const file of MEMORY_FILES) {
238
+ const path = join(this.workspaceDir, file);
239
+ if (existsSync(path)) {
240
+ const content = await this.safeReadFile(path);
241
+ if (content !== null) {
242
+ const fileStat = await stat(path);
243
+ entries.push({
244
+ id: `file:${file}`,
245
+ content,
246
+ source: file,
247
+ createdAt: fileStat.birthtime.toISOString(),
248
+ updatedAt: fileStat.mtime.toISOString(),
249
+ });
250
+ }
251
+ }
252
+ }
253
+ // Read memory directory (recursively, flat output)
254
+ for (const dir of MEMORY_DIRS) {
255
+ const dirPath = join(this.workspaceDir, dir);
256
+ if (existsSync(dirPath)) {
257
+ const memFiles = await this.walkDir(dirPath, ['.md', '.json', '.txt']);
258
+ for (const filePath of memFiles) {
259
+ const content = await this.safeReadFile(filePath);
260
+ if (content !== null) {
261
+ const relPath = `${dir}/${relative(dirPath, filePath)}`;
262
+ const fileStat = await stat(filePath);
263
+ entries.push({
264
+ id: `file:${relPath}`,
265
+ content,
266
+ source: relPath,
267
+ createdAt: fileStat.birthtime.toISOString(),
268
+ updatedAt: fileStat.mtime.toISOString(),
269
+ });
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return entries;
275
+ }
276
+ async readConversations() {
277
+ const conversations = [];
278
+ // Look for conversation logs in ~/.clawdbot/agents/*/sessions/*.jsonl
279
+ const agentsDir = join(homedir(), '.clawdbot', 'agents');
280
+ if (!existsSync(agentsDir))
281
+ return conversations;
282
+ try {
283
+ const agents = await readdir(agentsDir);
284
+ for (const agent of agents) {
285
+ const sessionsDir = join(agentsDir, agent, 'sessions');
286
+ if (!existsSync(sessionsDir))
287
+ continue;
288
+ const sessionFiles = await readdir(sessionsDir);
289
+ for (const sessionFile of sessionFiles) {
290
+ if (!sessionFile.endsWith('.jsonl'))
291
+ continue;
292
+ const sessionPath = join(sessionsDir, sessionFile);
293
+ const sessionId = sessionFile.replace('.jsonl', '');
294
+ try {
295
+ const fileStat = await stat(sessionPath);
296
+ // Count lines (messages) without loading full content
297
+ let messageCount = 0;
298
+ if (fileStat.size <= MAX_FILE_SIZE) {
299
+ const content = await readFile(sessionPath, 'utf-8');
300
+ messageCount = content.split('\n').filter(l => l.trim()).length;
301
+ }
302
+ else {
303
+ this.warnings.push(`Conversation ${sessionId} too large for content (${(fileStat.size / 1024 / 1024).toFixed(1)}MB), capturing metadata only`);
304
+ }
305
+ conversations.push({
306
+ id: `${agent}/${sessionId}`,
307
+ title: `${agent} session ${sessionId.slice(0, 8)}`,
308
+ createdAt: fileStat.birthtime.toISOString(),
309
+ updatedAt: fileStat.mtime.toISOString(),
310
+ messageCount,
311
+ path: `conversations/${agent}/${sessionFile}`,
312
+ });
313
+ }
314
+ catch {
315
+ // Skip unreadable session files
316
+ }
317
+ }
318
+ }
319
+ }
320
+ catch {
321
+ // Agents directory not readable
322
+ }
323
+ return conversations;
324
+ }
325
+ /**
326
+ * Read skills/ directory — capture SKILL.md and scripts/ for each skill.
327
+ */
328
+ async readSkills() {
329
+ const skills = [];
330
+ const skillsDir = join(this.workspaceDir, 'skills');
331
+ if (!existsSync(skillsDir))
332
+ return skills;
333
+ try {
334
+ const skillDirs = await readdir(skillsDir);
335
+ for (const skillName of skillDirs) {
336
+ const skillPath = join(skillsDir, skillName);
337
+ const s = await stat(skillPath).catch(() => null);
338
+ if (!s?.isDirectory())
339
+ continue;
340
+ const entry = { name: skillName, files: {} };
341
+ // Read SKILL.md
342
+ const skillMdPath = join(skillPath, 'SKILL.md');
343
+ if (existsSync(skillMdPath)) {
344
+ const content = await this.safeReadFile(skillMdPath);
345
+ if (content !== null) {
346
+ entry.skillMd = content;
347
+ entry.files['SKILL.md'] = content;
348
+ }
349
+ }
350
+ // Read config files in skill root
351
+ const skillFiles = await readdir(skillPath).catch(() => []);
352
+ for (const f of skillFiles) {
353
+ if (SKIP_DIRS.has(f))
354
+ continue;
355
+ const fPath = join(skillPath, f);
356
+ const fStat = await stat(fPath).catch(() => null);
357
+ if (!fStat?.isFile())
358
+ continue;
359
+ if (this.isBinary(fPath))
360
+ continue;
361
+ // Capture config-like files: .json, .yaml, .yml, .toml, .md, .txt, .sh, .py, .ts, .js
362
+ const ext = extname(f).toLowerCase();
363
+ const captureExts = new Set(['.json', '.yaml', '.yml', '.toml', '.md', '.txt', '.sh', '.py', '.ts', '.js', '.env']);
364
+ if (!captureExts.has(ext))
365
+ continue;
366
+ const content = await this.safeReadFile(fPath);
367
+ if (content !== null) {
368
+ entry.files[f] = content;
369
+ }
370
+ }
371
+ // Read scripts/ subdirectory
372
+ const scriptsDir = join(skillPath, 'scripts');
373
+ if (existsSync(scriptsDir)) {
374
+ const scriptFiles = await this.walkDir(scriptsDir, ['.sh', '.py', '.ts', '.js', '.rb']);
375
+ for (const sf of scriptFiles) {
376
+ const content = await this.safeReadFile(sf);
377
+ if (content !== null) {
378
+ const relPath = `scripts/${relative(scriptsDir, sf)}`;
379
+ entry.files[relPath] = content;
380
+ }
381
+ }
382
+ }
383
+ if (Object.keys(entry.files).length > 0 || entry.skillMd) {
384
+ skills.push(entry);
385
+ }
386
+ }
387
+ }
388
+ catch {
389
+ // skills directory not readable
390
+ }
391
+ return skills;
392
+ }
393
+ /**
394
+ * Read personal-scripts/ and personal-scripts/cron-wrappers/
395
+ */
396
+ async readScripts() {
397
+ const scripts = [];
398
+ const scriptsDir = join(this.workspaceDir, 'personal-scripts');
399
+ if (!existsSync(scriptsDir))
400
+ return scripts;
401
+ try {
402
+ const allFiles = await this.walkDir(scriptsDir);
403
+ for (const filePath of allFiles) {
404
+ const content = await this.safeReadFile(filePath);
405
+ if (content !== null) {
406
+ const relPath = `personal-scripts/${relative(scriptsDir, filePath)}`;
407
+ const isCronWrapper = filePath.includes('cron-wrappers');
408
+ scripts.push({ path: relPath, content, isCronWrapper });
409
+ }
410
+ }
411
+ }
412
+ catch {
413
+ // personal-scripts not readable
414
+ }
415
+ return scripts;
416
+ }
417
+ /**
418
+ * Read extensions/ directory configs (skip node_modules and binary files).
419
+ */
420
+ async readExtensions() {
421
+ const extensions = [];
422
+ const extDir = join(this.workspaceDir, 'extensions');
423
+ if (!existsSync(extDir))
424
+ return extensions;
425
+ try {
426
+ const extDirs = await readdir(extDir);
427
+ for (const extName of extDirs) {
428
+ const extPath = join(extDir, extName);
429
+ const s = await stat(extPath).catch(() => null);
430
+ if (!s?.isDirectory())
431
+ continue;
432
+ const entry = { name: extName, configs: {} };
433
+ // Only capture config-like files (not source code or node_modules)
434
+ const configExts = new Set(['.json', '.yaml', '.yml', '.toml', '.md', '.env', '.env.example']);
435
+ const files = await readdir(extPath).catch(() => []);
436
+ for (const f of files) {
437
+ if (SKIP_DIRS.has(f))
438
+ continue;
439
+ const fPath = join(extPath, f);
440
+ const fStat = await stat(fPath).catch(() => null);
441
+ if (!fStat?.isFile())
442
+ continue;
443
+ const ext = extname(f).toLowerCase();
444
+ if (configExts.has(ext) || f === 'package.json' || f === 'README.md' || f === 'SKILL.md') {
445
+ const content = await this.safeReadFile(fPath);
446
+ if (content !== null) {
447
+ entry.configs[f] = content;
448
+ }
449
+ }
450
+ }
451
+ if (Object.keys(entry.configs).length > 0) {
452
+ extensions.push(entry);
453
+ }
454
+ }
455
+ }
456
+ catch {
457
+ // extensions directory not readable
458
+ }
459
+ return extensions;
460
+ }
461
+ /**
462
+ * Read config files from workspace root.
463
+ */
464
+ async readConfigFiles() {
465
+ const configs = {};
466
+ for (const file of CONFIG_FILES) {
467
+ const filePath = join(this.workspaceDir, file);
468
+ if (existsSync(filePath)) {
469
+ const content = await this.safeReadFile(filePath);
470
+ if (content !== null) {
471
+ configs[file] = content;
472
+ }
473
+ }
474
+ }
475
+ // Also check .savestate/agent-config.json
476
+ const agentConfigPath = join(this.workspaceDir, '.savestate', 'agent-config.json');
477
+ if (existsSync(agentConfigPath)) {
478
+ const content = await this.safeReadFile(agentConfigPath);
479
+ if (content !== null) {
480
+ configs['.savestate/agent-config.json'] = content;
481
+ }
482
+ }
483
+ return Object.keys(configs).length > 0 ? configs : undefined;
484
+ }
485
+ /**
486
+ * Build knowledge documents index from skills and scripts.
487
+ */
488
+ async buildKnowledgeIndex(skills, scripts) {
489
+ const docs = [];
490
+ for (const skill of skills) {
491
+ if (skill.skillMd) {
492
+ const buf = Buffer.from(skill.skillMd, 'utf-8');
493
+ docs.push({
494
+ id: `skill:${skill.name}`,
495
+ filename: `skills/${skill.name}/SKILL.md`,
496
+ mimeType: 'text/markdown',
497
+ path: `knowledge/skills/${skill.name}/SKILL.md`,
498
+ size: buf.length,
499
+ checksum: computeChecksum(buf),
500
+ });
501
+ }
502
+ }
503
+ for (const script of scripts) {
504
+ const buf = Buffer.from(script.content, 'utf-8');
505
+ docs.push({
506
+ id: `script:${script.path}`,
507
+ filename: script.path,
508
+ mimeType: 'text/plain',
509
+ path: `knowledge/${script.path}`,
510
+ size: buf.length,
511
+ checksum: computeChecksum(buf),
512
+ });
513
+ }
514
+ return docs;
515
+ }
516
+ async readJsonSafe(path) {
517
+ try {
518
+ const content = await readFile(path, 'utf-8');
519
+ return JSON.parse(content);
520
+ }
521
+ catch {
522
+ return undefined;
523
+ }
524
+ }
525
+ /**
526
+ * Walk a directory recursively and return file paths matching optional extensions.
527
+ */
528
+ async walkDir(dir, extensions) {
529
+ const results = [];
530
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
531
+ for (const entry of entries) {
532
+ if (SKIP_DIRS.has(entry.name))
533
+ continue;
534
+ if (entry.name.startsWith('.'))
535
+ continue; // Skip hidden files/dirs
536
+ const fullPath = join(dir, entry.name);
537
+ if (entry.isDirectory()) {
538
+ const sub = await this.walkDir(fullPath, extensions);
539
+ results.push(...sub);
540
+ }
541
+ else if (entry.isFile()) {
542
+ if (this.isBinary(fullPath))
543
+ continue;
544
+ if (extensions) {
545
+ const ext = extname(entry.name).toLowerCase();
546
+ if (!extensions.includes(ext))
547
+ continue;
548
+ }
549
+ results.push(fullPath);
550
+ }
551
+ }
552
+ return results;
553
+ }
554
+ // ─── Restore helpers ──────────────────────────────────────
555
+ /**
556
+ * Parse concatenated personality back into individual files and write them.
557
+ * Files are joined with `--- FILENAME ---` markers.
558
+ */
559
+ async restoreIdentity(personality) {
560
+ const files = this.parsePersonality(personality);
561
+ for (const [filename, content] of files) {
562
+ const targetPath = join(this.workspaceDir, filename);
563
+ // Backup existing file
564
+ await this.backupFile(targetPath);
565
+ // Write restored content
566
+ await mkdir(dirname(targetPath), { recursive: true });
567
+ await writeFile(targetPath, content, 'utf-8');
568
+ }
569
+ }
570
+ /**
571
+ * Parse the concatenated personality string into individual files.
572
+ * Format: `--- FILENAME ---\ncontent\n\n--- NEXTFILE ---\ncontent`
573
+ */
574
+ parsePersonality(personality) {
575
+ const files = new Map();
576
+ const regex = /^--- (.+?) ---$/gm;
577
+ const matches = [...personality.matchAll(regex)];
578
+ for (let i = 0; i < matches.length; i++) {
579
+ const filename = matches[i][1];
580
+ const startIdx = matches[i].index + matches[i][0].length + 1; // +1 for newline
581
+ const endIdx = i + 1 < matches.length ? matches[i + 1].index : personality.length;
582
+ let content = personality.slice(startIdx, endIdx);
583
+ // Trim trailing newlines between sections (but keep content intact)
584
+ content = content.replace(/\n\n$/, '\n');
585
+ if (!content.endsWith('\n'))
586
+ content += '\n';
587
+ files.set(filename, content);
588
+ }
589
+ return files;
590
+ }
591
+ /**
592
+ * Restore memory entries back to their source files.
593
+ */
594
+ async restoreMemory(entries) {
595
+ for (const entry of entries) {
596
+ const targetPath = join(this.workspaceDir, entry.source);
597
+ // Backup existing file
598
+ await this.backupFile(targetPath);
599
+ // Ensure directory exists
600
+ await mkdir(dirname(targetPath), { recursive: true });
601
+ // Write restored content
602
+ await writeFile(targetPath, entry.content, 'utf-8');
603
+ }
604
+ }
605
+ /**
606
+ * Restore skills back to skills/ directory.
607
+ */
608
+ async restoreSkills(skills) {
609
+ for (const skill of skills) {
610
+ const skillDir = join(this.workspaceDir, 'skills', skill.name);
611
+ await mkdir(skillDir, { recursive: true });
612
+ for (const [relPath, content] of Object.entries(skill.files)) {
613
+ const targetPath = join(skillDir, relPath);
614
+ await this.backupFile(targetPath);
615
+ await mkdir(dirname(targetPath), { recursive: true });
616
+ await writeFile(targetPath, content, 'utf-8');
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Restore personal scripts.
622
+ */
623
+ async restoreScripts(scripts) {
624
+ for (const script of scripts) {
625
+ const targetPath = join(this.workspaceDir, script.path);
626
+ await this.backupFile(targetPath);
627
+ await mkdir(dirname(targetPath), { recursive: true });
628
+ await writeFile(targetPath, script.content, 'utf-8');
629
+ }
630
+ }
631
+ /**
632
+ * Restore extension configs.
633
+ */
634
+ async restoreExtensions(extensions) {
635
+ for (const ext of extensions) {
636
+ const extDir = join(this.workspaceDir, 'extensions', ext.name);
637
+ await mkdir(extDir, { recursive: true });
638
+ for (const [filename, content] of Object.entries(ext.configs)) {
639
+ const targetPath = join(extDir, filename);
640
+ await this.backupFile(targetPath);
641
+ await writeFile(targetPath, content, 'utf-8');
642
+ }
643
+ }
644
+ }
645
+ /**
646
+ * Restore config files to workspace root.
647
+ */
648
+ async restoreConfigFiles(configs) {
649
+ for (const [file, value] of Object.entries(configs)) {
650
+ if (typeof value !== 'string')
651
+ continue;
652
+ const targetPath = join(this.workspaceDir, file);
653
+ await this.backupFile(targetPath);
654
+ await mkdir(dirname(targetPath), { recursive: true });
655
+ await writeFile(targetPath, value, 'utf-8');
656
+ }
657
+ }
658
+ /**
659
+ * Create a .bak backup of an existing file before overwriting.
660
+ */
661
+ async backupFile(filePath) {
662
+ if (existsSync(filePath)) {
663
+ const backupPath = filePath + '.bak';
664
+ try {
665
+ await rename(filePath, backupPath);
666
+ }
667
+ catch {
668
+ // If rename fails (e.g., permissions), continue without backup
669
+ }
670
+ }
671
+ }
672
+ }
673
+ //# sourceMappingURL=clawdbot.js.map