@ectplsm/relic 0.3.2 → 0.5.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 +20 -13
- 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/archive-cursor-update.d.ts +6 -0
- package/dist/core/usecases/archive-cursor-update.js +18 -0
- package/dist/core/usecases/archive-pending.d.ts +18 -1
- package/dist/core/usecases/archive-pending.js +37 -2
- 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 +11 -3
- package/dist/core/usecases/index.js +10 -2
- package/dist/core/usecases/inject.d.ts +1 -0
- package/dist/core/usecases/inject.js +2 -0
- package/dist/core/usecases/memory-write.d.ts +5 -0
- package/dist/core/usecases/memory-write.js +26 -16
- 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 +60 -1
- 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/interfaces/mcp/index.js +109 -38
- package/dist/shared/config.d.ts +25 -5
- package/dist/shared/config.js +33 -5
- package/dist/shared/engram-composer.d.ts +1 -1
- package/dist/shared/engram-composer.js +9 -5
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|