@ectplsm/relic 0.3.2 → 0.4.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 (60) hide show
  1. package/LICENCE.md +1 -1
  2. package/README.md +19 -12
  3. package/dist/adapters/mikoshi/client.d.ts +21 -0
  4. package/dist/adapters/mikoshi/client.js +84 -0
  5. package/dist/core/ports/index.d.ts +1 -0
  6. package/dist/core/ports/mikoshi.d.ts +937 -0
  7. package/dist/core/ports/mikoshi.js +209 -0
  8. package/dist/core/sync/crypto.d.ts +55 -0
  9. package/dist/core/sync/crypto.js +189 -0
  10. package/dist/core/sync/memory-hash.d.ts +35 -0
  11. package/dist/core/sync/memory-hash.js +61 -0
  12. package/dist/core/sync/memory-merge.d.ts +13 -0
  13. package/dist/core/sync/memory-merge.js +84 -0
  14. package/dist/core/sync/normalize.d.ts +10 -0
  15. package/dist/core/sync/normalize.js +21 -0
  16. package/dist/core/sync/persona-hash.d.ts +15 -0
  17. package/dist/core/sync/persona-hash.js +36 -0
  18. package/dist/core/usecases/claw-pull.d.ts +44 -0
  19. package/dist/core/usecases/claw-pull.js +117 -0
  20. package/dist/core/usecases/extract.d.ts +0 -1
  21. package/dist/core/usecases/extract.js +13 -27
  22. package/dist/core/usecases/index.d.ts +8 -0
  23. package/dist/core/usecases/index.js +8 -0
  24. package/dist/core/usecases/inject.d.ts +1 -0
  25. package/dist/core/usecases/inject.js +2 -0
  26. package/dist/core/usecases/mikoshi-clone.d.ts +29 -0
  27. package/dist/core/usecases/mikoshi-clone.js +94 -0
  28. package/dist/core/usecases/mikoshi-download.d.ts +29 -0
  29. package/dist/core/usecases/mikoshi-download.js +94 -0
  30. package/dist/core/usecases/mikoshi-memory-pull.d.ts +38 -0
  31. package/dist/core/usecases/mikoshi-memory-pull.js +142 -0
  32. package/dist/core/usecases/mikoshi-memory-push.d.ts +34 -0
  33. package/dist/core/usecases/mikoshi-memory-push.js +105 -0
  34. package/dist/core/usecases/mikoshi-memory-sync.d.ts +36 -0
  35. package/dist/core/usecases/mikoshi-memory-sync.js +175 -0
  36. package/dist/core/usecases/mikoshi-pull.d.ts +44 -0
  37. package/dist/core/usecases/mikoshi-pull.js +124 -0
  38. package/dist/core/usecases/mikoshi-push.d.ts +39 -0
  39. package/dist/core/usecases/mikoshi-push.js +127 -0
  40. package/dist/core/usecases/mikoshi-status.d.ts +37 -0
  41. package/dist/core/usecases/mikoshi-status.js +92 -0
  42. package/dist/core/usecases/sync.d.ts +0 -5
  43. package/dist/core/usecases/sync.js +23 -61
  44. package/dist/interfaces/cli/commands/claw.js +134 -85
  45. package/dist/interfaces/cli/commands/config.js +59 -0
  46. package/dist/interfaces/cli/commands/create.js +17 -16
  47. package/dist/interfaces/cli/commands/init.js +11 -10
  48. package/dist/interfaces/cli/commands/list.js +9 -7
  49. package/dist/interfaces/cli/commands/migrate.js +8 -7
  50. package/dist/interfaces/cli/commands/mikoshi.d.ts +2 -0
  51. package/dist/interfaces/cli/commands/mikoshi.js +565 -0
  52. package/dist/interfaces/cli/commands/refresh-samples.js +10 -9
  53. package/dist/interfaces/cli/index.js +2 -0
  54. package/dist/interfaces/cli/output.d.ts +5 -0
  55. package/dist/interfaces/cli/output.js +16 -0
  56. package/dist/interfaces/cli/spinner.d.ts +13 -0
  57. package/dist/interfaces/cli/spinner.js +47 -0
  58. package/dist/shared/config.d.ts +23 -3
  59. package/dist/shared/config.js +30 -2
  60. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient } from "../ports/mikoshi.js";
3
+ export type MikoshiPushOutcome = "create_required" | "push_required" | "already_synced";
4
+ export interface MikoshiPushResult {
5
+ outcome: MikoshiPushOutcome;
6
+ engramId: string;
7
+ engramName: string;
8
+ /** クラウド側の canonical ID (existing remote only) */
9
+ cloudEngramId?: string;
10
+ remotePersonaHash?: string | null;
11
+ }
12
+ export interface MikoshiPushApplyResult {
13
+ action: "created" | "updated";
14
+ cloudEngramId: string;
15
+ newPersonaHash?: string;
16
+ }
17
+ export declare class MikoshiPushEngramNotFoundError extends Error {
18
+ readonly engramId: string;
19
+ constructor(engramId: string);
20
+ }
21
+ export declare class MikoshiPushPersonaHashError extends Error {
22
+ readonly engramId: string;
23
+ constructor(engramId: string);
24
+ }
25
+ export declare class MikoshiPushPersonaConflictError extends Error {
26
+ readonly engramId: string;
27
+ readonly conflictRemoteHash?: string | undefined;
28
+ constructor(engramId: string, conflictRemoteHash?: string | undefined);
29
+ }
30
+ export declare class MikoshiPush {
31
+ private readonly localRepo;
32
+ private readonly mikoshi;
33
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
34
+ check(engramId: string): Promise<{
35
+ result: MikoshiPushResult;
36
+ apply?: () => Promise<MikoshiPushApplyResult>;
37
+ }>;
38
+ execute(engramId: string): Promise<MikoshiPushApplyResult | undefined>;
39
+ }
@@ -0,0 +1,127 @@
1
+ import { MikoshiApiError } from "../ports/mikoshi.js";
2
+ import { computePersonaHash } from "../sync/persona-hash.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Errors
5
+ // ---------------------------------------------------------------------------
6
+ export class MikoshiPushEngramNotFoundError extends Error {
7
+ engramId;
8
+ constructor(engramId) {
9
+ super(`Engram "${engramId}" not found locally`);
10
+ this.engramId = engramId;
11
+ this.name = "MikoshiPushEngramNotFoundError";
12
+ }
13
+ }
14
+ export class MikoshiPushPersonaHashError extends Error {
15
+ engramId;
16
+ constructor(engramId) {
17
+ super(`Cannot compute persona hash for "${engramId}" — SOUL.md or IDENTITY.md is missing`);
18
+ this.engramId = engramId;
19
+ this.name = "MikoshiPushPersonaHashError";
20
+ }
21
+ }
22
+ export class MikoshiPushPersonaConflictError extends Error {
23
+ engramId;
24
+ conflictRemoteHash;
25
+ constructor(engramId, conflictRemoteHash) {
26
+ super(`Persona conflict for "${engramId}"`);
27
+ this.engramId = engramId;
28
+ this.conflictRemoteHash = conflictRemoteHash;
29
+ this.name = "MikoshiPushPersonaConflictError";
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Usecase
34
+ // ---------------------------------------------------------------------------
35
+ export class MikoshiPush {
36
+ localRepo;
37
+ mikoshi;
38
+ constructor(localRepo, mikoshi) {
39
+ this.localRepo = localRepo;
40
+ this.mikoshi = mikoshi;
41
+ }
42
+ async check(engramId) {
43
+ const local = await this.localRepo.get(engramId);
44
+ if (!local)
45
+ throw new MikoshiPushEngramNotFoundError(engramId);
46
+ const { soul, identity } = local.files;
47
+ const localHash = computePersonaHash(soul, identity);
48
+ if (!localHash)
49
+ throw new MikoshiPushPersonaHashError(engramId);
50
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
51
+ if (!cloudEngram) {
52
+ return {
53
+ result: {
54
+ outcome: "create_required",
55
+ engramId,
56
+ engramName: local.meta.name,
57
+ remotePersonaHash: null,
58
+ },
59
+ apply: async () => {
60
+ const created = await this.mikoshi.createEngram({
61
+ name: local.meta.name,
62
+ sourceEngramId: engramId,
63
+ description: local.meta.description,
64
+ tags: local.meta.tags,
65
+ soul,
66
+ identity,
67
+ });
68
+ return {
69
+ action: "created",
70
+ cloudEngramId: created.id,
71
+ };
72
+ },
73
+ };
74
+ }
75
+ const syncStatus = await this.mikoshi.getSyncStatus(cloudEngram.id);
76
+ const remoteHash = syncStatus.persona.token?.hash ?? null;
77
+ if (remoteHash && localHash === remoteHash) {
78
+ return {
79
+ result: {
80
+ outcome: "already_synced",
81
+ engramId,
82
+ engramName: local.meta.name,
83
+ cloudEngramId: cloudEngram.id,
84
+ remotePersonaHash: remoteHash,
85
+ },
86
+ };
87
+ }
88
+ return {
89
+ result: {
90
+ outcome: "push_required",
91
+ engramId,
92
+ engramName: local.meta.name,
93
+ cloudEngramId: cloudEngram.id,
94
+ remotePersonaHash: remoteHash,
95
+ },
96
+ apply: async () => {
97
+ const expectedHash = remoteHash ?? "";
98
+ try {
99
+ const updated = await this.mikoshi.updatePersona(cloudEngram.id, {
100
+ soul,
101
+ identity,
102
+ expectedRemotePersonaHash: expectedHash,
103
+ });
104
+ return {
105
+ action: "updated",
106
+ cloudEngramId: cloudEngram.id,
107
+ newPersonaHash: updated.persona.hash,
108
+ };
109
+ }
110
+ catch (err) {
111
+ if (err instanceof MikoshiApiError && err.isConflict && err.code === "PERSONA_CONFLICT") {
112
+ const body = err.body;
113
+ throw new MikoshiPushPersonaConflictError(engramId, body?.currentPersona?.hash);
114
+ }
115
+ throw err;
116
+ }
117
+ },
118
+ };
119
+ }
120
+ async execute(engramId) {
121
+ const { result, apply } = await this.check(engramId);
122
+ if (result.outcome === "already_synced" || !apply) {
123
+ return undefined;
124
+ }
125
+ return apply();
126
+ }
127
+ }
@@ -0,0 +1,37 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient, SyncStatusResponse } from "../ports/mikoshi.js";
3
+ export type PersonaDriftStatus = "synced" | "local_differs" | "remote_only" | "unavailable";
4
+ export type MemoryDriftStatus = "synced" | "local_differs" | "not_uploaded" | "local_empty";
5
+ export interface MikoshiStatusResult {
6
+ engramId: string;
7
+ engramName: string;
8
+ /** Mikoshi 側の canonical ID */
9
+ cloudEngramId: string;
10
+ sourceEngramId: string;
11
+ persona: {
12
+ status: PersonaDriftStatus;
13
+ localHash: string | null;
14
+ remoteHash: string | null;
15
+ };
16
+ memory: {
17
+ status: MemoryDriftStatus;
18
+ localHash: string | null;
19
+ remoteHash: string | null;
20
+ remoteExists: boolean;
21
+ remoteSummary: SyncStatusResponse["memory"]["summary"] | null;
22
+ };
23
+ }
24
+ export declare class MikoshiStatusEngramNotFoundError extends Error {
25
+ readonly engramId: string;
26
+ constructor(engramId: string);
27
+ }
28
+ export declare class MikoshiStatusCloudNotFoundError extends Error {
29
+ readonly sourceEngramId: string;
30
+ constructor(sourceEngramId: string);
31
+ }
32
+ export declare class MikoshiStatus {
33
+ private readonly localRepo;
34
+ private readonly mikoshi;
35
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
36
+ execute(engramId: string): Promise<MikoshiStatusResult>;
37
+ }
@@ -0,0 +1,92 @@
1
+ import { computePersonaHash } from "../sync/persona-hash.js";
2
+ import { computeMemoryContentHash, collectMemoryFiles } from "../sync/memory-hash.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Errors
5
+ // ---------------------------------------------------------------------------
6
+ export class MikoshiStatusEngramNotFoundError extends Error {
7
+ engramId;
8
+ constructor(engramId) {
9
+ super(`Engram "${engramId}" not found locally`);
10
+ this.engramId = engramId;
11
+ this.name = "MikoshiStatusEngramNotFoundError";
12
+ }
13
+ }
14
+ export class MikoshiStatusCloudNotFoundError extends Error {
15
+ sourceEngramId;
16
+ constructor(sourceEngramId) {
17
+ super(`Engram "${sourceEngramId}" not found on Mikoshi`);
18
+ this.sourceEngramId = sourceEngramId;
19
+ this.name = "MikoshiStatusCloudNotFoundError";
20
+ }
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Usecase
24
+ // ---------------------------------------------------------------------------
25
+ export class MikoshiStatus {
26
+ localRepo;
27
+ mikoshi;
28
+ constructor(localRepo, mikoshi) {
29
+ this.localRepo = localRepo;
30
+ this.mikoshi = mikoshi;
31
+ }
32
+ async execute(engramId) {
33
+ // 1. ローカル Engram 読み込み
34
+ const local = await this.localRepo.get(engramId);
35
+ if (!local)
36
+ throw new MikoshiStatusEngramNotFoundError(engramId);
37
+ // 2. クラウド側を sourceEngramId で検索
38
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
39
+ if (!cloudEngram)
40
+ throw new MikoshiStatusCloudNotFoundError(engramId);
41
+ // 3. sync-status 取得
42
+ const syncStatus = await this.mikoshi.getSyncStatus(cloudEngram.id);
43
+ // 4. ローカル persona hash 計算
44
+ const localPersonaHash = computePersonaHash(local.files.soul, local.files.identity);
45
+ // 5. Persona drift 判定
46
+ const remotePersonaHash = syncStatus.persona.token?.hash ?? null;
47
+ const personaStatus = classifyPersonaDrift(localPersonaHash, remotePersonaHash);
48
+ // 6. ローカル memory content hash 計算
49
+ const localMemoryFiles = collectMemoryFiles(local.files);
50
+ const localMemoryHash = computeMemoryContentHash(localMemoryFiles);
51
+ // 7. Memory drift 判定
52
+ const remoteMemoryHash = syncStatus.memory.token?.memoryContentHash ?? null;
53
+ const memoryStatus = classifyMemoryDrift(localMemoryHash, remoteMemoryHash, syncStatus.memory.exists);
54
+ return {
55
+ engramId,
56
+ engramName: local.meta.name,
57
+ cloudEngramId: cloudEngram.id,
58
+ sourceEngramId: cloudEngram.sourceEngramId,
59
+ persona: {
60
+ status: personaStatus,
61
+ localHash: localPersonaHash,
62
+ remoteHash: remotePersonaHash,
63
+ },
64
+ memory: {
65
+ status: memoryStatus,
66
+ localHash: localMemoryHash,
67
+ remoteHash: remoteMemoryHash,
68
+ remoteExists: syncStatus.memory.exists,
69
+ remoteSummary: syncStatus.memory.summary,
70
+ },
71
+ };
72
+ }
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Drift classification helpers
76
+ // ---------------------------------------------------------------------------
77
+ function classifyPersonaDrift(localHash, remoteHash) {
78
+ if (!localHash)
79
+ return "unavailable";
80
+ if (!remoteHash)
81
+ return "remote_only";
82
+ return localHash === remoteHash ? "synced" : "local_differs";
83
+ }
84
+ function classifyMemoryDrift(localHash, remoteHash, remoteExists) {
85
+ if (!localHash)
86
+ return "local_empty";
87
+ if (!remoteExists)
88
+ return "not_uploaded";
89
+ if (!remoteHash)
90
+ return "not_uploaded";
91
+ return localHash === remoteHash ? "synced" : "local_differs";
92
+ }
@@ -41,11 +41,6 @@ export declare class Sync {
41
41
  * 単一ファイルの双方向マージ(MEMORY.md, USER.md 等)
42
42
  */
43
43
  private mergeSingleFile;
44
- /**
45
- * 2つのテキスト内容をマージする。
46
- * 重複行を除外しつつ、両方の内容を結合する。
47
- */
48
- private mergeContents;
49
44
  /**
50
45
  * memory/ ディレクトリから日付 → 内容のマップを読む
51
46
  */
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { mergeMemoryRecords } from "../sync/memory-merge.js";
5
6
  import { extractAgentName, MEMORY_DIR } from "../../shared/openclaw.js";
6
7
  const DEFAULT_OPENCLAW_DIR = join(homedir(), ".openclaw");
7
8
  const MEMORY_INDEX = "MEMORY.md";
@@ -73,37 +74,20 @@ export class Sync {
73
74
  const openclawMemDir = join(openclawDir, MEMORY_DIR);
74
75
  const relicEntries = await this.readMemoryDir(relicMemDir);
75
76
  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++;
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
- // 内容が同じ → 何もしない
77
+ const relicRecord = Object.fromEntries(Object.entries(relicEntries).map(([date, content]) => [`memory/${date}.md`, content]));
78
+ const openclawRecord = Object.fromEntries(Object.entries(openclawEntries).map(([date, content]) => [`memory/${date}.md`, content]));
79
+ const { merged, changedPaths } = mergeMemoryRecords(relicRecord, openclawRecord);
80
+ if (changedPaths.length === 0) {
81
+ return 0;
105
82
  }
106
- return mergedCount;
83
+ await mkdir(relicMemDir, { recursive: true });
84
+ await mkdir(openclawMemDir, { recursive: true });
85
+ for (const [path, content] of Object.entries(merged)) {
86
+ const date = path.replace("memory/", "").replace(".md", "");
87
+ await writeFile(join(relicMemDir, `${date}.md`), content, "utf-8");
88
+ await writeFile(join(openclawMemDir, `${date}.md`), content, "utf-8");
89
+ }
90
+ return changedPaths.length;
107
91
  }
108
92
  /**
109
93
  * MEMORY.md を双方向マージ
@@ -123,41 +107,19 @@ export class Sync {
123
107
  const openclawContent = existsSync(openclawPath)
124
108
  ? await readFile(openclawPath, "utf-8")
125
109
  : 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");
110
+ const relicRecord = relicContent ? { [filename]: relicContent } : {};
111
+ const openclawRecord = openclawContent ? { [filename]: openclawContent } : {};
112
+ const { merged, changedPaths } = mergeMemoryRecords(relicRecord, openclawRecord);
113
+ if (changedPaths.length > 0) {
114
+ const mergedContent = merged[filename];
115
+ if (mergedContent !== undefined) {
116
+ await writeFile(relicPath, mergedContent, "utf-8");
117
+ await writeFile(openclawPath, mergedContent, "utf-8");
118
+ }
138
119
  return true;
139
120
  }
140
121
  return false;
141
122
  }
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
123
  /**
162
124
  * memory/ ディレクトリから日付 → 内容のマップを読む
163
125
  */