@ectplsm/relic 0.1.3 → 0.2.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 (36) hide show
  1. package/README.md +261 -167
  2. package/dist/adapters/shells/claude-hook.d.ts +6 -3
  3. package/dist/adapters/shells/claude-hook.js +13 -10
  4. package/dist/adapters/shells/claude-shell.js +4 -2
  5. package/dist/adapters/shells/codex-hook.d.ts +6 -3
  6. package/dist/adapters/shells/codex-hook.js +13 -10
  7. package/dist/adapters/shells/codex-shell.js +4 -2
  8. package/dist/adapters/shells/gemini-hook.d.ts +6 -3
  9. package/dist/adapters/shells/gemini-hook.js +13 -10
  10. package/dist/adapters/shells/gemini-shell.js +4 -2
  11. package/dist/core/usecases/extract.d.ts +5 -23
  12. package/dist/core/usecases/extract.js +19 -115
  13. package/dist/core/usecases/index.d.ts +3 -3
  14. package/dist/core/usecases/index.js +3 -3
  15. package/dist/core/usecases/inject.d.ts +8 -3
  16. package/dist/core/usecases/inject.js +31 -12
  17. package/dist/core/usecases/sync.d.ts +44 -20
  18. package/dist/core/usecases/sync.js +182 -56
  19. package/dist/interfaces/cli/commands/claw.d.ts +2 -0
  20. package/dist/interfaces/cli/commands/claw.js +149 -0
  21. package/dist/interfaces/cli/commands/config.js +6 -6
  22. package/dist/interfaces/cli/commands/extract.js +7 -12
  23. package/dist/interfaces/cli/commands/inject.js +2 -2
  24. package/dist/interfaces/cli/commands/sync.js +24 -68
  25. package/dist/interfaces/cli/index.js +2 -6
  26. package/dist/interfaces/mcp/index.js +14 -0
  27. package/dist/shared/config.d.ts +7 -7
  28. package/dist/shared/config.js +18 -58
  29. package/dist/shared/engram-composer.js +5 -3
  30. package/dist/shared/openclaw.d.ts +25 -3
  31. package/dist/shared/openclaw.js +48 -4
  32. package/package.json +3 -2
  33. package/templates/engrams/johnny/IDENTITY.md +25 -0
  34. package/templates/engrams/johnny/SOUL.md +35 -0
  35. package/templates/engrams/motoko/IDENTITY.md +25 -0
  36. package/templates/engrams/motoko/SOUL.md +38 -0
@@ -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,149 @@
1
+ import { LocalEngramRepository } from "../../../adapters/local/index.js";
2
+ import { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, Sync, SyncOpenclawDirNotFoundError, } from "../../../core/usecases/index.js";
3
+ import { resolveEngramsPath, resolveClawPath } from "../../../shared/config.js";
4
+ import { resolveWorkspacePath } from "../../../shared/openclaw.js";
5
+ export function registerClawCommand(program) {
6
+ const claw = program
7
+ .command("claw")
8
+ .description("Manage Claw agent workspaces (OpenClaw and compatible)");
9
+ // --- claw inject ---
10
+ claw
11
+ .command("inject")
12
+ .description("Push an Engram into a Claw workspace")
13
+ .requiredOption("-e, --engram <id>", "Engram ID to inject")
14
+ .option("--to <agent>", "Inject into a different agent name")
15
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
16
+ .option("--merge-identity", "Merge IDENTITY.md into SOUL.md (for non-OpenClaw Claw frameworks)")
17
+ .option("--no-sync", "Skip automatic memory sync after inject")
18
+ .option("-p, --path <dir>", "Override engrams directory path")
19
+ .action(async (opts) => {
20
+ const engramsPath = await resolveEngramsPath(opts.path);
21
+ const clawDir = await resolveClawPath(opts.dir);
22
+ const repo = new LocalEngramRepository(engramsPath);
23
+ const inject = new Inject(repo);
24
+ try {
25
+ const result = await inject.execute(opts.engram, {
26
+ to: opts.to,
27
+ openclawDir: clawDir,
28
+ mergeIdentity: opts.mergeIdentity,
29
+ });
30
+ console.log(`Injected "${result.engramName}" into ${result.targetPath}`);
31
+ console.log(` Files written: ${result.filesWritten.join(", ")}`);
32
+ if (!opts.sync)
33
+ return;
34
+ // Auto-sync memory after inject
35
+ const sync = new Sync(repo, engramsPath);
36
+ const agentName = opts.to ?? opts.engram;
37
+ const workspacePath = resolveWorkspacePath(agentName, clawDir);
38
+ const syncResult = await sync.syncPair({
39
+ engramId: opts.engram,
40
+ workspacePath,
41
+ });
42
+ const details = [];
43
+ if (syncResult.memoryFilesMerged > 0) {
44
+ details.push(`${syncResult.memoryFilesMerged} memory file(s)`);
45
+ }
46
+ if (syncResult.memoryIndexMerged) {
47
+ details.push("MEMORY.md");
48
+ }
49
+ if (syncResult.userMerged) {
50
+ details.push("USER.md");
51
+ }
52
+ if (details.length > 0) {
53
+ console.log(` Synced: ${details.join(", ")}`);
54
+ }
55
+ else {
56
+ console.log(` Already in sync`);
57
+ }
58
+ }
59
+ catch (err) {
60
+ if (err instanceof InjectEngramNotFoundError ||
61
+ err instanceof InjectClawDirNotFoundError ||
62
+ err instanceof InjectWorkspaceNotFoundError) {
63
+ console.error(`Error: ${err.message}`);
64
+ process.exit(1);
65
+ }
66
+ throw err;
67
+ }
68
+ });
69
+ // --- claw extract ---
70
+ claw
71
+ .command("extract")
72
+ .description("Create a new Engram from a Claw agent workspace")
73
+ .option("-a, --agent <name>", "Agent name to extract from (default: main)")
74
+ .option("--name <name>", "Engram display name (defaults to agent name)")
75
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
76
+ .option("-p, --path <dir>", "Override engrams directory path")
77
+ .action(async (opts) => {
78
+ const engramsPath = await resolveEngramsPath(opts.path);
79
+ const clawDir = await resolveClawPath(opts.dir);
80
+ const repo = new LocalEngramRepository(engramsPath);
81
+ const extract = new Extract(repo);
82
+ try {
83
+ const agentName = opts.agent ?? "main";
84
+ const result = await extract.execute(agentName, {
85
+ name: opts.name,
86
+ openclawDir: clawDir,
87
+ });
88
+ console.log(`Extracted "${result.engramName}" from ${result.sourcePath}`);
89
+ console.log(` Files read: ${result.filesRead.join(", ")}`);
90
+ console.log(` Saved as Engram: ${result.engramId}`);
91
+ }
92
+ catch (err) {
93
+ if (err instanceof WorkspaceNotFoundError ||
94
+ err instanceof WorkspaceEmptyError ||
95
+ err instanceof AlreadyExtractedError) {
96
+ console.error(`Error: ${err.message}`);
97
+ process.exit(1);
98
+ }
99
+ throw err;
100
+ }
101
+ });
102
+ // --- claw sync ---
103
+ claw
104
+ .command("sync")
105
+ .description("Bidirectional memory sync between Engrams and Claw workspaces")
106
+ .option("--dir <dir>", "Override Claw directory path (default: ~/.openclaw)")
107
+ .option("-p, --path <dir>", "Override engrams directory path")
108
+ .action(async (opts) => {
109
+ const engramsPath = await resolveEngramsPath(opts.path);
110
+ const clawDir = await resolveClawPath(opts.dir);
111
+ const repo = new LocalEngramRepository(engramsPath);
112
+ const sync = new Sync(repo, engramsPath);
113
+ try {
114
+ const result = await sync.execute(clawDir);
115
+ if (result.synced.length === 0 && result.skipped.length === 0) {
116
+ console.log("No Claw workspaces found.");
117
+ return;
118
+ }
119
+ for (const s of result.synced) {
120
+ const details = [];
121
+ if (s.memoryFilesMerged > 0) {
122
+ details.push(`${s.memoryFilesMerged} memory file(s)`);
123
+ }
124
+ if (s.memoryIndexMerged) {
125
+ details.push("MEMORY.md");
126
+ }
127
+ if (s.userMerged) {
128
+ details.push("USER.md");
129
+ }
130
+ if (details.length > 0) {
131
+ console.log(` ${s.engramId}: merged ${details.join(", ")}`);
132
+ }
133
+ else {
134
+ console.log(` ${s.engramId}: already in sync`);
135
+ }
136
+ }
137
+ if (result.skipped.length > 0) {
138
+ console.log(` Skipped (no matching Engram): ${result.skipped.join(", ")}`);
139
+ }
140
+ }
141
+ catch (err) {
142
+ if (err instanceof SyncOpenclawDirNotFoundError) {
143
+ console.error(`Error: ${err.message}`);
144
+ process.exit(1);
145
+ }
146
+ throw err;
147
+ }
148
+ });
149
+ }
@@ -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
  }
@@ -1,51 +1,43 @@
1
- import { watch } from "node:fs";
2
- import { join } from "node:path";
3
1
  import { LocalEngramRepository } from "../../../adapters/local/index.js";
4
- import { Sync, SyncAgentsDirNotFoundError, } from "../../../core/usecases/index.js";
2
+ import { Sync, SyncOpenclawDirNotFoundError, } from "../../../core/usecases/index.js";
5
3
  import { resolveEngramsPath } from "../../../shared/config.js";
6
4
  export function registerSyncCommand(program) {
7
5
  program
8
6
  .command("sync")
9
- .description("Watch OpenClaw agents and auto-sync with Engrams")
7
+ .description("Bidirectional memory sync between Relic Engrams and OpenClaw workspaces")
10
8
  .option("--openclaw <dir>", "Override OpenClaw directory path (default: ~/.openclaw)")
11
9
  .option("-p, --path <dir>", "Override engrams directory path")
12
10
  .action(async (opts) => {
13
11
  const engramsPath = await resolveEngramsPath(opts.path);
14
12
  const repo = new LocalEngramRepository(engramsPath);
15
- const sync = new Sync(repo);
13
+ const sync = new Sync(repo, engramsPath);
16
14
  try {
17
- console.log("Starting Relic sync...");
18
- // 初回同期
19
- const result = await sync.initialSync(opts.openclaw);
20
- if (result.injected.length > 0) {
21
- console.log(` Injected: ${result.injected.join(", ")}`);
22
- }
23
- if (result.extracted.length > 0) {
24
- console.log(` Extracted memory: ${result.extracted.join(", ")}`);
25
- }
26
- if (result.targets.length === 0) {
27
- console.log(" No agents found.");
15
+ const result = await sync.execute(opts.openclaw);
16
+ if (result.synced.length === 0 && result.skipped.length === 0) {
17
+ console.log("No OpenClaw workspaces found.");
28
18
  return;
29
19
  }
30
- // ファイル監視を開始
31
- const watchers = startWatching(result.targets, sync, opts.openclaw);
32
- console.log(`\nWatching ${result.targets.length} agent(s) for memory changes...`);
33
- console.log("Press Ctrl+C to stop.\n");
34
- // Ctrl+C でクリーンアップ
35
- const cleanup = () => {
36
- console.log("\nStopping sync...");
37
- for (const w of watchers) {
38
- w.close();
20
+ for (const s of result.synced) {
21
+ const details = [];
22
+ if (s.memoryFilesMerged > 0) {
23
+ details.push(`${s.memoryFilesMerged} memory file(s)`);
24
+ }
25
+ if (s.memoryIndexMerged) {
26
+ details.push("MEMORY.md");
39
27
  }
40
- process.exit(0);
41
- };
42
- process.on("SIGINT", cleanup);
43
- process.on("SIGTERM", cleanup);
44
- // プロセスを維持
45
- await new Promise(() => { });
28
+ if (details.length > 0) {
29
+ console.log(` ${s.engramId}: merged ${details.join(", ")}`);
30
+ }
31
+ else {
32
+ console.log(` ${s.engramId}: already in sync`);
33
+ }
34
+ }
35
+ if (result.skipped.length > 0) {
36
+ console.log(` Skipped (no matching Engram): ${result.skipped.join(", ")}`);
37
+ }
46
38
  }
47
39
  catch (err) {
48
- if (err instanceof SyncAgentsDirNotFoundError) {
40
+ if (err instanceof SyncOpenclawDirNotFoundError) {
49
41
  console.error(`Error: ${err.message}`);
50
42
  process.exit(1);
51
43
  }
@@ -53,39 +45,3 @@ export function registerSyncCommand(program) {
53
45
  }
54
46
  });
55
47
  }
56
- function startWatching(targets, sync, openclawDir) {
57
- const watchers = [];
58
- // デバウンス用タイマー
59
- const debounceTimers = new Map();
60
- for (const target of targets) {
61
- const memoryDir = join(target.agentPath, "memory");
62
- try {
63
- const watcher = watch(memoryDir, { recursive: true }, (_event, filename) => {
64
- if (!filename?.endsWith(".md"))
65
- return;
66
- // デバウンス(同一agentの連続変更を500msでまとめる)
67
- const existing = debounceTimers.get(target.engramId);
68
- if (existing)
69
- clearTimeout(existing);
70
- debounceTimers.set(target.engramId, setTimeout(async () => {
71
- debounceTimers.delete(target.engramId);
72
- try {
73
- await sync.syncMemory(target.engramId, openclawDir);
74
- const now = new Date().toLocaleTimeString();
75
- console.log(`[${now}] Synced memory: ${target.engramId} (${filename})`);
76
- }
77
- catch {
78
- // sync失敗は警告のみ
79
- console.error(`[warn] Failed to sync memory for ${target.engramId}`);
80
- }
81
- }, 500));
82
- });
83
- watchers.push(watcher);
84
- }
85
- catch {
86
- // memory/ ディレクトリが存在しない場合はスキップ
87
- // (エージェントにまだメモリがない)
88
- }
89
- }
90
- return watchers;
91
- }
@@ -7,9 +7,7 @@ import { registerInitCommand } from "./commands/init.js";
7
7
  import { registerListCommand } from "./commands/list.js";
8
8
  import { registerShowCommand } from "./commands/show.js";
9
9
  import { registerShellCommands } from "./commands/shell.js";
10
- import { registerInjectCommand } from "./commands/inject.js";
11
- import { registerExtractCommand } from "./commands/extract.js";
12
- import { registerSyncCommand } from "./commands/sync.js";
10
+ import { registerClawCommand } from "./commands/claw.js";
13
11
  import { registerConfigCommand } from "./commands/config.js";
14
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
13
  const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../../package.json"), "utf-8"));
@@ -22,8 +20,6 @@ registerInitCommand(program);
22
20
  registerListCommand(program);
23
21
  registerShowCommand(program);
24
22
  registerShellCommands(program);
25
- registerInjectCommand(program);
26
- registerExtractCommand(program);
27
- registerSyncCommand(program);
23
+ registerClawCommand(program);
28
24
  registerConfigCommand(program);
29
25
  program.parse();