@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,147 +1,111 @@
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 inspectPersona(agentName, options) {
17
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
23
18
  if (!existsSync(sourcePath)) {
24
19
  throw new WorkspaceNotFoundError(sourcePath);
25
20
  }
26
- const { files, filesRead } = await this.readFiles(sourcePath);
27
- if (filesRead.length === 0) {
28
- throw new WorkspaceEmptyError(sourcePath);
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);
21
+ const { files } = await this.readFiles(sourcePath);
22
+ const existing = await this.repository.get(agentName);
23
+ if (!existing) {
48
24
  return {
49
- engramId,
50
- engramName: existing.meta.name,
25
+ engramId: agentName,
26
+ engramName: options?.name ?? agentName,
51
27
  sourcePath,
52
- filesRead,
53
- memoryMerged: merged,
28
+ existing: false,
29
+ name: "missing",
30
+ soul: "missing",
31
+ identity: "missing",
32
+ overwriteRequired: false,
54
33
  };
55
34
  }
56
- // 新規作成 nameは必須
57
- const engramName = options?.name;
58
- if (!engramName) {
59
- throw new ExtractNameRequiredError(engramId);
35
+ const requestedName = options?.name ?? existing.meta.name;
36
+ const name = requestedName === existing.meta.name ? "same" : "different";
37
+ const soul = this.comparePersonaFile(existing.files.soul, files.soul);
38
+ const identity = this.comparePersonaFile(existing.files.identity, files.identity);
39
+ return {
40
+ engramId: agentName,
41
+ engramName: existing.meta.name,
42
+ sourcePath,
43
+ existing: true,
44
+ name,
45
+ soul,
46
+ identity,
47
+ overwriteRequired: name === "different" ||
48
+ soul === "different" ||
49
+ identity === "different",
50
+ };
51
+ }
52
+ async execute(agentName, options) {
53
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
54
+ if (!existsSync(sourcePath)) {
55
+ throw new WorkspaceNotFoundError(sourcePath);
56
+ }
57
+ const existing = await this.repository.get(agentName);
58
+ if (existing && !options?.force) {
59
+ throw new AlreadyExtractedError(agentName);
60
+ }
61
+ const { files, filesRead } = await this.readFiles(sourcePath);
62
+ if (filesRead.length === 0) {
63
+ throw new WorkspaceEmptyError(sourcePath);
60
64
  }
61
65
  const now = new Date().toISOString();
62
- const engram = {
63
- meta: {
64
- id: engramId,
65
- name: engramName,
66
- description: `Extracted from OpenClaw agent (${engramId})`,
67
- createdAt: now,
68
- updatedAt: now,
69
- tags: ["extracted", "openclaw"],
70
- },
71
- files,
72
- };
66
+ const engram = existing && options?.force
67
+ ? {
68
+ meta: {
69
+ ...existing.meta,
70
+ name: options?.name ?? existing.meta.name,
71
+ updatedAt: now,
72
+ },
73
+ // Force extract only replaces persona files from Claw.
74
+ files: {
75
+ ...existing.files,
76
+ soul: files.soul ?? existing.files.soul,
77
+ identity: files.identity ?? existing.files.identity,
78
+ },
79
+ }
80
+ : {
81
+ meta: {
82
+ id: agentName,
83
+ name: options?.name ?? agentName,
84
+ description: `Extracted from OpenClaw workspace (${agentName})`,
85
+ createdAt: now,
86
+ updatedAt: now,
87
+ tags: ["extracted", "openclaw"],
88
+ },
89
+ files,
90
+ };
73
91
  await this.repository.save(engram);
74
92
  return {
75
- engramId,
76
- engramName,
93
+ engramId: agentName,
94
+ engramName: engram.meta.name,
77
95
  sourcePath,
78
96
  filesRead,
79
- memoryMerged: false,
80
- };
81
- }
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
97
  };
112
- await this.repository.save(updatedEngram);
113
- return memoryMerged;
114
98
  }
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
- }
99
+ comparePersonaFile(currentContent, incomingContent) {
100
+ if (currentContent === undefined || incomingContent === undefined) {
101
+ return "missing";
132
102
  }
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;
103
+ return currentContent === incomingContent ? "same" : "different";
140
104
  }
141
105
  async readFiles(sourcePath) {
142
106
  const files = {};
143
107
  const filesRead = [];
144
- for (const [key, filename] of Object.entries(FILE_MAP)) {
108
+ for (const [key, filename] of Object.entries(RELIC_FILE_MAP)) {
145
109
  const filePath = join(sourcePath, filename);
146
110
  if (existsSync(filePath)) {
147
111
  files[key] = await readFile(filePath, "utf-8");
@@ -176,15 +140,9 @@ export class WorkspaceEmptyError extends Error {
176
140
  this.name = "WorkspaceEmptyError";
177
141
  }
178
142
  }
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 {
143
+ export class AlreadyExtractedError extends Error {
186
144
  constructor(id) {
187
- super(`No existing Engram "${id}" found. --name is required for new Engrams.`);
188
- this.name = "ExtractNameRequiredError";
145
+ super(`Engram "${id}" already exists. Re-run with "--force" option to overwrite local persona files from the Claw workspace.`);
146
+ this.name = "AlreadyExtractedError";
189
147
  }
190
148
  }
@@ -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 InjectPersonaFileDiff, type InjectPersonaDiffResult, type InjectResult, } from "./inject.js";
5
+ export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractPersonaFileDiff, type ExtractPersonaDiffResult, 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";
@@ -1,4 +1,13 @@
1
1
  import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export type InjectPersonaFileDiff = "missing" | "same" | "different" | "skipped";
3
+ export interface InjectPersonaDiffResult {
4
+ engramId: string;
5
+ engramName: string;
6
+ targetPath: string;
7
+ soul: InjectPersonaFileDiff;
8
+ identity: InjectPersonaFileDiff;
9
+ overwriteRequired: boolean;
10
+ }
2
11
  export interface InjectResult {
3
12
  engramId: string;
4
13
  engramName: string;
@@ -8,21 +17,32 @@ export interface InjectResult {
8
17
  /**
9
18
  * Inject — EngramのファイルをOpenClawワークスペースに注入する
10
19
  *
11
- * agent名 = Engram ID の規約に基づき、agents/<engramId>/agent/ に書き込む。
20
+ * OpenClawではエージェントごとに workspace-<name>/ を使い、
21
+ * デフォルト(main)エージェントのみ workspace/ を使う。
12
22
  * memoryEntries はOpenClaw側の管理に委ねるため注入しない。
13
23
  */
14
24
  export declare class Inject {
15
25
  private readonly repository;
16
26
  constructor(repository: EngramRepository);
27
+ inspectPersona(engramId: string, options?: {
28
+ openclawDir?: string;
29
+ mergeIdentity?: boolean;
30
+ }): Promise<InjectPersonaDiffResult>;
17
31
  execute(engramId: string, options?: {
18
- to?: string;
19
32
  openclawDir?: string;
33
+ mergeIdentity?: boolean;
20
34
  }): Promise<InjectResult>;
35
+ private loadInjectTarget;
36
+ private resolveInjectedContent;
37
+ private compareTargetFile;
21
38
  private writeFiles;
22
39
  }
23
40
  export declare class InjectEngramNotFoundError extends Error {
24
41
  constructor(id: string);
25
42
  }
26
- export declare class InjectAgentNotFoundError extends Error {
27
- constructor(engramId: string, path: string);
43
+ export declare class InjectClawDirNotFoundError extends Error {
44
+ constructor(path: string);
45
+ }
46
+ export declare class InjectWorkspaceNotFoundError extends Error {
47
+ constructor(engramId: string);
28
48
  }
@@ -1,11 +1,12 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { writeFile } from "node:fs/promises";
4
- import { FILE_MAP, resolveAgentPath } from "../../shared/openclaw.js";
3
+ import { readFile, writeFile } from "node:fs/promises";
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 {
@@ -13,17 +14,25 @@ export class Inject {
13
14
  constructor(repository) {
14
15
  this.repository = repository;
15
16
  }
17
+ async inspectPersona(engramId, options) {
18
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
19
+ const mergeIdentity = options?.mergeIdentity ?? false;
20
+ const soul = await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.soul), this.resolveInjectedContent("soul", engram.files, mergeIdentity));
21
+ const identity = mergeIdentity
22
+ ? "skipped"
23
+ : await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.identity), this.resolveInjectedContent("identity", engram.files, mergeIdentity));
24
+ return {
25
+ engramId: engram.meta.id,
26
+ engramName: engram.meta.name,
27
+ targetPath,
28
+ soul,
29
+ identity,
30
+ overwriteRequired: soul === "different" || identity === "different",
31
+ };
32
+ }
16
33
  async execute(engramId, options) {
17
- const engram = await this.repository.get(engramId);
18
- if (!engram) {
19
- throw new InjectEngramNotFoundError(engramId);
20
- }
21
- const agentName = options?.to ?? engramId;
22
- const targetPath = resolveAgentPath(agentName, options?.openclawDir);
23
- if (!existsSync(targetPath)) {
24
- throw new InjectAgentNotFoundError(agentName, targetPath);
25
- }
26
- const filesWritten = await this.writeFiles(targetPath, engram.files);
34
+ const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
35
+ const filesWritten = await this.writeFiles(targetPath, engram.files, options?.mergeIdentity ?? false);
27
36
  return {
28
37
  engramId: engram.meta.id,
29
38
  engramName: engram.meta.name,
@@ -31,10 +40,48 @@ export class Inject {
31
40
  filesWritten,
32
41
  };
33
42
  }
34
- async writeFiles(targetPath, files) {
43
+ async loadInjectTarget(engramId, options) {
44
+ const engram = await this.repository.get(engramId);
45
+ if (!engram) {
46
+ throw new InjectEngramNotFoundError(engramId);
47
+ }
48
+ if (options?.openclawDir && !existsSync(options.openclawDir)) {
49
+ throw new InjectClawDirNotFoundError(options.openclawDir);
50
+ }
51
+ const targetPath = resolveWorkspacePath(engramId, options?.openclawDir);
52
+ if (!existsSync(targetPath)) {
53
+ throw new InjectWorkspaceNotFoundError(engramId);
54
+ }
55
+ return { engram, targetPath };
56
+ }
57
+ resolveInjectedContent(key, files, mergeIdentity) {
58
+ if (mergeIdentity && key === "identity") {
59
+ return undefined;
60
+ }
61
+ let content = files[key];
62
+ if (mergeIdentity && key === "soul" && files.identity) {
63
+ content = content + "\n" + files.identity;
64
+ }
65
+ return content;
66
+ }
67
+ async compareTargetFile(filePath, expectedContent) {
68
+ if (expectedContent === undefined) {
69
+ return "skipped";
70
+ }
71
+ if (!existsSync(filePath)) {
72
+ return "missing";
73
+ }
74
+ const currentContent = await readFile(filePath, "utf-8");
75
+ return currentContent === expectedContent ? "same" : "different";
76
+ }
77
+ async writeFiles(targetPath, files, mergeIdentity) {
35
78
  const written = [];
36
- for (const [key, filename] of Object.entries(FILE_MAP)) {
37
- const content = files[key];
79
+ for (const [key, filename] of Object.entries(INJECT_FILE_MAP)) {
80
+ // --merge-identity: skip IDENTITY.md (merged into SOUL.md below)
81
+ if (mergeIdentity && key === "identity") {
82
+ continue;
83
+ }
84
+ const content = this.resolveInjectedContent(key, files, mergeIdentity);
38
85
  if (content !== undefined) {
39
86
  await writeFile(join(targetPath, filename), content, "utf-8");
40
87
  written.push(filename);
@@ -49,9 +96,15 @@ export class InjectEngramNotFoundError extends Error {
49
96
  this.name = "InjectEngramNotFoundError";
50
97
  }
51
98
  }
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";
99
+ export class InjectClawDirNotFoundError extends Error {
100
+ constructor(path) {
101
+ super(`Claw directory not found at ${path}`);
102
+ this.name = "InjectClawDirNotFoundError";
103
+ }
104
+ }
105
+ export class InjectWorkspaceNotFoundError extends Error {
106
+ constructor(engramId) {
107
+ super(`OpenClaw agent "${engramId}" has not been created yet. Run "openclaw agents add ${engramId}" first, then try again.`);
108
+ this.name = "InjectWorkspaceNotFoundError";
56
109
  }
57
110
  }
@@ -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
  }