@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,8 +1,11 @@
1
1
  export declare const CODEX_HOOK_SCRIPT_PATH: string;
2
2
  /**
3
- * Codex CLI の Stop フックをセットアップする。
4
- * - ~/.relic/hooks/codex-stop.js を生成
5
- * - ~/.codex/hooks.json に Stop フックを登録
3
+ * フックスクリプトを最新の内容で書き出す。
4
+ * 毎回呼ばれ、ソース変更がデプロイされることを保証する。
5
+ */
6
+ export declare function writeCodexHookScript(): void;
7
+ /**
8
+ * Codex CLI の Stop フックを settings.json に登録する。
6
9
  * 既にセットアップ済みの場合はスキップ。
7
10
  */
8
11
  export declare function setupCodexHook(): void;
@@ -17,8 +17,8 @@ const HOOK_SCRIPT = `#!/usr/bin/env node
17
17
  // Relic Stop hook for Codex CLI
18
18
  // Automatically logs each conversation turn to the Engram archive.
19
19
  // Receives Stop hook JSON on stdin.
20
- const { appendFileSync, existsSync, readFileSync } = require("node:fs");
21
- const { join } = require("node:path");
20
+ const { appendFileSync, existsSync, mkdirSync, readFileSync } = require("node:fs");
21
+ const { join, dirname } = require("node:path");
22
22
  const { homedir } = require("node:os");
23
23
 
24
24
  let raw = "";
@@ -31,7 +31,7 @@ process.stdin.on("end", () => {
31
31
  if (!engramId) process.exit(0);
32
32
 
33
33
  const archivePath = join(homedir(), ".relic", "engrams", engramId, "archive.md");
34
- if (!existsSync(archivePath)) process.exit(0);
34
+ mkdirSync(dirname(archivePath), { recursive: true });
35
35
 
36
36
  // Codex Stop hook は last_assistant_message を直接提供する
37
37
  const lastResponse = (input.last_assistant_message || "").trim();
@@ -80,16 +80,19 @@ process.stdin.on("end", () => {
80
80
  });
81
81
  `;
82
82
  /**
83
- * Codex CLI の Stop フックをセットアップする。
84
- * - ~/.relic/hooks/codex-stop.js を生成
85
- * - ~/.codex/hooks.json に Stop フックを登録
86
- * 既にセットアップ済みの場合はスキップ。
83
+ * フックスクリプトを最新の内容で書き出す。
84
+ * 毎回呼ばれ、ソース変更がデプロイされることを保証する。
87
85
  */
88
- export function setupCodexHook() {
89
- // 1. フックスクリプトを生成
86
+ export function writeCodexHookScript() {
90
87
  mkdirSync(HOOKS_DIR, { recursive: true });
91
88
  writeFileSync(CODEX_HOOK_SCRIPT_PATH, HOOK_SCRIPT, { encoding: "utf-8", mode: 0o755 });
92
- // 2. ~/.codex/hooks.json に Stop フックを登録
89
+ }
90
+ /**
91
+ * Codex CLI の Stop フックを settings.json に登録する。
92
+ * 既にセットアップ済みの場合はスキップ。
93
+ */
94
+ export function setupCodexHook() {
95
+ // ~/.codex/hooks.json に Stop フックを登録
93
96
  const codexDir = join(homedir(), ".codex");
94
97
  mkdirSync(codexDir, { recursive: true });
95
98
  let hooksConfig = {};
@@ -2,7 +2,7 @@ import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { spawnShell } from "./spawn-shell.js";
4
4
  import { wrapWithOverride } from "./override-preamble.js";
5
- import { setupCodexHook, isCodexHookSetup } from "./codex-hook.js";
5
+ import { setupCodexHook, isCodexHookSetup, writeCodexHookScript } from "./codex-hook.js";
6
6
  const execAsync = promisify(exec);
7
7
  /**
8
8
  * Codex CLI アダプター
@@ -29,7 +29,9 @@ export class CodexShell {
29
29
  }
30
30
  }
31
31
  async launch(prompt, options) {
32
- // Stop フックを初回のみセットアップ
32
+ // フックスクリプトを毎回最新に更新
33
+ writeCodexHookScript();
34
+ // hooks.json への登録は初回のみ
33
35
  if (!isCodexHookSetup()) {
34
36
  console.log("Setting up Codex CLI Stop hook (first run only)...");
35
37
  setupCodexHook();
@@ -1,8 +1,11 @@
1
1
  export declare const GEMINI_HOOK_SCRIPT_PATH: string;
2
2
  /**
3
- * Gemini CLI の AfterAgent フックをセットアップする。
4
- * - ~/.relic/hooks/gemini-after-agent.js を生成
5
- * - ~/.gemini/settings.json に AfterAgent フックを登録
3
+ * フックスクリプトを最新の内容で書き出す。
4
+ * 毎回呼ばれ、ソース変更がデプロイされることを保証する。
5
+ */
6
+ export declare function writeGeminiHookScript(): void;
7
+ /**
8
+ * Gemini CLI の AfterAgent フックを settings.json に登録する。
6
9
  * 既にセットアップ済みの場合はスキップ。
7
10
  */
8
11
  export declare function setupGeminiHook(): void;
@@ -15,8 +15,8 @@ const HOOK_SCRIPT = `#!/usr/bin/env node
15
15
  // Relic AfterAgent hook for Gemini CLI
16
16
  // Automatically logs each conversation turn to the Engram archive.
17
17
  // Receives AfterAgentInput JSON on stdin.
18
- const { appendFileSync, existsSync } = require("node:fs");
19
- const { join } = require("node:path");
18
+ const { appendFileSync, existsSync, mkdirSync } = require("node:fs");
19
+ const { join, dirname } = require("node:path");
20
20
  const { homedir } = require("node:os");
21
21
 
22
22
  let raw = "";
@@ -33,7 +33,7 @@ process.stdin.on("end", () => {
33
33
  if (!prompt && !response) process.exit(0);
34
34
 
35
35
  const archivePath = join(homedir(), ".relic", "engrams", engramId, "archive.md");
36
- if (!existsSync(archivePath)) process.exit(0);
36
+ mkdirSync(dirname(archivePath), { recursive: true });
37
37
 
38
38
  const date = new Date().toISOString().split("T")[0];
39
39
  const summary = prompt.slice(0, 80).replace(/\\n/g, " ");
@@ -46,16 +46,19 @@ process.stdin.on("end", () => {
46
46
  });
47
47
  `;
48
48
  /**
49
- * Gemini CLI の AfterAgent フックをセットアップする。
50
- * - ~/.relic/hooks/gemini-after-agent.js を生成
51
- * - ~/.gemini/settings.json に AfterAgent フックを登録
52
- * 既にセットアップ済みの場合はスキップ。
49
+ * フックスクリプトを最新の内容で書き出す。
50
+ * 毎回呼ばれ、ソース変更がデプロイされることを保証する。
53
51
  */
54
- export function setupGeminiHook() {
55
- // 1. フックスクリプトを生成
52
+ export function writeGeminiHookScript() {
56
53
  mkdirSync(HOOKS_DIR, { recursive: true });
57
54
  writeFileSync(GEMINI_HOOK_SCRIPT_PATH, HOOK_SCRIPT, { encoding: "utf-8", mode: 0o755 });
58
- // 2. ~/.gemini/settings.json にフックを登録
55
+ }
56
+ /**
57
+ * Gemini CLI の AfterAgent フックを settings.json に登録する。
58
+ * 既にセットアップ済みの場合はスキップ。
59
+ */
60
+ export function setupGeminiHook() {
61
+ // ~/.gemini/settings.json にフックを登録
59
62
  const geminiDir = join(homedir(), ".gemini");
60
63
  mkdirSync(geminiDir, { recursive: true });
61
64
  let settings = {};
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
5
5
  import { join } from "node:path";
6
6
  import { homedir, tmpdir } from "node:os";
7
7
  import { spawnShell, writeTempPrompt } from "./spawn-shell.js";
8
- import { setupGeminiHook, isGeminiHookSetup } from "./gemini-hook.js";
8
+ import { setupGeminiHook, isGeminiHookSetup, writeGeminiHookScript } from "./gemini-hook.js";
9
9
  const execAsync = promisify(exec);
10
10
  const RELIC_DIR = join(homedir(), ".relic");
11
11
  const GEMINI_DEFAULT_CACHE = join(RELIC_DIR, "gemini-system-default.md");
@@ -97,7 +97,9 @@ export class GeminiShell {
97
97
  }
98
98
  }
99
99
  async launch(prompt, options) {
100
- // 1. AfterAgent フックを初回のみセットアップ
100
+ // 1. フックスクリプトを毎回最新に更新
101
+ writeGeminiHookScript();
102
+ // settings.json への登録は初回のみ
101
103
  if (!isGeminiHookSetup()) {
102
104
  console.log("Setting up Gemini AfterAgent hook (first run only)...");
103
105
  setupGeminiHook();
@@ -4,35 +4,20 @@ export interface ExtractResult {
4
4
  engramName: string;
5
5
  sourcePath: string;
6
6
  filesRead: string[];
7
- memoryMerged: boolean;
8
7
  }
9
8
  /**
10
- * Extract — OpenClawワークスペースからEngramを作成する
9
+ * Extract — OpenClawワークスペースからEngramを新規作成する
11
10
  *
12
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ から読み取る。
13
- *
14
- * 既存Engramがある場合:
15
- * - --force なし → memoryエントリのみマージ(persona部分は変更しない)
16
- * - --force あり → persona部分を上書き + memoryをマージ
17
- * 既存Engramがない場合:
18
- * - 新規作成(全ファイル含む)
11
+ * 初回取り込み専用。Engramが既に存在する場合はエラーを返す。
12
+ * Relicが真のデータソースであり、extractは初期インポートのみを担う。
19
13
  */
20
14
  export declare class Extract {
21
15
  private readonly repository;
22
16
  constructor(repository: EngramRepository);
23
- execute(engramId: string, options?: {
17
+ execute(agentName: string, options?: {
24
18
  name?: string;
25
19
  openclawDir?: string;
26
- force?: boolean;
27
20
  }): Promise<ExtractResult>;
28
- /**
29
- * --force時: persona上書き + memoryマージ
30
- */
31
- private mergeAndSave;
32
- /**
33
- * --forceなし時: memoryエントリのみ追記(repositoryのAPIを使用)
34
- */
35
- private mergeMemoryOnly;
36
21
  private readFiles;
37
22
  }
38
23
  export declare class WorkspaceNotFoundError extends Error {
@@ -41,9 +26,6 @@ export declare class WorkspaceNotFoundError extends Error {
41
26
  export declare class WorkspaceEmptyError extends Error {
42
27
  constructor(path: string);
43
28
  }
44
- export declare class EngramAlreadyExistsError extends Error {
45
- constructor(id: string);
46
- }
47
- export declare class ExtractNameRequiredError extends Error {
29
+ export declare class AlreadyExtractedError extends Error {
48
30
  constructor(id: string);
49
31
  }
@@ -1,69 +1,39 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, readdir } from "node:fs/promises";
4
- import { FILE_MAP, MEMORY_DIR, resolveAgentPath } from "../../shared/openclaw.js";
4
+ import { RELIC_FILE_MAP, MEMORY_DIR, resolveWorkspacePath } from "../../shared/openclaw.js";
5
5
  /**
6
- * Extract — OpenClawワークスペースからEngramを作成する
6
+ * Extract — OpenClawワークスペースからEngramを新規作成する
7
7
  *
8
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ から読み取る。
9
- *
10
- * 既存Engramがある場合:
11
- * - --force なし → memoryエントリのみマージ(persona部分は変更しない)
12
- * - --force あり → persona部分を上書き + memoryをマージ
13
- * 既存Engramがない場合:
14
- * - 新規作成(全ファイル含む)
8
+ * 初回取り込み専用。Engramが既に存在する場合はエラーを返す。
9
+ * Relicが真のデータソースであり、extractは初期インポートのみを担う。
15
10
  */
16
11
  export class Extract {
17
12
  repository;
18
13
  constructor(repository) {
19
14
  this.repository = repository;
20
15
  }
21
- async execute(engramId, options) {
22
- const sourcePath = resolveAgentPath(engramId, options?.openclawDir);
16
+ async execute(agentName, options) {
17
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
23
18
  if (!existsSync(sourcePath)) {
24
19
  throw new WorkspaceNotFoundError(sourcePath);
25
20
  }
21
+ // 既存Engramがあればエラー — Relic側が真のデータソース
22
+ const existing = await this.repository.get(agentName);
23
+ if (existing) {
24
+ throw new AlreadyExtractedError(agentName);
25
+ }
26
26
  const { files, filesRead } = await this.readFiles(sourcePath);
27
27
  if (filesRead.length === 0) {
28
28
  throw new WorkspaceEmptyError(sourcePath);
29
29
  }
30
- const existing = await this.repository.get(engramId);
31
- if (existing) {
32
- if (options?.force) {
33
- // --force: persona上書き + memoryマージ
34
- const merged = await this.mergeAndSave(existing, files, options.name ?? existing.meta.name);
35
- return {
36
- engramId,
37
- engramName: options.name ?? existing.meta.name,
38
- sourcePath,
39
- filesRead,
40
- memoryMerged: merged,
41
- };
42
- }
43
- // --forceなし: memoryのみマージ
44
- if (!files.memoryEntries || Object.keys(files.memoryEntries).length === 0) {
45
- throw new EngramAlreadyExistsError(engramId);
46
- }
47
- const merged = await this.mergeMemoryOnly(engramId, files.memoryEntries);
48
- return {
49
- engramId,
50
- engramName: existing.meta.name,
51
- sourcePath,
52
- filesRead,
53
- memoryMerged: merged,
54
- };
55
- }
56
- // 新規作成 — nameは必須
57
- const engramName = options?.name;
58
- if (!engramName) {
59
- throw new ExtractNameRequiredError(engramId);
60
- }
30
+ const engramName = options?.name ?? agentName;
61
31
  const now = new Date().toISOString();
62
32
  const engram = {
63
33
  meta: {
64
- id: engramId,
34
+ id: agentName,
65
35
  name: engramName,
66
- description: `Extracted from OpenClaw agent (${engramId})`,
36
+ description: `Extracted from OpenClaw workspace (${agentName})`,
67
37
  createdAt: now,
68
38
  updatedAt: now,
69
39
  tags: ["extracted", "openclaw"],
@@ -72,76 +42,16 @@ export class Extract {
72
42
  };
73
43
  await this.repository.save(engram);
74
44
  return {
75
- engramId,
45
+ engramId: agentName,
76
46
  engramName,
77
47
  sourcePath,
78
48
  filesRead,
79
- memoryMerged: false,
80
49
  };
81
50
  }
82
- /**
83
- * --force時: persona上書き + memoryマージ
84
- */
85
- async mergeAndSave(existing, newFiles, engramName) {
86
- const mergedFiles = { ...newFiles };
87
- // memoryEntriesをマージ
88
- let memoryMerged = false;
89
- if (newFiles.memoryEntries) {
90
- mergedFiles.memoryEntries = { ...existing.files.memoryEntries };
91
- for (const [date, content] of Object.entries(newFiles.memoryEntries)) {
92
- const existingContent = mergedFiles.memoryEntries?.[date];
93
- if (existingContent) {
94
- const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
95
- mergedFiles.memoryEntries[date] = existingContent + separator + content;
96
- }
97
- else {
98
- mergedFiles.memoryEntries = mergedFiles.memoryEntries ?? {};
99
- mergedFiles.memoryEntries[date] = content;
100
- }
101
- memoryMerged = true;
102
- }
103
- }
104
- const updatedEngram = {
105
- meta: {
106
- ...existing.meta,
107
- name: engramName,
108
- updatedAt: new Date().toISOString(),
109
- },
110
- files: mergedFiles,
111
- };
112
- await this.repository.save(updatedEngram);
113
- return memoryMerged;
114
- }
115
- /**
116
- * --forceなし時: memoryエントリのみ追記(repositoryのAPIを使用)
117
- */
118
- async mergeMemoryOnly(engramId, newEntries) {
119
- const existing = await this.repository.get(engramId);
120
- if (!existing)
121
- return false;
122
- const mergedEntries = { ...existing.files.memoryEntries };
123
- for (const [date, content] of Object.entries(newEntries)) {
124
- const existingContent = mergedEntries[date];
125
- if (existingContent) {
126
- const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
127
- mergedEntries[date] = existingContent + separator + content;
128
- }
129
- else {
130
- mergedEntries[date] = content;
131
- }
132
- }
133
- const updatedEngram = {
134
- ...existing,
135
- meta: { ...existing.meta, updatedAt: new Date().toISOString() },
136
- files: { ...existing.files, memoryEntries: mergedEntries },
137
- };
138
- await this.repository.save(updatedEngram);
139
- return true;
140
- }
141
51
  async readFiles(sourcePath) {
142
52
  const files = {};
143
53
  const filesRead = [];
144
- for (const [key, filename] of Object.entries(FILE_MAP)) {
54
+ for (const [key, filename] of Object.entries(RELIC_FILE_MAP)) {
145
55
  const filePath = join(sourcePath, filename);
146
56
  if (existsSync(filePath)) {
147
57
  files[key] = await readFile(filePath, "utf-8");
@@ -176,15 +86,9 @@ export class WorkspaceEmptyError extends Error {
176
86
  this.name = "WorkspaceEmptyError";
177
87
  }
178
88
  }
179
- export class EngramAlreadyExistsError extends Error {
180
- constructor(id) {
181
- super(`Engram "${id}" already exists. Use --force to overwrite.`);
182
- this.name = "EngramAlreadyExistsError";
183
- }
184
- }
185
- export class ExtractNameRequiredError extends Error {
89
+ export class AlreadyExtractedError extends Error {
186
90
  constructor(id) {
187
- super(`No existing Engram "${id}" found. --name is required for new Engrams.`);
188
- this.name = "ExtractNameRequiredError";
91
+ super(`Engram "${id}" already exists. Relic is the source of truth — use "relic claw inject" to push changes.`);
92
+ this.name = "AlreadyExtractedError";
189
93
  }
190
94
  }
@@ -1,9 +1,9 @@
1
1
  export { Summon, EngramNotFoundError, type SummonResult } from "./summon.js";
2
2
  export { ListEngrams } from "./list-engrams.js";
3
3
  export { Init, type InitResult } from "./init.js";
4
- export { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, type InjectResult, } from "./inject.js";
5
- export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, type ExtractResult, } from "./extract.js";
4
+ export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectResult, } from "./inject.js";
5
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractResult, } from "./extract.js";
6
6
  export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
7
- export { Sync, SyncAgentsDirNotFoundError, type SyncTarget, type SyncInitialResult, } from "./sync.js";
7
+ export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
8
8
  export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
9
9
  export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";
@@ -1,9 +1,9 @@
1
1
  export { Summon, EngramNotFoundError } from "./summon.js";
2
2
  export { ListEngrams } from "./list-engrams.js";
3
3
  export { Init } from "./init.js";
4
- export { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, } from "./inject.js";
5
- export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, } from "./extract.js";
4
+ export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, } from "./inject.js";
5
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, } from "./extract.js";
6
6
  export { MemoryWrite, MemoryWriteEngramNotFoundError, } from "./memory-write.js";
7
- export { Sync, SyncAgentsDirNotFoundError, } from "./sync.js";
7
+ export { Sync, SyncOpenclawDirNotFoundError, } from "./sync.js";
8
8
  export { ArchivePending, ArchivePendingEngramNotFoundError, } from "./archive-pending.js";
9
9
  export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, } from "./archive-cursor-update.js";
@@ -8,7 +8,8 @@ export interface InjectResult {
8
8
  /**
9
9
  * Inject — EngramのファイルをOpenClawワークスペースに注入する
10
10
  *
11
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ に書き込む。
11
+ * OpenClawではエージェントごとに workspace-<name>/ を使い、
12
+ * デフォルト(main)エージェントのみ workspace/ を使う。
12
13
  * memoryEntries はOpenClaw側の管理に委ねるため注入しない。
13
14
  */
14
15
  export declare class Inject {
@@ -17,12 +18,16 @@ export declare class Inject {
17
18
  execute(engramId: string, options?: {
18
19
  to?: string;
19
20
  openclawDir?: string;
21
+ mergeIdentity?: boolean;
20
22
  }): Promise<InjectResult>;
21
23
  private writeFiles;
22
24
  }
23
25
  export declare class InjectEngramNotFoundError extends Error {
24
26
  constructor(id: string);
25
27
  }
26
- export declare class InjectAgentNotFoundError extends Error {
27
- constructor(engramId: string, path: string);
28
+ export declare class InjectClawDirNotFoundError extends Error {
29
+ constructor(path: string);
30
+ }
31
+ export declare class InjectWorkspaceNotFoundError extends Error {
32
+ constructor(engramId: string);
28
33
  }
@@ -1,11 +1,12 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { writeFile } from "node:fs/promises";
4
- import { FILE_MAP, resolveAgentPath } from "../../shared/openclaw.js";
4
+ import { INJECT_FILE_MAP, resolveWorkspacePath } from "../../shared/openclaw.js";
5
5
  /**
6
6
  * Inject — EngramのファイルをOpenClawワークスペースに注入する
7
7
  *
8
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ に書き込む。
8
+ * OpenClawではエージェントごとに workspace-<name>/ を使い、
9
+ * デフォルト(main)エージェントのみ workspace/ を使う。
9
10
  * memoryEntries はOpenClaw側の管理に委ねるため注入しない。
10
11
  */
11
12
  export class Inject {
@@ -18,12 +19,16 @@ export class Inject {
18
19
  if (!engram) {
19
20
  throw new InjectEngramNotFoundError(engramId);
20
21
  }
22
+ // ベースディレクトリ(--dir)の存在チェック
23
+ if (options?.openclawDir && !existsSync(options.openclawDir)) {
24
+ throw new InjectClawDirNotFoundError(options.openclawDir);
25
+ }
21
26
  const agentName = options?.to ?? engramId;
22
- const targetPath = resolveAgentPath(agentName, options?.openclawDir);
27
+ const targetPath = resolveWorkspacePath(agentName, options?.openclawDir);
23
28
  if (!existsSync(targetPath)) {
24
- throw new InjectAgentNotFoundError(agentName, targetPath);
29
+ throw new InjectWorkspaceNotFoundError(agentName);
25
30
  }
26
- const filesWritten = await this.writeFiles(targetPath, engram.files);
31
+ const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
27
32
  return {
28
33
  engramId: engram.meta.id,
29
34
  engramName: engram.meta.name,
@@ -31,10 +36,18 @@ export class Inject {
31
36
  filesWritten,
32
37
  };
33
38
  }
34
- async writeFiles(targetPath, files) {
39
+ async writeFiles(targetPath, files, mergeIdentity) {
35
40
  const written = [];
36
- for (const [key, filename] of Object.entries(FILE_MAP)) {
37
- const content = files[key];
41
+ for (const [key, filename] of Object.entries(INJECT_FILE_MAP)) {
42
+ // --merge-identity: skip IDENTITY.md (merged into SOUL.md below)
43
+ if (mergeIdentity && key === "identity") {
44
+ continue;
45
+ }
46
+ let content = files[key];
47
+ // --merge-identity: append IDENTITY.md content to SOUL.md
48
+ if (mergeIdentity && key === "soul" && files.identity) {
49
+ content = content + "\n" + files.identity;
50
+ }
38
51
  if (content !== undefined) {
39
52
  await writeFile(join(targetPath, filename), content, "utf-8");
40
53
  written.push(filename);
@@ -49,9 +62,15 @@ export class InjectEngramNotFoundError extends Error {
49
62
  this.name = "InjectEngramNotFoundError";
50
63
  }
51
64
  }
52
- export class InjectAgentNotFoundError extends Error {
53
- constructor(engramId, path) {
54
- super(`OpenClaw agent "${engramId}" not found at ${path}. The agent must exist before injecting.`);
55
- this.name = "InjectAgentNotFoundError";
65
+ export class InjectClawDirNotFoundError extends Error {
66
+ constructor(path) {
67
+ super(`Claw directory not found at ${path}`);
68
+ this.name = "InjectClawDirNotFoundError";
69
+ }
70
+ }
71
+ export class InjectWorkspaceNotFoundError extends Error {
72
+ constructor(engramId) {
73
+ super(`OpenClaw agent "${engramId}" has not been created yet. Run "openclaw agents add ${engramId}" first, then try again.`);
74
+ this.name = "InjectWorkspaceNotFoundError";
56
75
  }
57
76
  }
@@ -1,40 +1,64 @@
1
1
  import type { EngramRepository } from "../ports/engram-repository.js";
2
2
  export interface SyncTarget {
3
3
  engramId: string;
4
- agentPath: string;
5
- hasEngram: boolean;
4
+ workspacePath: string;
5
+ }
6
+ export interface SyncResult {
7
+ engramId: string;
8
+ memoryFilesMerged: number;
9
+ memoryIndexMerged: boolean;
10
+ userMerged: boolean;
6
11
  }
7
12
  export interface SyncInitialResult {
8
- injected: string[];
9
- extracted: string[];
10
- targets: SyncTarget[];
13
+ synced: SyncResult[];
14
+ skipped: string[];
11
15
  }
12
16
  /**
13
- * Sync — OpenClawのagentsディレクトリをスキャンし、
14
- * 一致するEngramをinject / memoryをextractする。
17
+ * Sync — Relic Engram と OpenClaw workspace 間で memory を双方向マージする。
15
18
  *
16
- * 初回スキャン後は、呼び出し側がファイル監視を行い、
17
- * 変更検知時に syncMemory() を呼ぶ。
19
+ * 対象: 同名の engram/agent が両方に存在するペアのみ。
20
+ * マージ対象: memory/*.md と MEMORY.md
21
+ * マージ結果は両方に書き戻される。
18
22
  */
19
23
  export declare class Sync {
20
24
  private readonly repository;
21
- private readonly inject;
22
- private readonly extract;
23
- constructor(repository: EngramRepository);
25
+ private readonly engramsPath;
26
+ constructor(repository: EngramRepository, engramsPath: string);
27
+ execute(openclawDir?: string): Promise<SyncInitialResult>;
28
+ /**
29
+ * 1ペア分の memory マージ(inject 後の自動 sync 等でも使用)
30
+ */
31
+ syncPair(target: SyncTarget): Promise<SyncResult>;
32
+ /**
33
+ * memory/*.md を双方向マージ
34
+ */
35
+ private mergeMemoryEntries;
36
+ /**
37
+ * MEMORY.md を双方向マージ
38
+ */
39
+ private mergeMemoryIndex;
40
+ /**
41
+ * 単一ファイルの双方向マージ(MEMORY.md, USER.md 等)
42
+ */
43
+ private mergeSingleFile;
44
+ /**
45
+ * 2つのテキスト内容をマージする。
46
+ * 重複行を除外しつつ、両方の内容を結合する。
47
+ */
48
+ private mergeContents;
24
49
  /**
25
- * OpenClawのagentsディレクトリをスキャンし、
26
- * Engramが存在するagentにはinject、全agentからmemoryをextractする。
50
+ * memory/ ディレクトリから日付 → 内容のマップを読む
27
51
  */
28
- initialSync(openclawDir?: string): Promise<SyncInitialResult>;
52
+ private readMemoryDir;
29
53
  /**
30
- * 特定agentのmemoryを同期(ファイル変更検知時に呼ばれる)
54
+ * 同名の engram/agent が両方に存在するペアを返す
31
55
  */
32
- syncMemory(engramId: string, openclawDir?: string): Promise<void>;
56
+ private scanMatchingPairs;
33
57
  /**
34
- * agentsディレクトリをスキャンしてターゲット一覧を返す
58
+ * 全 workspace のエージェント名一覧
35
59
  */
36
- private scanAgents;
60
+ private scanAllWorkspaces;
37
61
  }
38
- export declare class SyncAgentsDirNotFoundError extends Error {
62
+ export declare class SyncOpenclawDirNotFoundError extends Error {
39
63
  constructor(path: string);
40
64
  }