@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.
Files changed (69) hide show
  1. package/LICENCE.md +1 -1
  2. package/README.md +20 -13
  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/archive-cursor-update.d.ts +6 -0
  19. package/dist/core/usecases/archive-cursor-update.js +18 -0
  20. package/dist/core/usecases/archive-pending.d.ts +18 -1
  21. package/dist/core/usecases/archive-pending.js +37 -2
  22. package/dist/core/usecases/claw-pull.d.ts +44 -0
  23. package/dist/core/usecases/claw-pull.js +117 -0
  24. package/dist/core/usecases/extract.d.ts +0 -1
  25. package/dist/core/usecases/extract.js +13 -27
  26. package/dist/core/usecases/index.d.ts +11 -3
  27. package/dist/core/usecases/index.js +10 -2
  28. package/dist/core/usecases/inject.d.ts +1 -0
  29. package/dist/core/usecases/inject.js +2 -0
  30. package/dist/core/usecases/memory-write.d.ts +5 -0
  31. package/dist/core/usecases/memory-write.js +26 -16
  32. package/dist/core/usecases/mikoshi-clone.d.ts +29 -0
  33. package/dist/core/usecases/mikoshi-clone.js +94 -0
  34. package/dist/core/usecases/mikoshi-download.d.ts +29 -0
  35. package/dist/core/usecases/mikoshi-download.js +94 -0
  36. package/dist/core/usecases/mikoshi-memory-pull.d.ts +38 -0
  37. package/dist/core/usecases/mikoshi-memory-pull.js +142 -0
  38. package/dist/core/usecases/mikoshi-memory-push.d.ts +34 -0
  39. package/dist/core/usecases/mikoshi-memory-push.js +105 -0
  40. package/dist/core/usecases/mikoshi-memory-sync.d.ts +36 -0
  41. package/dist/core/usecases/mikoshi-memory-sync.js +175 -0
  42. package/dist/core/usecases/mikoshi-pull.d.ts +44 -0
  43. package/dist/core/usecases/mikoshi-pull.js +124 -0
  44. package/dist/core/usecases/mikoshi-push.d.ts +39 -0
  45. package/dist/core/usecases/mikoshi-push.js +127 -0
  46. package/dist/core/usecases/mikoshi-status.d.ts +37 -0
  47. package/dist/core/usecases/mikoshi-status.js +92 -0
  48. package/dist/core/usecases/sync.d.ts +0 -5
  49. package/dist/core/usecases/sync.js +23 -61
  50. package/dist/interfaces/cli/commands/claw.js +134 -85
  51. package/dist/interfaces/cli/commands/config.js +60 -1
  52. package/dist/interfaces/cli/commands/create.js +17 -16
  53. package/dist/interfaces/cli/commands/init.js +11 -10
  54. package/dist/interfaces/cli/commands/list.js +9 -7
  55. package/dist/interfaces/cli/commands/migrate.js +8 -7
  56. package/dist/interfaces/cli/commands/mikoshi.d.ts +2 -0
  57. package/dist/interfaces/cli/commands/mikoshi.js +565 -0
  58. package/dist/interfaces/cli/commands/refresh-samples.js +10 -9
  59. package/dist/interfaces/cli/index.js +2 -0
  60. package/dist/interfaces/cli/output.d.ts +5 -0
  61. package/dist/interfaces/cli/output.js +16 -0
  62. package/dist/interfaces/cli/spinner.d.ts +13 -0
  63. package/dist/interfaces/cli/spinner.js +47 -0
  64. package/dist/interfaces/mcp/index.js +109 -38
  65. package/dist/shared/config.d.ts +25 -5
  66. package/dist/shared/config.js +33 -5
  67. package/dist/shared/engram-composer.d.ts +1 -1
  68. package/dist/shared/engram-composer.js +9 -5
  69. 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
+ }
@@ -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
  */