@ectplsm/relic 0.1.4 → 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.
- package/README.md +158 -99
- package/dist/core/usecases/extract.d.ts +5 -23
- package/dist/core/usecases/extract.js +19 -115
- package/dist/core/usecases/index.d.ts +3 -3
- package/dist/core/usecases/index.js +3 -3
- package/dist/core/usecases/inject.d.ts +8 -3
- package/dist/core/usecases/inject.js +31 -12
- package/dist/core/usecases/sync.d.ts +44 -20
- package/dist/core/usecases/sync.js +182 -56
- package/dist/interfaces/cli/commands/claw.d.ts +2 -0
- package/dist/interfaces/cli/commands/claw.js +149 -0
- package/dist/interfaces/cli/commands/config.js +6 -6
- package/dist/interfaces/cli/commands/extract.js +7 -12
- package/dist/interfaces/cli/commands/inject.js +2 -2
- package/dist/interfaces/cli/commands/sync.js +24 -68
- package/dist/interfaces/cli/index.js +2 -6
- package/dist/interfaces/mcp/index.js +14 -0
- package/dist/shared/config.d.ts +7 -7
- package/dist/shared/config.js +18 -58
- package/dist/shared/engram-composer.js +5 -3
- package/dist/shared/openclaw.d.ts +25 -3
- package/dist/shared/openclaw.js +48 -4
- package/package.json +3 -2
- package/templates/engrams/johnny/IDENTITY.md +25 -0
- package/templates/engrams/johnny/SOUL.md +35 -0
- package/templates/engrams/motoko/IDENTITY.md +25 -0
- package/templates/engrams/motoko/SOUL.md +38 -0
|
@@ -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,
|
|
5
|
-
export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError,
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
27
|
-
constructor(
|
|
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 {
|
|
4
|
+
import { INJECT_FILE_MAP, resolveWorkspacePath } from "../../shared/openclaw.js";
|
|
5
5
|
/**
|
|
6
6
|
* Inject — EngramのファイルをOpenClawワークスペースに注入する
|
|
7
7
|
*
|
|
8
|
-
*
|
|
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 =
|
|
27
|
+
const targetPath = resolveWorkspacePath(agentName, options?.openclawDir);
|
|
23
28
|
if (!existsSync(targetPath)) {
|
|
24
|
-
throw new
|
|
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(
|
|
37
|
-
|
|
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
|
|
53
|
-
constructor(
|
|
54
|
-
super(`
|
|
55
|
-
this.name = "
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
targets: SyncTarget[];
|
|
13
|
+
synced: SyncResult[];
|
|
14
|
+
skipped: string[];
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
|
-
* Sync — OpenClaw
|
|
14
|
-
* 一致するEngramをinject / memoryをextractする。
|
|
17
|
+
* Sync — Relic Engram と OpenClaw workspace 間で memory を双方向マージする。
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
*
|
|
26
|
-
* Engramが存在するagentにはinject、全agentからmemoryをextractする。
|
|
50
|
+
* memory/ ディレクトリから日付 → 内容のマップを読む
|
|
27
51
|
*/
|
|
28
|
-
|
|
52
|
+
private readMemoryDir;
|
|
29
53
|
/**
|
|
30
|
-
*
|
|
54
|
+
* 同名の engram/agent が両方に存在するペアを返す
|
|
31
55
|
*/
|
|
32
|
-
|
|
56
|
+
private scanMatchingPairs;
|
|
33
57
|
/**
|
|
34
|
-
*
|
|
58
|
+
* 全 workspace のエージェント名一覧
|
|
35
59
|
*/
|
|
36
|
-
private
|
|
60
|
+
private scanAllWorkspaces;
|
|
37
61
|
}
|
|
38
|
-
export declare class
|
|
62
|
+
export declare class SyncOpenclawDirNotFoundError extends Error {
|
|
39
63
|
constructor(path: string);
|
|
40
64
|
}
|
|
@@ -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 {
|
|
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
|
|
10
|
-
* 一致するEngramをinject / memoryをextractする。
|
|
9
|
+
* Sync — Relic Engram と OpenClaw workspace 間で memory を双方向マージする。
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* 対象: 同名の engram/agent が両方に存在するペアのみ。
|
|
12
|
+
* マージ対象: memory/*.md と MEMORY.md
|
|
13
|
+
* マージ結果は両方に書き戻される。
|
|
14
14
|
*/
|
|
15
15
|
export class Sync {
|
|
16
16
|
repository;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
constructor(repository) {
|
|
17
|
+
engramsPath;
|
|
18
|
+
constructor(repository, engramsPath) {
|
|
20
19
|
this.repository = repository;
|
|
21
|
-
this.
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
throw new SyncAgentsDirNotFoundError(agentsDir);
|
|
24
|
+
if (!existsSync(baseDir)) {
|
|
25
|
+
throw new SyncOpenclawDirNotFoundError(baseDir);
|
|
33
26
|
}
|
|
34
|
-
const targets = await this.
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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.
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
extracted.push(target.engramId);
|
|
32
|
+
const result = await this.syncPair(target);
|
|
33
|
+
synced.push(result);
|
|
54
34
|
}
|
|
55
35
|
catch {
|
|
56
|
-
|
|
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
|
|
106
|
+
return mergedCount;
|
|
60
107
|
}
|
|
61
108
|
/**
|
|
62
|
-
*
|
|
109
|
+
* MEMORY.md を双方向マージ
|
|
63
110
|
*/
|
|
64
|
-
async
|
|
65
|
-
|
|
111
|
+
async mergeMemoryIndex(relicDir, openclawDir) {
|
|
112
|
+
return this.mergeSingleFile(relicDir, openclawDir, MEMORY_INDEX);
|
|
66
113
|
}
|
|
67
114
|
/**
|
|
68
|
-
*
|
|
115
|
+
* 単一ファイルの双方向マージ(MEMORY.md, USER.md 等)
|
|
69
116
|
*/
|
|
70
|
-
async
|
|
71
|
-
const
|
|
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
|
|
77
|
-
if (!
|
|
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:
|
|
82
|
-
|
|
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
|
|
215
|
+
export class SyncOpenclawDirNotFoundError extends Error {
|
|
90
216
|
constructor(path) {
|
|
91
|
-
super(`OpenClaw
|
|
92
|
-
this.name = "
|
|
217
|
+
super(`OpenClaw directory not found at ${path}`);
|
|
218
|
+
this.name = "SyncOpenclawDirNotFoundError";
|
|
93
219
|
}
|
|
94
220
|
}
|
|
@@ -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
|
|
46
|
+
// relic config claw-path [path]
|
|
47
47
|
config
|
|
48
|
-
.command("
|
|
49
|
-
.description("Get or set the
|
|
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.
|
|
55
|
+
console.log(cfg.clawPath ?? "(not set — using ~/.openclaw)");
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
cfg.
|
|
58
|
+
cfg.clawPath = path;
|
|
59
59
|
await saveConfig(cfg);
|
|
60
|
-
console.log(`
|
|
60
|
+
console.log(`Claw path set to: ${path}`);
|
|
61
61
|
});
|
|
62
62
|
// relic config memory-window [n]
|
|
63
63
|
config
|