@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.
- package/LICENCE.md +1 -1
- package/README.md +19 -12
- package/dist/adapters/mikoshi/client.d.ts +21 -0
- package/dist/adapters/mikoshi/client.js +84 -0
- package/dist/core/ports/index.d.ts +1 -0
- package/dist/core/ports/mikoshi.d.ts +937 -0
- package/dist/core/ports/mikoshi.js +209 -0
- package/dist/core/sync/crypto.d.ts +55 -0
- package/dist/core/sync/crypto.js +189 -0
- package/dist/core/sync/memory-hash.d.ts +35 -0
- package/dist/core/sync/memory-hash.js +61 -0
- package/dist/core/sync/memory-merge.d.ts +13 -0
- package/dist/core/sync/memory-merge.js +84 -0
- package/dist/core/sync/normalize.d.ts +10 -0
- package/dist/core/sync/normalize.js +21 -0
- package/dist/core/sync/persona-hash.d.ts +15 -0
- package/dist/core/sync/persona-hash.js +36 -0
- package/dist/core/usecases/claw-pull.d.ts +44 -0
- package/dist/core/usecases/claw-pull.js +117 -0
- package/dist/core/usecases/extract.d.ts +0 -1
- package/dist/core/usecases/extract.js +13 -27
- package/dist/core/usecases/index.d.ts +8 -0
- package/dist/core/usecases/index.js +8 -0
- package/dist/core/usecases/inject.d.ts +1 -0
- package/dist/core/usecases/inject.js +2 -0
- package/dist/core/usecases/mikoshi-clone.d.ts +29 -0
- package/dist/core/usecases/mikoshi-clone.js +94 -0
- package/dist/core/usecases/mikoshi-download.d.ts +29 -0
- package/dist/core/usecases/mikoshi-download.js +94 -0
- package/dist/core/usecases/mikoshi-memory-pull.d.ts +38 -0
- package/dist/core/usecases/mikoshi-memory-pull.js +142 -0
- package/dist/core/usecases/mikoshi-memory-push.d.ts +34 -0
- package/dist/core/usecases/mikoshi-memory-push.js +105 -0
- package/dist/core/usecases/mikoshi-memory-sync.d.ts +36 -0
- package/dist/core/usecases/mikoshi-memory-sync.js +175 -0
- package/dist/core/usecases/mikoshi-pull.d.ts +44 -0
- package/dist/core/usecases/mikoshi-pull.js +124 -0
- package/dist/core/usecases/mikoshi-push.d.ts +39 -0
- package/dist/core/usecases/mikoshi-push.js +127 -0
- package/dist/core/usecases/mikoshi-status.d.ts +37 -0
- package/dist/core/usecases/mikoshi-status.js +92 -0
- package/dist/core/usecases/sync.d.ts +0 -5
- package/dist/core/usecases/sync.js +23 -61
- package/dist/interfaces/cli/commands/claw.js +134 -85
- package/dist/interfaces/cli/commands/config.js +59 -0
- package/dist/interfaces/cli/commands/create.js +17 -16
- package/dist/interfaces/cli/commands/init.js +11 -10
- package/dist/interfaces/cli/commands/list.js +9 -7
- package/dist/interfaces/cli/commands/migrate.js +8 -7
- package/dist/interfaces/cli/commands/mikoshi.d.ts +2 -0
- package/dist/interfaces/cli/commands/mikoshi.js +565 -0
- package/dist/interfaces/cli/commands/refresh-samples.js +10 -9
- package/dist/interfaces/cli/index.js +2 -0
- package/dist/interfaces/cli/output.d.ts +5 -0
- package/dist/interfaces/cli/output.js +16 -0
- package/dist/interfaces/cli/spinner.d.ts +13 -0
- package/dist/interfaces/cli/spinner.js +47 -0
- package/dist/shared/config.d.ts +23 -3
- package/dist/shared/config.js +30 -2
- 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
|
+
}
|
|
@@ -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
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
*/
|