@ectplsm/relic 0.1.4 → 0.2.1

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.
@@ -1,94 +1,220 @@
1
- import { join } from "node:path";
2
1
  import { existsSync } from "node:fs";
3
- import { readdir } from "node:fs/promises";
2
+ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
- import { Inject } from "./inject.js";
6
- import { Extract } from "./extract.js";
5
+ import { extractAgentName, MEMORY_DIR } from "../../shared/openclaw.js";
7
6
  const DEFAULT_OPENCLAW_DIR = join(homedir(), ".openclaw");
7
+ const MEMORY_INDEX = "MEMORY.md";
8
8
  /**
9
- * Sync — OpenClawのagentsディレクトリをスキャンし、
10
- * 一致するEngramをinject / memoryをextractする。
9
+ * Sync — Relic Engram と OpenClaw workspace 間で memory を双方向マージする。
11
10
  *
12
- * 初回スキャン後は、呼び出し側がファイル監視を行い、
13
- * 変更検知時に syncMemory() を呼ぶ。
11
+ * 対象: 同名の engram/agent が両方に存在するペアのみ。
12
+ * マージ対象: memory/*.md と MEMORY.md
13
+ * マージ結果は両方に書き戻される。
14
14
  */
15
15
  export class Sync {
16
16
  repository;
17
- inject;
18
- extract;
19
- constructor(repository) {
17
+ engramsPath;
18
+ constructor(repository, engramsPath) {
20
19
  this.repository = repository;
21
- this.inject = new Inject(repository);
22
- this.extract = new Extract(repository);
20
+ this.engramsPath = engramsPath;
23
21
  }
24
- /**
25
- * OpenClawのagentsディレクトリをスキャンし、
26
- * Engramが存在するagentにはinject、全agentからmemoryをextractする。
27
- */
28
- async initialSync(openclawDir) {
22
+ async execute(openclawDir) {
29
23
  const baseDir = openclawDir ?? DEFAULT_OPENCLAW_DIR;
30
- const agentsDir = join(baseDir, "agents");
31
- if (!existsSync(agentsDir)) {
32
- throw new SyncAgentsDirNotFoundError(agentsDir);
24
+ if (!existsSync(baseDir)) {
25
+ throw new SyncOpenclawDirNotFoundError(baseDir);
33
26
  }
34
- const targets = await this.scanAgents(agentsDir);
35
- const injected = [];
36
- const extracted = [];
27
+ const targets = await this.scanMatchingPairs(baseDir);
28
+ const synced = [];
29
+ const skipped = [];
37
30
  for (const target of targets) {
38
- // Engramがあるagentにはpersonaを注入
39
- if (target.hasEngram) {
40
- try {
41
- await this.inject.execute(target.engramId, { openclawDir });
42
- injected.push(target.engramId);
43
- }
44
- catch {
45
- // inject失敗は警告として続行
46
- }
47
- }
48
- // 全agentからmemoryを抽出
49
31
  try {
50
- await this.extract.execute(target.engramId, {
51
- openclawDir,
52
- });
53
- extracted.push(target.engramId);
32
+ const result = await this.syncPair(target);
33
+ synced.push(result);
54
34
  }
55
35
  catch {
56
- // extract失敗(新規Engramでname未指定等)はスキップ
36
+ skipped.push(target.engramId);
37
+ }
38
+ }
39
+ // workspace に対応する engram ��ないものを skipped に追加
40
+ const allWorkspaces = await this.scanAllWorkspaces(baseDir);
41
+ for (const ws of allWorkspaces) {
42
+ const matched = targets.some((t) => t.engramId === ws);
43
+ if (!matched) {
44
+ skipped.push(ws);
45
+ }
46
+ }
47
+ return { synced, skipped };
48
+ }
49
+ /**
50
+ * 1ペア分の memory マージ(inject 後の自動 sync 等でも使用)
51
+ */
52
+ async syncPair(target) {
53
+ const relicDir = join(this.engramsPath, target.engramId);
54
+ const openclawDir = target.workspacePath;
55
+ // memory/*.md のマージ
56
+ const memoryFilesMerged = await this.mergeMemoryEntries(relicDir, openclawDir);
57
+ // MEMORY.md のマージ
58
+ const memoryIndexMerged = await this.mergeMemoryIndex(relicDir, openclawDir);
59
+ // USER.md のマージ
60
+ const userMerged = await this.mergeSingleFile(relicDir, openclawDir, "USER.md");
61
+ return {
62
+ engramId: target.engramId,
63
+ memoryFilesMerged,
64
+ memoryIndexMerged,
65
+ userMerged,
66
+ };
67
+ }
68
+ /**
69
+ * memory/*.md を双方向マージ
70
+ */
71
+ async mergeMemoryEntries(relicDir, openclawDir) {
72
+ const relicMemDir = join(relicDir, MEMORY_DIR);
73
+ const openclawMemDir = join(openclawDir, MEMORY_DIR);
74
+ const relicEntries = await this.readMemoryDir(relicMemDir);
75
+ const openclawEntries = await this.readMemoryDir(openclawMemDir);
76
+ // 全日付の union
77
+ const allDates = new Set([
78
+ ...Object.keys(relicEntries),
79
+ ...Object.keys(openclawEntries),
80
+ ]);
81
+ let mergedCount = 0;
82
+ for (const date of allDates) {
83
+ const relicContent = relicEntries[date];
84
+ const openclawContent = openclawEntries[date];
85
+ if (relicContent && !openclawContent) {
86
+ // RELIC にだけある → OpenClaw にコピー
87
+ await mkdir(openclawMemDir, { recursive: true });
88
+ await writeFile(join(openclawMemDir, `${date}.md`), relicContent, "utf-8");
89
+ mergedCount++;
57
90
  }
91
+ else if (!relicContent && openclawContent) {
92
+ // OpenClaw にだけある → RELIC にコピー
93
+ await mkdir(relicMemDir, { recursive: true });
94
+ await writeFile(join(relicMemDir, `${date}.md`), openclawContent, "utf-8");
95
+ mergedCount++;
96
+ }
97
+ else if (relicContent && openclawContent && relicContent !== openclawContent) {
98
+ // 両方にあるが内容が違う → マージして両方に書き込み
99
+ const merged = this.mergeContents(relicContent, openclawContent);
100
+ await writeFile(join(relicMemDir, `${date}.md`), merged, "utf-8");
101
+ await writeFile(join(openclawMemDir, `${date}.md`), merged, "utf-8");
102
+ mergedCount++;
103
+ }
104
+ // 内容が同じ → 何もしない
58
105
  }
59
- return { injected, extracted, targets };
106
+ return mergedCount;
60
107
  }
61
108
  /**
62
- * 特定agentのmemoryを同期(ファイル変更検知時に呼ばれる)
109
+ * MEMORY.md を双方向マージ
63
110
  */
64
- async syncMemory(engramId, openclawDir) {
65
- await this.extract.execute(engramId, { openclawDir });
111
+ async mergeMemoryIndex(relicDir, openclawDir) {
112
+ return this.mergeSingleFile(relicDir, openclawDir, MEMORY_INDEX);
66
113
  }
67
114
  /**
68
- * agentsディレクトリをスキャンしてターゲット一覧を返す
115
+ * 単一ファイルの双方向マージ(MEMORY.md, USER.md 等)
69
116
  */
70
- async scanAgents(agentsDir) {
71
- const entries = await readdir(agentsDir, { withFileTypes: true });
117
+ async mergeSingleFile(relicDir, openclawDir, filename) {
118
+ const relicPath = join(relicDir, filename);
119
+ const openclawPath = join(openclawDir, filename);
120
+ const relicContent = existsSync(relicPath)
121
+ ? await readFile(relicPath, "utf-8")
122
+ : null;
123
+ const openclawContent = existsSync(openclawPath)
124
+ ? await readFile(openclawPath, "utf-8")
125
+ : null;
126
+ if (relicContent && !openclawContent) {
127
+ await writeFile(openclawPath, relicContent, "utf-8");
128
+ return true;
129
+ }
130
+ else if (!relicContent && openclawContent) {
131
+ await writeFile(relicPath, openclawContent, "utf-8");
132
+ return true;
133
+ }
134
+ else if (relicContent && openclawContent && relicContent !== openclawContent) {
135
+ const merged = this.mergeContents(relicContent, openclawContent);
136
+ await writeFile(relicPath, merged, "utf-8");
137
+ await writeFile(openclawPath, merged, "utf-8");
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+ /**
143
+ * 2つのテキスト内容をマージする。
144
+ * 重複行を除外しつつ、両方の内容を結合する。
145
+ */
146
+ mergeContents(a, b) {
147
+ const aLines = a.trimEnd();
148
+ const bLines = b.trimEnd();
149
+ // 完全一致チェック(ここには来ないはずだが安全策)
150
+ if (aLines === bLines)
151
+ return a;
152
+ // b の中で a に含まれない部分を抽出して追記
153
+ const aSet = new Set(aLines.split("\n"));
154
+ const uniqueB = bLines
155
+ .split("\n")
156
+ .filter((line) => !aSet.has(line));
157
+ if (uniqueB.length === 0)
158
+ return a;
159
+ return aLines + "\n\n" + uniqueB.join("\n") + "\n";
160
+ }
161
+ /**
162
+ * memory/ ディレクトリから日付 → 内容のマップを読む
163
+ */
164
+ async readMemoryDir(memDir) {
165
+ const entries = {};
166
+ if (!existsSync(memDir))
167
+ return entries;
168
+ const files = await readdir(memDir);
169
+ for (const file of files) {
170
+ if (!file.endsWith(".md"))
171
+ continue;
172
+ const date = file.replace(/\.md$/, "");
173
+ entries[date] = await readFile(join(memDir, file), "utf-8");
174
+ }
175
+ return entries;
176
+ }
177
+ /**
178
+ * 同名の engram/agent が両方に存在するペアを返す
179
+ */
180
+ async scanMatchingPairs(baseDir) {
181
+ const entries = await readdir(baseDir, { withFileTypes: true });
72
182
  const targets = [];
73
183
  for (const entry of entries) {
74
184
  if (!entry.isDirectory())
75
185
  continue;
76
- const agentPath = join(agentsDir, entry.name, "agent");
77
- if (!existsSync(agentPath))
186
+ const agentName = extractAgentName(entry.name);
187
+ if (!agentName)
188
+ continue;
189
+ const engram = await this.repository.get(agentName);
190
+ if (!engram)
78
191
  continue;
79
- const engram = await this.repository.get(entry.name);
80
192
  targets.push({
81
- engramId: entry.name,
82
- agentPath,
83
- hasEngram: engram !== null,
193
+ engramId: agentName,
194
+ workspacePath: join(baseDir, entry.name),
84
195
  });
85
196
  }
86
197
  return targets;
87
198
  }
199
+ /**
200
+ * 全 workspace のエージェント名一覧
201
+ */
202
+ async scanAllWorkspaces(baseDir) {
203
+ const entries = await readdir(baseDir, { withFileTypes: true });
204
+ const names = [];
205
+ for (const entry of entries) {
206
+ if (!entry.isDirectory())
207
+ continue;
208
+ const agentName = extractAgentName(entry.name);
209
+ if (agentName)
210
+ names.push(agentName);
211
+ }
212
+ return names;
213
+ }
88
214
  }
89
- export class SyncAgentsDirNotFoundError extends Error {
215
+ export class SyncOpenclawDirNotFoundError extends Error {
90
216
  constructor(path) {
91
- super(`OpenClaw agents directory not found at ${path}`);
92
- this.name = "SyncAgentsDirNotFoundError";
217
+ super(`OpenClaw directory not found at ${path}`);
218
+ this.name = "SyncOpenclawDirNotFoundError";
93
219
  }
94
220
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerClawCommand(program: Command): void;
@@ -0,0 +1,280 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { existsSync } from "node:fs";
4
+ import { LocalEngramRepository } from "../../../adapters/local/index.js";
5
+ import { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, Sync, SyncOpenclawDirNotFoundError, } from "../../../core/usecases/index.js";
6
+ import { resolveEngramsPath, resolveClawPath } from "../../../shared/config.js";
7
+ import { resolveWorkspacePath } from "../../../shared/openclaw.js";
8
+ export function registerClawCommand(program) {
9
+ const claw = program
10
+ .command("claw")
11
+ .description("Manage Claw agent workspaces (OpenClaw and compatible)");
12
+ // --- claw inject ---
13
+ claw
14
+ .command("inject")
15
+ .description("Push an Engram into a Claw workspace")
16
+ .requiredOption("-e, --engram <id>", "Engram ID to inject")
17
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
18
+ .option("--merge-identity", "Merge IDENTITY.md into SOUL.md (for non-OpenClaw Claw frameworks)")
19
+ .option("-y, --yes", "Skip persona overwrite confirmation")
20
+ .option("--no-sync", "Skip automatic memory sync after inject")
21
+ .option("-p, --path <dir>", "Override engrams directory path")
22
+ .action(async (opts) => {
23
+ const engramsPath = await resolveEngramsPath(opts.path);
24
+ const clawDir = await resolveClawPath(opts.dir);
25
+ const repo = new LocalEngramRepository(engramsPath);
26
+ const inject = new Inject(repo);
27
+ try {
28
+ const diff = await inject.inspectPersona(opts.engram, {
29
+ openclawDir: clawDir,
30
+ mergeIdentity: opts.mergeIdentity,
31
+ });
32
+ const alreadyInjected = diff.soul === "same" &&
33
+ (diff.identity === "same" || diff.identity === "skipped");
34
+ if (diff.overwriteRequired &&
35
+ !opts.yes &&
36
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist and differ in ${diff.targetPath}. Overwrite with local Relic version? [y/N] `))) {
37
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from local Relic Engram.");
38
+ process.exit(1);
39
+ }
40
+ if (alreadyInjected) {
41
+ console.log(` Already injected (${diff.targetPath})`);
42
+ }
43
+ else {
44
+ const result = await inject.execute(opts.engram, {
45
+ openclawDir: clawDir,
46
+ mergeIdentity: opts.mergeIdentity,
47
+ });
48
+ console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
49
+ console.log(` Files written: ${result.filesWritten.join(", ")}`);
50
+ }
51
+ if (!opts.sync)
52
+ return;
53
+ // Auto-sync memory after inject
54
+ const sync = new Sync(repo, engramsPath);
55
+ const workspacePath = resolveWorkspacePath(opts.engram, clawDir);
56
+ const syncResult = await sync.syncPair({
57
+ engramId: opts.engram,
58
+ workspacePath,
59
+ });
60
+ const details = [];
61
+ if (syncResult.memoryFilesMerged > 0) {
62
+ details.push(`${syncResult.memoryFilesMerged} memory file(s)`);
63
+ }
64
+ if (syncResult.memoryIndexMerged) {
65
+ details.push("MEMORY.md");
66
+ }
67
+ if (syncResult.userMerged) {
68
+ details.push("USER.md");
69
+ }
70
+ if (details.length > 0) {
71
+ console.log(` Synced: ${details.join(", ")}`);
72
+ }
73
+ else {
74
+ console.log(` Already in sync`);
75
+ }
76
+ }
77
+ catch (err) {
78
+ if (err instanceof InjectEngramNotFoundError ||
79
+ err instanceof InjectClawDirNotFoundError ||
80
+ err instanceof InjectWorkspaceNotFoundError) {
81
+ console.error(`Error: ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ throw err;
85
+ }
86
+ });
87
+ // --- claw extract ---
88
+ claw
89
+ .command("extract")
90
+ .description("Create a new Engram from a Claw agent workspace")
91
+ .option("-a, --agent <name>", "Agent name to extract from (default: main)")
92
+ .option("--name <name>", "Engram display name (defaults to agent name)")
93
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
94
+ .option("-f, --force", "Allow overwriting local persona files from the Claw workspace")
95
+ .option("-y, --yes", "Skip persona overwrite confirmation")
96
+ .option("--no-sync", "Skip automatic memory sync after extract")
97
+ .option("-p, --path <dir>", "Override engrams directory path")
98
+ .action(async (opts) => {
99
+ const engramsPath = await resolveEngramsPath(opts.path);
100
+ const clawDir = await resolveClawPath(opts.dir);
101
+ const repo = new LocalEngramRepository(engramsPath);
102
+ const extract = new Extract(repo);
103
+ const sync = new Sync(repo, engramsPath);
104
+ try {
105
+ const agentName = opts.agent ?? "main";
106
+ const diff = await extract.inspectPersona(agentName, {
107
+ name: opts.name,
108
+ openclawDir: clawDir,
109
+ });
110
+ if (diff.existing && !opts.force) {
111
+ throw new AlreadyExtractedError(agentName);
112
+ }
113
+ const alreadyExtracted = diff.existing &&
114
+ diff.name === "same" &&
115
+ diff.soul === "same" &&
116
+ diff.identity === "same";
117
+ if (diff.existing &&
118
+ diff.overwriteRequired &&
119
+ !opts.yes &&
120
+ !(await confirmOverwrite(`SOUL.md and/or IDENTITY.md already exist in local Engram "${agentName}" and differ from ${diff.sourcePath}. Overwrite with the Claw version? [y/N] `))) {
121
+ console.error("Error: Persona files already exist and differ. Re-run with --yes to overwrite from the Claw workspace.");
122
+ process.exit(1);
123
+ }
124
+ if (alreadyExtracted) {
125
+ console.log(opts.force
126
+ ? ` Already extracted and updated (${agentName})`
127
+ : ` Already extracted (${agentName})`);
128
+ }
129
+ else {
130
+ const result = await extract.execute(agentName, {
131
+ name: opts.name,
132
+ openclawDir: clawDir,
133
+ force: opts.force,
134
+ });
135
+ console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
136
+ if (diff.existing) {
137
+ console.log(` Files overwritten: SOUL.md, IDENTITY.md`);
138
+ if (diff.name === "different") {
139
+ console.log(` Metadata updated: engram.json (name)`);
140
+ }
141
+ console.log(` Saved as Engram: ${result.engramId}`);
142
+ }
143
+ else {
144
+ console.log(` Files extracted: ${result.filesRead.join(", ")}`);
145
+ console.log(` Saved as Engram: ${result.engramId}`);
146
+ }
147
+ }
148
+ if (!opts.sync)
149
+ return;
150
+ const syncResult = await sync.syncPair({
151
+ engramId: agentName,
152
+ workspacePath: diff.sourcePath,
153
+ });
154
+ const details = [];
155
+ if (syncResult.memoryFilesMerged > 0) {
156
+ details.push(`${syncResult.memoryFilesMerged} memory file(s)`);
157
+ }
158
+ if (syncResult.memoryIndexMerged) {
159
+ details.push("MEMORY.md");
160
+ }
161
+ if (syncResult.userMerged) {
162
+ details.push("USER.md");
163
+ }
164
+ if (details.length > 0) {
165
+ console.log(` Synced: ${details.join(", ")}`);
166
+ }
167
+ else {
168
+ console.log(` Already in sync`);
169
+ }
170
+ }
171
+ catch (err) {
172
+ if (err instanceof WorkspaceNotFoundError ||
173
+ err instanceof WorkspaceEmptyError ||
174
+ err instanceof AlreadyExtractedError) {
175
+ console.error(`Error: ${err.message}`);
176
+ process.exit(1);
177
+ }
178
+ throw err;
179
+ }
180
+ });
181
+ // --- claw sync ---
182
+ claw
183
+ .command("sync")
184
+ .description("Bidirectional memory sync between Engrams and Claw workspaces")
185
+ .option("-t, --target <id>", "Sync one target only by shared Engram/agent name")
186
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
187
+ .option("-p, --path <dir>", "Override engrams directory path")
188
+ .action(async (opts) => {
189
+ const engramsPath = await resolveEngramsPath(opts.path);
190
+ const clawDir = await resolveClawPath(opts.dir);
191
+ const repo = new LocalEngramRepository(engramsPath);
192
+ const sync = new Sync(repo, engramsPath);
193
+ try {
194
+ if (opts.target) {
195
+ const targetId = opts.target.trim();
196
+ if (!targetId) {
197
+ console.error(`Error: Invalid sync target "${opts.target}".`);
198
+ process.exit(1);
199
+ }
200
+ const engram = await repo.get(targetId);
201
+ if (!engram) {
202
+ console.error(`Error: Engram "${targetId}" not found.`);
203
+ process.exit(1);
204
+ }
205
+ const workspacePath = resolveWorkspacePath(targetId, clawDir);
206
+ if (!existsSync(workspacePath)) {
207
+ console.error(`Error: Claw agent "${targetId}" workspace not found.`);
208
+ process.exit(1);
209
+ }
210
+ const result = await sync.syncPair({
211
+ engramId: targetId,
212
+ workspacePath,
213
+ });
214
+ const details = [];
215
+ if (result.memoryFilesMerged > 0) {
216
+ details.push(`${result.memoryFilesMerged} memory file(s)`);
217
+ }
218
+ if (result.memoryIndexMerged) {
219
+ details.push("MEMORY.md");
220
+ }
221
+ if (result.userMerged) {
222
+ details.push("USER.md");
223
+ }
224
+ if (details.length > 0) {
225
+ console.log(` ${targetId}: merged ${details.join(", ")}`);
226
+ }
227
+ else {
228
+ console.log(` Already in sync (${targetId})`);
229
+ }
230
+ return;
231
+ }
232
+ const result = await sync.execute(clawDir);
233
+ if (result.synced.length === 0 && result.skipped.length === 0) {
234
+ console.log("No Claw workspaces found.");
235
+ return;
236
+ }
237
+ for (const s of result.synced) {
238
+ const details = [];
239
+ if (s.memoryFilesMerged > 0) {
240
+ details.push(`${s.memoryFilesMerged} memory file(s)`);
241
+ }
242
+ if (s.memoryIndexMerged) {
243
+ details.push("MEMORY.md");
244
+ }
245
+ if (s.userMerged) {
246
+ details.push("USER.md");
247
+ }
248
+ if (details.length > 0) {
249
+ console.log(` ${s.engramId}: merged ${details.join(", ")}`);
250
+ }
251
+ else {
252
+ console.log(` Already in sync (${s.engramId})`);
253
+ }
254
+ }
255
+ if (result.skipped.length > 0) {
256
+ console.log(` Skipped (no matching Engram): ${result.skipped.join(", ")}`);
257
+ }
258
+ }
259
+ catch (err) {
260
+ if (err instanceof SyncOpenclawDirNotFoundError) {
261
+ console.error(`Error: ${err.message}`);
262
+ process.exit(1);
263
+ }
264
+ throw err;
265
+ }
266
+ });
267
+ }
268
+ async function confirmOverwrite(message) {
269
+ if (!input.isTTY || !output.isTTY) {
270
+ return false;
271
+ }
272
+ const rl = createInterface({ input, output });
273
+ try {
274
+ const answer = await rl.question(message);
275
+ return /^(y|yes)$/i.test(answer.trim());
276
+ }
277
+ finally {
278
+ rl.close();
279
+ }
280
+ }
@@ -43,21 +43,21 @@ export function registerConfigCommand(program) {
43
43
  await saveConfig(cfg);
44
44
  console.log(`Default Engram set to: ${engram.meta.name} (${id})`);
45
45
  });
46
- // relic config openclaw-path [path]
46
+ // relic config claw-path [path]
47
47
  config
48
- .command("openclaw-path [path]")
49
- .description("Get or set the OpenClaw directory path (default: ~/.openclaw)")
48
+ .command("claw-path [path]")
49
+ .description("Get or set the Claw directory path (default: ~/.openclaw)")
50
50
  .action(async (path) => {
51
51
  await ensureInitialized();
52
52
  const cfg = await loadConfig();
53
53
  if (!path) {
54
54
  // getter
55
- console.log(cfg.openclawPath ?? "(not set — using ~/.openclaw)");
55
+ console.log(cfg.clawPath ?? "(not set — using ~/.openclaw)");
56
56
  return;
57
57
  }
58
- cfg.openclawPath = path;
58
+ cfg.clawPath = path;
59
59
  await saveConfig(cfg);
60
- console.log(`OpenClaw path set to: ${path}`);
60
+ console.log(`Claw path set to: ${path}`);
61
61
  });
62
62
  // relic config memory-window [n]
63
63
  config
@@ -1,38 +1,33 @@
1
1
  import { LocalEngramRepository } from "../../../adapters/local/index.js";
2
- import { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, } from "../../../core/usecases/index.js";
2
+ import { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, } from "../../../core/usecases/index.js";
3
3
  import { resolveEngramsPath, resolveOpenclawPath } from "../../../shared/config.js";
4
4
  export function registerExtractCommand(program) {
5
5
  program
6
6
  .command("extract")
7
- .description("Extract an Engram from an OpenClaw workspace")
8
- .requiredOption("-e, --engram <id>", "Engram ID (= agent name)")
9
- .option("--name <name>", "Engram display name (required for new Engrams)")
7
+ .description("Create a new Engram from an OpenClaw agent workspace")
8
+ .option("-a, --agent <name>", "OpenClaw agent name to extract from (default: main)")
9
+ .option("--name <name>", "Engram display name (defaults to agent name)")
10
10
  .option("--openclaw <dir>", "Override OpenClaw directory path (default: ~/.openclaw)")
11
11
  .option("-p, --path <dir>", "Override engrams directory path")
12
- .option("-f, --force", "Overwrite existing Engram persona files")
13
12
  .action(async (opts) => {
14
13
  const engramsPath = await resolveEngramsPath(opts.path);
15
14
  const openclawDir = await resolveOpenclawPath(opts.openclaw);
16
15
  const repo = new LocalEngramRepository(engramsPath);
17
16
  const extract = new Extract(repo);
18
17
  try {
19
- const result = await extract.execute(opts.engram, {
18
+ const agentName = opts.agent ?? "main";
19
+ const result = await extract.execute(agentName, {
20
20
  name: opts.name,
21
21
  openclawDir,
22
- force: opts.force,
23
22
  });
24
23
  console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
25
24
  console.log(` Files read: ${result.filesRead.join(", ")}`);
26
25
  console.log(` Saved as Engram: ${result.engramId}`);
27
- if (result.memoryMerged) {
28
- console.log(` Memory entries merged into existing Engram`);
29
- }
30
26
  }
31
27
  catch (err) {
32
28
  if (err instanceof WorkspaceNotFoundError ||
33
29
  err instanceof WorkspaceEmptyError ||
34
- err instanceof EngramAlreadyExistsError ||
35
- err instanceof ExtractNameRequiredError) {
30
+ err instanceof AlreadyExtractedError) {
36
31
  console.error(`Error: ${err.message}`);
37
32
  process.exit(1);
38
33
  }
@@ -1,5 +1,5 @@
1
1
  import { LocalEngramRepository } from "../../../adapters/local/index.js";
2
- import { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, } from "../../../core/usecases/index.js";
2
+ import { Inject, InjectEngramNotFoundError, InjectWorkspaceNotFoundError, } from "../../../core/usecases/index.js";
3
3
  import { resolveEngramsPath, resolveOpenclawPath } from "../../../shared/config.js";
4
4
  export function registerInjectCommand(program) {
5
5
  program
@@ -24,7 +24,7 @@ export function registerInjectCommand(program) {
24
24
  }
25
25
  catch (err) {
26
26
  if (err instanceof InjectEngramNotFoundError ||
27
- err instanceof InjectAgentNotFoundError) {
27
+ err instanceof InjectWorkspaceNotFoundError) {
28
28
  console.error(`Error: ${err.message}`);
29
29
  process.exit(1);
30
30
  }