@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,94 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Errors
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export class MikoshiCloneAlreadyExistsError extends Error {
|
|
5
|
+
engramId;
|
|
6
|
+
constructor(engramId) {
|
|
7
|
+
super(`Engram "${engramId}" already exists locally. Use "relic mikoshi pull" to update it.`);
|
|
8
|
+
this.engramId = engramId;
|
|
9
|
+
this.name = "MikoshiCloneAlreadyExistsError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class MikoshiCloneCloudNotFoundError extends Error {
|
|
13
|
+
sourceEngramId;
|
|
14
|
+
constructor(sourceEngramId) {
|
|
15
|
+
super(`Engram "${sourceEngramId}" not found on Mikoshi`);
|
|
16
|
+
this.sourceEngramId = sourceEngramId;
|
|
17
|
+
this.name = "MikoshiCloneCloudNotFoundError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class MikoshiClonePersonaMissingError 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 = "MikoshiClonePersonaMissingError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Usecase
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
export class MikoshiClone {
|
|
32
|
+
localRepo;
|
|
33
|
+
mikoshi;
|
|
34
|
+
constructor(localRepo, mikoshi) {
|
|
35
|
+
this.localRepo = localRepo;
|
|
36
|
+
this.mikoshi = mikoshi;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Clone a remote Engram from Mikoshi to local.
|
|
40
|
+
* Fails if the local Engram already exists.
|
|
41
|
+
*/
|
|
42
|
+
async execute(engramId) {
|
|
43
|
+
// 1. ローカルに既に存在 → エラー
|
|
44
|
+
const existing = await this.localRepo.get(engramId);
|
|
45
|
+
if (existing) {
|
|
46
|
+
throw new MikoshiCloneAlreadyExistsError(engramId);
|
|
47
|
+
}
|
|
48
|
+
// 2. クラウド検索
|
|
49
|
+
const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
|
|
50
|
+
if (!cloudEngram)
|
|
51
|
+
throw new MikoshiCloneCloudNotFoundError(engramId);
|
|
52
|
+
// 3. Detail 取得 (persona content 込み)
|
|
53
|
+
const detail = await this.mikoshi.getEngram(cloudEngram.id);
|
|
54
|
+
const { soul, identity } = extractPersona(detail);
|
|
55
|
+
if (!soul || !identity) {
|
|
56
|
+
throw new MikoshiClonePersonaMissingError(cloudEngram.id);
|
|
57
|
+
}
|
|
58
|
+
// 4. ローカルに新規作成
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
await this.localRepo.save({
|
|
61
|
+
meta: {
|
|
62
|
+
id: engramId,
|
|
63
|
+
name: detail.name,
|
|
64
|
+
description: detail.description ?? undefined,
|
|
65
|
+
tags: detail.tags,
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
},
|
|
69
|
+
files: {
|
|
70
|
+
soul,
|
|
71
|
+
identity,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
engramId,
|
|
76
|
+
engramName: detail.name,
|
|
77
|
+
cloudEngramId: cloudEngram.id,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
function extractPersona(detail) {
|
|
85
|
+
let soul;
|
|
86
|
+
let identity;
|
|
87
|
+
for (const f of detail.personaFiles) {
|
|
88
|
+
if (f.fileType === "SOUL")
|
|
89
|
+
soul = f.content;
|
|
90
|
+
if (f.fileType === "IDENTITY")
|
|
91
|
+
identity = f.content;
|
|
92
|
+
}
|
|
93
|
+
return { soul, identity };
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
2
|
+
import type { MikoshiClient } from "../ports/mikoshi.js";
|
|
3
|
+
export interface MikoshiDownloadResult {
|
|
4
|
+
engramId: string;
|
|
5
|
+
engramName: string;
|
|
6
|
+
cloudEngramId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class MikoshiDownloadAlreadyExistsError extends Error {
|
|
9
|
+
readonly engramId: string;
|
|
10
|
+
constructor(engramId: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class MikoshiDownloadCloudNotFoundError extends Error {
|
|
13
|
+
readonly sourceEngramId: string;
|
|
14
|
+
constructor(sourceEngramId: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class MikoshiDownloadPersonaMissingError extends Error {
|
|
17
|
+
readonly cloudEngramId: string;
|
|
18
|
+
constructor(cloudEngramId: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class MikoshiDownload {
|
|
21
|
+
private readonly localRepo;
|
|
22
|
+
private readonly mikoshi;
|
|
23
|
+
constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
|
|
24
|
+
/**
|
|
25
|
+
* Download a remote Engram from Mikoshi to local.
|
|
26
|
+
* Fails if the local Engram already exists.
|
|
27
|
+
*/
|
|
28
|
+
execute(engramId: string): Promise<MikoshiDownloadResult>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Errors
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export class MikoshiDownloadAlreadyExistsError extends Error {
|
|
5
|
+
engramId;
|
|
6
|
+
constructor(engramId) {
|
|
7
|
+
super(`Engram "${engramId}" already exists locally. Use "relic mikoshi pull" to update it.`);
|
|
8
|
+
this.engramId = engramId;
|
|
9
|
+
this.name = "MikoshiDownloadAlreadyExistsError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class MikoshiDownloadCloudNotFoundError extends Error {
|
|
13
|
+
sourceEngramId;
|
|
14
|
+
constructor(sourceEngramId) {
|
|
15
|
+
super(`Engram "${sourceEngramId}" not found on Mikoshi`);
|
|
16
|
+
this.sourceEngramId = sourceEngramId;
|
|
17
|
+
this.name = "MikoshiDownloadCloudNotFoundError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class MikoshiDownloadPersonaMissingError 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 = "MikoshiDownloadPersonaMissingError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Usecase
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
export class MikoshiDownload {
|
|
32
|
+
localRepo;
|
|
33
|
+
mikoshi;
|
|
34
|
+
constructor(localRepo, mikoshi) {
|
|
35
|
+
this.localRepo = localRepo;
|
|
36
|
+
this.mikoshi = mikoshi;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Download a remote Engram from Mikoshi to local.
|
|
40
|
+
* Fails if the local Engram already exists.
|
|
41
|
+
*/
|
|
42
|
+
async execute(engramId) {
|
|
43
|
+
// 1. ローカルに既に存在 → エラー
|
|
44
|
+
const existing = await this.localRepo.get(engramId);
|
|
45
|
+
if (existing) {
|
|
46
|
+
throw new MikoshiDownloadAlreadyExistsError(engramId);
|
|
47
|
+
}
|
|
48
|
+
// 2. クラウド検索
|
|
49
|
+
const cloudEngram = await this.mikoshi.getEngramBySourceId(engramId);
|
|
50
|
+
if (!cloudEngram)
|
|
51
|
+
throw new MikoshiDownloadCloudNotFoundError(engramId);
|
|
52
|
+
// 3. Detail 取得 (persona content 込み)
|
|
53
|
+
const detail = await this.mikoshi.getEngram(cloudEngram.id);
|
|
54
|
+
const { soul, identity } = extractPersona(detail);
|
|
55
|
+
if (!soul || !identity) {
|
|
56
|
+
throw new MikoshiDownloadPersonaMissingError(cloudEngram.id);
|
|
57
|
+
}
|
|
58
|
+
// 4. ローカルに新規作成
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
await this.localRepo.save({
|
|
61
|
+
meta: {
|
|
62
|
+
id: engramId,
|
|
63
|
+
name: detail.name,
|
|
64
|
+
description: detail.description ?? undefined,
|
|
65
|
+
tags: detail.tags,
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
},
|
|
69
|
+
files: {
|
|
70
|
+
soul,
|
|
71
|
+
identity,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
engramId,
|
|
76
|
+
engramName: detail.name,
|
|
77
|
+
cloudEngramId: cloudEngram.id,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
function extractPersona(detail) {
|
|
85
|
+
let soul;
|
|
86
|
+
let identity;
|
|
87
|
+
for (const f of detail.personaFiles) {
|
|
88
|
+
if (f.fileType === "SOUL")
|
|
89
|
+
soul = f.content;
|
|
90
|
+
if (f.fileType === "IDENTITY")
|
|
91
|
+
identity = f.content;
|
|
92
|
+
}
|
|
93
|
+
return { soul, identity };
|
|
94
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
2
|
+
import type { MikoshiClient } from "../ports/mikoshi.js";
|
|
3
|
+
export type MikoshiMemoryPullOutcome = "pulled" | "already_synced" | "no_remote_memory";
|
|
4
|
+
export interface MikoshiMemoryPullDiff {
|
|
5
|
+
added: string[];
|
|
6
|
+
changed: string[];
|
|
7
|
+
removed: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface MikoshiMemoryPullResult {
|
|
10
|
+
outcome: MikoshiMemoryPullOutcome;
|
|
11
|
+
engramId: string;
|
|
12
|
+
engramName: string;
|
|
13
|
+
cloudEngramId: string;
|
|
14
|
+
diff?: MikoshiMemoryPullDiff;
|
|
15
|
+
}
|
|
16
|
+
export declare class MikoshiMemoryPullEngramNotFoundError extends Error {
|
|
17
|
+
readonly engramId: string;
|
|
18
|
+
constructor(engramId: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class MikoshiMemoryPullCloudNotFoundError extends Error {
|
|
21
|
+
readonly sourceEngramId: string;
|
|
22
|
+
constructor(sourceEngramId: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class MikoshiMemoryPullDecryptError extends Error {
|
|
25
|
+
constructor(cause?: unknown);
|
|
26
|
+
}
|
|
27
|
+
export declare class MikoshiMemoryPull {
|
|
28
|
+
private readonly localRepo;
|
|
29
|
+
private readonly mikoshi;
|
|
30
|
+
constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
|
|
31
|
+
/**
|
|
32
|
+
* Phase 1: download + decrypt + diff。書き込みはまだしない。
|
|
33
|
+
*/
|
|
34
|
+
check(engramId: string, passphrase: string): Promise<{
|
|
35
|
+
result: MikoshiMemoryPullResult;
|
|
36
|
+
apply?: () => Promise<void>;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|