@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,142 @@
1
+ import { decryptMemoryBundle } from "../sync/crypto.js";
2
+ import { collectMemoryFiles, memoryFilesToRecord } from "../sync/memory-hash.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Errors
5
+ // ---------------------------------------------------------------------------
6
+ export class MikoshiMemoryPullEngramNotFoundError extends Error {
7
+ engramId;
8
+ constructor(engramId) {
9
+ super(`Engram "${engramId}" not found locally`);
10
+ this.engramId = engramId;
11
+ this.name = "MikoshiMemoryPullEngramNotFoundError";
12
+ }
13
+ }
14
+ export class MikoshiMemoryPullCloudNotFoundError extends Error {
15
+ sourceEngramId;
16
+ constructor(sourceEngramId) {
17
+ super(`Engram "${sourceEngramId}" not found on Mikoshi`);
18
+ this.sourceEngramId = sourceEngramId;
19
+ this.name = "MikoshiMemoryPullCloudNotFoundError";
20
+ }
21
+ }
22
+ export class MikoshiMemoryPullDecryptError extends Error {
23
+ constructor(cause) {
24
+ super("Failed to decrypt memory bundle — wrong passphrase or corrupted data");
25
+ this.name = "MikoshiMemoryPullDecryptError";
26
+ this.cause = cause;
27
+ }
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // Usecase
31
+ // ---------------------------------------------------------------------------
32
+ export class MikoshiMemoryPull {
33
+ localRepo;
34
+ mikoshi;
35
+ constructor(localRepo, mikoshi) {
36
+ this.localRepo = localRepo;
37
+ this.mikoshi = mikoshi;
38
+ }
39
+ /**
40
+ * Phase 1: download + decrypt + diff。書き込みはまだしない。
41
+ */
42
+ async check(engramId, passphrase) {
43
+ // 1. ローカル Engram
44
+ const local = await this.localRepo.get(engramId);
45
+ if (!local)
46
+ throw new MikoshiMemoryPullEngramNotFoundError(engramId);
47
+ // 2. クラウド検索
48
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
49
+ if (!cloudEngram)
50
+ throw new MikoshiMemoryPullCloudNotFoundError(engramId);
51
+ // 3. 暗号化バンドルをダウンロード
52
+ const memoryRes = await this.mikoshi.downloadMemory(cloudEngram.id);
53
+ if (!memoryRes.hasMemory) {
54
+ return {
55
+ result: {
56
+ outcome: "no_remote_memory",
57
+ engramId,
58
+ engramName: local.meta.name,
59
+ cloudEngramId: cloudEngram.id,
60
+ },
61
+ };
62
+ }
63
+ // 4. 復号
64
+ let remoteFiles;
65
+ try {
66
+ remoteFiles = decryptMemoryBundle(memoryRes, passphrase);
67
+ }
68
+ catch (err) {
69
+ throw new MikoshiMemoryPullDecryptError(err);
70
+ }
71
+ // 5. ローカルとの差分計算
72
+ const localEntries = collectMemoryFiles(local.files);
73
+ const localRecord = memoryFilesToRecord(localEntries);
74
+ const diff = computeDiff(localRecord, remoteFiles);
75
+ if (diff.added.length === 0 && diff.changed.length === 0 && diff.removed.length === 0) {
76
+ return {
77
+ result: {
78
+ outcome: "already_synced",
79
+ engramId,
80
+ engramName: local.meta.name,
81
+ cloudEngramId: cloudEngram.id,
82
+ },
83
+ };
84
+ }
85
+ // 6. apply クロージャ
86
+ const apply = async () => {
87
+ const fresh = await this.localRepo.get(engramId);
88
+ if (!fresh)
89
+ throw new MikoshiMemoryPullEngramNotFoundError(engramId);
90
+ const updatedFiles = { ...fresh.files };
91
+ // リモートバンドルからファイルを適用
92
+ updatedFiles.user = remoteFiles["USER.md"] ?? undefined;
93
+ updatedFiles.memory = remoteFiles["MEMORY.md"] ?? undefined;
94
+ // memory entries を再構築
95
+ const newEntries = {};
96
+ for (const [path, content] of Object.entries(remoteFiles)) {
97
+ if (path.startsWith("memory/") && path.endsWith(".md")) {
98
+ const date = path.replace("memory/", "").replace(".md", "");
99
+ newEntries[date] = content;
100
+ }
101
+ }
102
+ updatedFiles.memoryEntries = Object.keys(newEntries).length > 0 ? newEntries : undefined;
103
+ await this.localRepo.save({
104
+ meta: { ...fresh.meta, updatedAt: new Date().toISOString() },
105
+ files: updatedFiles,
106
+ });
107
+ };
108
+ return {
109
+ result: {
110
+ outcome: "pulled",
111
+ engramId,
112
+ engramName: local.meta.name,
113
+ cloudEngramId: cloudEngram.id,
114
+ diff,
115
+ },
116
+ apply,
117
+ };
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // Helpers
122
+ // ---------------------------------------------------------------------------
123
+ function computeDiff(local, remote) {
124
+ const allPaths = new Set([...Object.keys(local), ...Object.keys(remote)]);
125
+ const added = [];
126
+ const changed = [];
127
+ const removed = [];
128
+ for (const path of [...allPaths].sort()) {
129
+ const inLocal = path in local;
130
+ const inRemote = path in remote;
131
+ if (inRemote && !inLocal) {
132
+ added.push(path);
133
+ }
134
+ else if (inLocal && !inRemote) {
135
+ removed.push(path);
136
+ }
137
+ else if (inLocal && inRemote && local[path] !== remote[path]) {
138
+ changed.push(path);
139
+ }
140
+ }
141
+ return { added, changed, removed };
142
+ }
@@ -0,0 +1,34 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient } from "../ports/mikoshi.js";
3
+ export type MikoshiMemoryPushOutcome = "uploaded" | "conflict";
4
+ export interface MikoshiMemoryPushResult {
5
+ outcome: MikoshiMemoryPushOutcome;
6
+ engramId: string;
7
+ engramName: string;
8
+ cloudEngramId: string;
9
+ /** upload 成功時 */
10
+ version?: number;
11
+ memoryContentHash?: string;
12
+ bundleHash?: string;
13
+ /** conflict 時のリモート情報 */
14
+ conflictRemoteHash?: string;
15
+ conflictRemoteVersion?: number;
16
+ }
17
+ export declare class MikoshiMemoryPushEngramNotFoundError extends Error {
18
+ readonly engramId: string;
19
+ constructor(engramId: string);
20
+ }
21
+ export declare class MikoshiMemoryPushNoFilesError extends Error {
22
+ readonly engramId: string;
23
+ constructor(engramId: string);
24
+ }
25
+ export declare class MikoshiMemoryPushCloudNotFoundError extends Error {
26
+ readonly sourceEngramId: string;
27
+ constructor(sourceEngramId: string);
28
+ }
29
+ export declare class MikoshiMemoryPush {
30
+ private readonly localRepo;
31
+ private readonly mikoshi;
32
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
33
+ execute(engramId: string, passphrase: string): Promise<MikoshiMemoryPushResult>;
34
+ }
@@ -0,0 +1,105 @@
1
+ import { MikoshiApiError } from "../ports/mikoshi.js";
2
+ import { collectMemoryFiles, memoryFilesToRecord, computeMemoryContentHash } from "../sync/memory-hash.js";
3
+ import { encryptMemoryBundle } from "../sync/crypto.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Errors
6
+ // ---------------------------------------------------------------------------
7
+ export class MikoshiMemoryPushEngramNotFoundError extends Error {
8
+ engramId;
9
+ constructor(engramId) {
10
+ super(`Engram "${engramId}" not found locally`);
11
+ this.engramId = engramId;
12
+ this.name = "MikoshiMemoryPushEngramNotFoundError";
13
+ }
14
+ }
15
+ export class MikoshiMemoryPushNoFilesError extends Error {
16
+ engramId;
17
+ constructor(engramId) {
18
+ super(`Engram "${engramId}" has no memory files to upload`);
19
+ this.engramId = engramId;
20
+ this.name = "MikoshiMemoryPushNoFilesError";
21
+ }
22
+ }
23
+ export class MikoshiMemoryPushCloudNotFoundError extends Error {
24
+ sourceEngramId;
25
+ constructor(sourceEngramId) {
26
+ super(`Engram "${sourceEngramId}" not found on Mikoshi. Push persona first with: relic mikoshi push`);
27
+ this.sourceEngramId = sourceEngramId;
28
+ this.name = "MikoshiMemoryPushCloudNotFoundError";
29
+ }
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Usecase
33
+ // ---------------------------------------------------------------------------
34
+ export class MikoshiMemoryPush {
35
+ localRepo;
36
+ mikoshi;
37
+ constructor(localRepo, mikoshi) {
38
+ this.localRepo = localRepo;
39
+ this.mikoshi = mikoshi;
40
+ }
41
+ async execute(engramId, passphrase) {
42
+ // 1. ローカル Engram 読み込み
43
+ const local = await this.localRepo.get(engramId);
44
+ if (!local)
45
+ throw new MikoshiMemoryPushEngramNotFoundError(engramId);
46
+ // 2. Memory ファイル収集
47
+ const memoryEntries = collectMemoryFiles(local.files);
48
+ if (memoryEntries.length === 0)
49
+ throw new MikoshiMemoryPushNoFilesError(engramId);
50
+ // 3. クラウド検索
51
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
52
+ if (!cloudEngram)
53
+ throw new MikoshiMemoryPushCloudNotFoundError(engramId);
54
+ // 4. 暗号化
55
+ const filesRecord = memoryFilesToRecord(memoryEntries);
56
+ const bundle = encryptMemoryBundle(filesRecord, passphrase);
57
+ // 5. ローカル memory content hash
58
+ const memoryContentHash = computeMemoryContentHash(memoryEntries);
59
+ if (!memoryContentHash)
60
+ throw new MikoshiMemoryPushNoFilesError(engramId);
61
+ // 6. リモートの現在の memory hash を取得
62
+ const syncStatus = await this.mikoshi.getSyncStatus(cloudEngram.id);
63
+ const expectedRemoteHash = syncStatus.memory.token?.memoryContentHash ?? null;
64
+ // 7. アップロード
65
+ try {
66
+ const res = await this.mikoshi.uploadMemory(cloudEngram.id, {
67
+ ciphertext: bundle.ciphertext,
68
+ cipherAlgorithm: bundle.cipherAlgorithm,
69
+ cipherNonce: bundle.cipherNonce,
70
+ wrappedBundleKey: bundle.wrappedBundleKey,
71
+ wrapAlgorithm: bundle.wrapAlgorithm,
72
+ kdfAlgorithm: bundle.kdfAlgorithm,
73
+ kdfSalt: bundle.kdfSalt,
74
+ kdfParams: bundle.kdfParams,
75
+ manifest: bundle.manifest,
76
+ expectedRemoteMemoryContentHash: expectedRemoteHash,
77
+ memoryContentHash,
78
+ bundleHash: bundle.bundleHash,
79
+ });
80
+ return {
81
+ outcome: "uploaded",
82
+ engramId,
83
+ engramName: local.meta.name,
84
+ cloudEngramId: cloudEngram.id,
85
+ version: res.version,
86
+ memoryContentHash: res.memoryContentHash,
87
+ bundleHash: res.bundleHash,
88
+ };
89
+ }
90
+ catch (err) {
91
+ if (err instanceof MikoshiApiError && err.isConflict && err.code === "MEMORY_CONFLICT") {
92
+ const body = err.body;
93
+ return {
94
+ outcome: "conflict",
95
+ engramId,
96
+ engramName: local.meta.name,
97
+ cloudEngramId: cloudEngram.id,
98
+ conflictRemoteHash: body?.currentMemory?.memoryContentHash,
99
+ conflictRemoteVersion: body?.currentMemory?.version,
100
+ };
101
+ }
102
+ throw err;
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,36 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient } from "../ports/mikoshi.js";
3
+ import { type MemoryRecordDiff } from "../sync/memory-merge.js";
4
+ export type MikoshiMemorySyncOutcome = "synced" | "already_synced" | "conflict";
5
+ export interface MikoshiMemorySyncResult {
6
+ outcome: MikoshiMemorySyncOutcome;
7
+ engramId: string;
8
+ engramName: string;
9
+ cloudEngramId: string;
10
+ localUpdated?: boolean;
11
+ remoteUpdated?: boolean;
12
+ diff?: MemoryRecordDiff;
13
+ mergedPaths?: string[];
14
+ version?: number;
15
+ memoryContentHash?: string;
16
+ bundleHash?: string;
17
+ conflictRemoteHash?: string;
18
+ conflictRemoteVersion?: number;
19
+ }
20
+ export declare class MikoshiMemorySyncEngramNotFoundError extends Error {
21
+ readonly engramId: string;
22
+ constructor(engramId: string);
23
+ }
24
+ export declare class MikoshiMemorySyncCloudNotFoundError extends Error {
25
+ readonly sourceEngramId: string;
26
+ constructor(sourceEngramId: string);
27
+ }
28
+ export declare class MikoshiMemorySyncDecryptError extends Error {
29
+ constructor(cause?: unknown);
30
+ }
31
+ export declare class MikoshiMemorySync {
32
+ private readonly localRepo;
33
+ private readonly mikoshi;
34
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
35
+ execute(engramId: string, passphrase: string): Promise<MikoshiMemorySyncResult>;
36
+ }
@@ -0,0 +1,175 @@
1
+ import { MikoshiApiError } from "../ports/mikoshi.js";
2
+ import { decryptMemoryBundle, encryptMemoryBundle } from "../sync/crypto.js";
3
+ import { collectMemoryFiles, computeMemoryContentHash, memoryFilesToRecord } from "../sync/memory-hash.js";
4
+ import { applyMemoryRecordToFiles, computeMemoryRecordDiff, mergeMemoryRecords, } from "../sync/memory-merge.js";
5
+ export class MikoshiMemorySyncEngramNotFoundError extends Error {
6
+ engramId;
7
+ constructor(engramId) {
8
+ super(`Engram "${engramId}" not found locally`);
9
+ this.engramId = engramId;
10
+ this.name = "MikoshiMemorySyncEngramNotFoundError";
11
+ }
12
+ }
13
+ export class MikoshiMemorySyncCloudNotFoundError extends Error {
14
+ sourceEngramId;
15
+ constructor(sourceEngramId) {
16
+ super(`Engram "${sourceEngramId}" not found on Mikoshi. Push persona first with: relic mikoshi push`);
17
+ this.sourceEngramId = sourceEngramId;
18
+ this.name = "MikoshiMemorySyncCloudNotFoundError";
19
+ }
20
+ }
21
+ export class MikoshiMemorySyncDecryptError extends Error {
22
+ constructor(cause) {
23
+ super("Failed to decrypt memory bundle — wrong passphrase or corrupted data");
24
+ this.name = "MikoshiMemorySyncDecryptError";
25
+ this.cause = cause;
26
+ }
27
+ }
28
+ export class MikoshiMemorySync {
29
+ localRepo;
30
+ mikoshi;
31
+ constructor(localRepo, mikoshi) {
32
+ this.localRepo = localRepo;
33
+ this.mikoshi = mikoshi;
34
+ }
35
+ async execute(engramId, passphrase) {
36
+ const local = await this.localRepo.get(engramId);
37
+ if (!local)
38
+ throw new MikoshiMemorySyncEngramNotFoundError(engramId);
39
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
40
+ if (!cloudEngram)
41
+ throw new MikoshiMemorySyncCloudNotFoundError(engramId);
42
+ const localRecord = memoryFilesToRecord(collectMemoryFiles(local.files));
43
+ const memoryRes = await this.mikoshi.downloadMemory(cloudEngram.id);
44
+ let remoteRecord = {};
45
+ let expectedRemoteHash = null;
46
+ if (memoryRes.hasMemory) {
47
+ try {
48
+ remoteRecord = decryptMemoryBundle(memoryRes, passphrase);
49
+ }
50
+ catch (err) {
51
+ throw new MikoshiMemorySyncDecryptError(err);
52
+ }
53
+ expectedRemoteHash = memoryRes.memoryContentHash;
54
+ }
55
+ if (Object.keys(localRecord).length === 0 && Object.keys(remoteRecord).length === 0) {
56
+ return {
57
+ outcome: "already_synced",
58
+ engramId,
59
+ engramName: local.meta.name,
60
+ cloudEngramId: cloudEngram.id,
61
+ localUpdated: false,
62
+ remoteUpdated: false,
63
+ };
64
+ }
65
+ const diff = computeMemoryRecordDiff(localRecord, remoteRecord);
66
+ const { merged, changedPaths } = mergeMemoryRecords(localRecord, remoteRecord);
67
+ const localUpdated = !recordsEqual(localRecord, merged);
68
+ const remoteUpdated = !recordsEqual(remoteRecord, merged);
69
+ if (!localUpdated && !remoteUpdated) {
70
+ return {
71
+ outcome: "already_synced",
72
+ engramId,
73
+ engramName: local.meta.name,
74
+ cloudEngramId: cloudEngram.id,
75
+ localUpdated: false,
76
+ remoteUpdated: false,
77
+ diff,
78
+ mergedPaths: [],
79
+ };
80
+ }
81
+ if (localUpdated) {
82
+ await this.localRepo.save({
83
+ meta: { ...local.meta, updatedAt: new Date().toISOString() },
84
+ files: applyMemoryRecordToFiles(local.files, merged),
85
+ });
86
+ }
87
+ if (!remoteUpdated) {
88
+ return {
89
+ outcome: "synced",
90
+ engramId,
91
+ engramName: local.meta.name,
92
+ cloudEngramId: cloudEngram.id,
93
+ localUpdated,
94
+ remoteUpdated: false,
95
+ diff,
96
+ mergedPaths: changedPaths,
97
+ };
98
+ }
99
+ const mergedBundle = encryptMemoryBundle(merged, passphrase);
100
+ const mergedEntries = Object.entries(merged).map(([path, content]) => ({ path, content }));
101
+ const memoryContentHash = computeMemoryContentHash(mergedEntries);
102
+ if (!memoryContentHash) {
103
+ return {
104
+ outcome: "already_synced",
105
+ engramId,
106
+ engramName: local.meta.name,
107
+ cloudEngramId: cloudEngram.id,
108
+ localUpdated,
109
+ remoteUpdated: false,
110
+ diff,
111
+ mergedPaths: changedPaths,
112
+ };
113
+ }
114
+ try {
115
+ const uploaded = await this.mikoshi.uploadMemory(cloudEngram.id, {
116
+ ciphertext: mergedBundle.ciphertext,
117
+ cipherAlgorithm: mergedBundle.cipherAlgorithm,
118
+ cipherNonce: mergedBundle.cipherNonce,
119
+ wrappedBundleKey: mergedBundle.wrappedBundleKey,
120
+ wrapAlgorithm: mergedBundle.wrapAlgorithm,
121
+ kdfAlgorithm: mergedBundle.kdfAlgorithm,
122
+ kdfSalt: mergedBundle.kdfSalt,
123
+ kdfParams: mergedBundle.kdfParams,
124
+ manifest: mergedBundle.manifest,
125
+ expectedRemoteMemoryContentHash: expectedRemoteHash,
126
+ memoryContentHash,
127
+ bundleHash: mergedBundle.bundleHash,
128
+ });
129
+ return {
130
+ outcome: "synced",
131
+ engramId,
132
+ engramName: local.meta.name,
133
+ cloudEngramId: cloudEngram.id,
134
+ localUpdated,
135
+ remoteUpdated: true,
136
+ diff,
137
+ mergedPaths: changedPaths,
138
+ version: uploaded.version,
139
+ memoryContentHash: uploaded.memoryContentHash,
140
+ bundleHash: uploaded.bundleHash,
141
+ };
142
+ }
143
+ catch (err) {
144
+ if (err instanceof MikoshiApiError && err.isConflict && err.code === "MEMORY_CONFLICT") {
145
+ const body = err.body;
146
+ return {
147
+ outcome: "conflict",
148
+ engramId,
149
+ engramName: local.meta.name,
150
+ cloudEngramId: cloudEngram.id,
151
+ localUpdated,
152
+ remoteUpdated: true,
153
+ diff,
154
+ mergedPaths: changedPaths,
155
+ conflictRemoteHash: body?.currentMemory?.memoryContentHash,
156
+ conflictRemoteVersion: body?.currentMemory?.version,
157
+ };
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ }
163
+ function recordsEqual(a, b) {
164
+ const aKeys = Object.keys(a).sort();
165
+ const bKeys = Object.keys(b).sort();
166
+ if (aKeys.length !== bKeys.length)
167
+ return false;
168
+ for (let i = 0; i < aKeys.length; i += 1) {
169
+ if (aKeys[i] !== bKeys[i])
170
+ return false;
171
+ if (a[aKeys[i]] !== b[bKeys[i]])
172
+ return false;
173
+ }
174
+ return true;
175
+ }
@@ -0,0 +1,44 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient } from "../ports/mikoshi.js";
3
+ export type MikoshiPullOutcome = "pulled" | "already_synced";
4
+ export interface MikoshiPullDiff {
5
+ soulDiffers: boolean;
6
+ identityDiffers: boolean;
7
+ remoteSoul: string;
8
+ remoteIdentity: string;
9
+ }
10
+ export interface MikoshiPullResult {
11
+ outcome: MikoshiPullOutcome;
12
+ engramId: string;
13
+ engramName: string;
14
+ cloudEngramId: string;
15
+ diff?: MikoshiPullDiff;
16
+ }
17
+ export declare class MikoshiPullEngramNotFoundError extends Error {
18
+ readonly engramId: string;
19
+ constructor(engramId: string);
20
+ }
21
+ export declare class MikoshiPullCloudNotFoundError extends Error {
22
+ readonly sourceEngramId: string;
23
+ constructor(sourceEngramId: string);
24
+ }
25
+ export declare class MikoshiPullPersonaMissingError extends Error {
26
+ readonly cloudEngramId: string;
27
+ constructor(cloudEngramId: string);
28
+ }
29
+ export declare class MikoshiPull {
30
+ private readonly localRepo;
31
+ private readonly mikoshi;
32
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
33
+ /**
34
+ * Phase 1: fetch + diff。上書きはまだしない。
35
+ *
36
+ * - ローカル未作成 → EngramNotFoundError
37
+ * - ローカル既存 + 差分なし → already_synced
38
+ * - ローカル既存 + 差分あり → apply で上書き
39
+ */
40
+ check(engramId: string): Promise<{
41
+ result: MikoshiPullResult;
42
+ apply?: () => Promise<void>;
43
+ }>;
44
+ }
@@ -0,0 +1,124 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Errors
3
+ // ---------------------------------------------------------------------------
4
+ export class MikoshiPullEngramNotFoundError extends Error {
5
+ engramId;
6
+ constructor(engramId) {
7
+ super(`Engram "${engramId}" not found locally. Use "relic mikoshi pull" to create or update it.`);
8
+ this.engramId = engramId;
9
+ this.name = "MikoshiPullEngramNotFoundError";
10
+ }
11
+ }
12
+ export class MikoshiPullCloudNotFoundError extends Error {
13
+ sourceEngramId;
14
+ constructor(sourceEngramId) {
15
+ super(`Engram "${sourceEngramId}" not found on Mikoshi`);
16
+ this.sourceEngramId = sourceEngramId;
17
+ this.name = "MikoshiPullCloudNotFoundError";
18
+ }
19
+ }
20
+ export class MikoshiPullPersonaMissingError extends Error {
21
+ cloudEngramId;
22
+ constructor(cloudEngramId) {
23
+ super(`Remote Engram "${cloudEngramId}" is missing SOUL.md or IDENTITY.md`);
24
+ this.cloudEngramId = cloudEngramId;
25
+ this.name = "MikoshiPullPersonaMissingError";
26
+ }
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Usecase
30
+ // ---------------------------------------------------------------------------
31
+ export class MikoshiPull {
32
+ localRepo;
33
+ mikoshi;
34
+ constructor(localRepo, mikoshi) {
35
+ this.localRepo = localRepo;
36
+ this.mikoshi = mikoshi;
37
+ }
38
+ /**
39
+ * Phase 1: fetch + diff。上書きはまだしない。
40
+ *
41
+ * - ローカル未作成 → EngramNotFoundError
42
+ * - ローカル既存 + 差分なし → already_synced
43
+ * - ローカル既存 + 差分あり → apply で上書き
44
+ */
45
+ async check(engramId) {
46
+ // 1. ローカル Engram 必須
47
+ const local = await this.localRepo.get(engramId);
48
+ if (!local) {
49
+ throw new MikoshiPullEngramNotFoundError(engramId);
50
+ }
51
+ // 2. クラウド検索
52
+ const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
53
+ if (!cloudEngram)
54
+ throw new MikoshiPullCloudNotFoundError(engramId);
55
+ // 3. Detail 取得 (persona content 込み)
56
+ const detail = await this.mikoshi.getEngram(cloudEngram.id);
57
+ const { soul: remoteSoul, identity: remoteIdentity } = extractPersona(detail);
58
+ if (!remoteSoul || !remoteIdentity) {
59
+ throw new MikoshiPullPersonaMissingError(cloudEngram.id);
60
+ }
61
+ // 4. 差分計算 (末尾空白の差異は無視)
62
+ const soulDiffers = normalizeContent(local.files.soul) !== normalizeContent(remoteSoul);
63
+ const identityDiffers = normalizeContent(local.files.identity) !== normalizeContent(remoteIdentity);
64
+ if (!soulDiffers && !identityDiffers) {
65
+ return {
66
+ result: {
67
+ outcome: "already_synced",
68
+ engramId,
69
+ engramName: local.meta.name,
70
+ cloudEngramId: cloudEngram.id,
71
+ },
72
+ };
73
+ }
74
+ const diff = {
75
+ soulDiffers,
76
+ identityDiffers,
77
+ remoteSoul,
78
+ remoteIdentity,
79
+ };
80
+ // 5. apply クロージャ
81
+ const apply = async () => {
82
+ const fresh = await this.localRepo.get(engramId);
83
+ if (!fresh)
84
+ throw new MikoshiPullEngramNotFoundError(engramId);
85
+ const updatedFiles = { ...fresh.files };
86
+ if (soulDiffers)
87
+ updatedFiles.soul = remoteSoul;
88
+ if (identityDiffers)
89
+ updatedFiles.identity = remoteIdentity;
90
+ await this.localRepo.save({
91
+ meta: { ...fresh.meta, updatedAt: new Date().toISOString() },
92
+ files: updatedFiles,
93
+ });
94
+ };
95
+ return {
96
+ result: {
97
+ outcome: "pulled",
98
+ engramId,
99
+ engramName: local.meta.name,
100
+ cloudEngramId: cloudEngram.id,
101
+ diff,
102
+ },
103
+ apply,
104
+ };
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Helpers
109
+ // ---------------------------------------------------------------------------
110
+ function extractPersona(detail) {
111
+ let soul;
112
+ let identity;
113
+ for (const f of detail.personaFiles) {
114
+ if (f.fileType === "SOUL")
115
+ soul = f.content;
116
+ if (f.fileType === "IDENTITY")
117
+ identity = f.content;
118
+ }
119
+ return { soul, identity };
120
+ }
121
+ /** Trim trailing whitespace so minor formatting differences don't cause false diffs. */
122
+ function normalizeContent(content) {
123
+ return (content ?? "").trimEnd();
124
+ }