@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,15 @@
1
+ /**
2
+ * Persona hash canonicalization — mikoshi-sync.md 準拠
3
+ *
4
+ * 正規化ルール:
5
+ * - UTF-8 BOM 除去
6
+ * - 改行を \n に統一
7
+ * - 末尾 \n を strip
8
+ *
9
+ * Canonical payload:
10
+ * header \n SOUL.md \n byteLen \n content \n IDENTITY.md \n byteLen \n content
11
+ *
12
+ * 出力: "sha256:<64 lowercase hex>"
13
+ * soul / identity どちらかが欠落なら null
14
+ */
15
+ export declare function computePersonaHash(soul: string | undefined, identity: string | undefined): string | null;
@@ -0,0 +1,36 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeText } from "./normalize.js";
3
+ const HEADER = "mikoshi.persona.v1";
4
+ /**
5
+ * Persona hash canonicalization — mikoshi-sync.md 準拠
6
+ *
7
+ * 正規化ルール:
8
+ * - UTF-8 BOM 除去
9
+ * - 改行を \n に統一
10
+ * - 末尾 \n を strip
11
+ *
12
+ * Canonical payload:
13
+ * header \n SOUL.md \n byteLen \n content \n IDENTITY.md \n byteLen \n content
14
+ *
15
+ * 出力: "sha256:<64 lowercase hex>"
16
+ * soul / identity どちらかが欠落なら null
17
+ */
18
+ export function computePersonaHash(soul, identity) {
19
+ if (!soul || !identity)
20
+ return null;
21
+ const normSoul = normalizeText(soul, { stripTrailingNewlines: true });
22
+ const normIdentity = normalizeText(identity, { stripTrailingNewlines: true });
23
+ const soulBytes = Buffer.byteLength(normSoul, "utf-8");
24
+ const identityBytes = Buffer.byteLength(normIdentity, "utf-8");
25
+ const payload = [
26
+ HEADER,
27
+ "SOUL.md",
28
+ String(soulBytes),
29
+ normSoul,
30
+ "IDENTITY.md",
31
+ String(identityBytes),
32
+ normIdentity,
33
+ ].join("\n");
34
+ const hex = createHash("sha256").update(payload, "utf-8").digest("hex");
35
+ return `sha256:${hex}`;
36
+ }
@@ -0,0 +1,44 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ export type ClawPullPersonaFileDiff = "missing" | "same" | "different";
3
+ export interface ClawPullDiff {
4
+ soulDiff: ClawPullPersonaFileDiff;
5
+ identityDiff: ClawPullPersonaFileDiff;
6
+ sourcePath: string;
7
+ remoteSoul?: string;
8
+ remoteIdentity?: string;
9
+ }
10
+ export type ClawPullOutcome = "pulled" | "already_synced";
11
+ export interface ClawPullResult {
12
+ outcome: ClawPullOutcome;
13
+ engramId: string;
14
+ engramName: string;
15
+ sourcePath: string;
16
+ diff?: ClawPullDiff;
17
+ }
18
+ export declare class ClawPullEngramNotFoundError extends Error {
19
+ readonly engramId: string;
20
+ constructor(engramId: string);
21
+ }
22
+ export declare class ClawPullWorkspaceNotFoundError extends Error {
23
+ readonly path: string;
24
+ constructor(path: string);
25
+ }
26
+ export declare class ClawPullPersonaMissingError extends Error {
27
+ readonly path: string;
28
+ constructor(path: string);
29
+ }
30
+ export declare class ClawPull {
31
+ private readonly localRepo;
32
+ constructor(localRepo: EngramRepository);
33
+ /**
34
+ * Phase 1: read workspace + diff. Does not write anything yet.
35
+ *
36
+ * Returns an `apply` closure when there are changes to write.
37
+ */
38
+ check(agentName: string, options?: {
39
+ openclawDir?: string;
40
+ }): Promise<{
41
+ result: ClawPullResult;
42
+ apply?: () => Promise<void>;
43
+ }>;
44
+ }
@@ -0,0 +1,117 @@
1
+ import { join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { RELIC_FILE_MAP, resolveWorkspacePath } from "../../shared/openclaw.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Errors
7
+ // ---------------------------------------------------------------------------
8
+ export class ClawPullEngramNotFoundError extends Error {
9
+ engramId;
10
+ constructor(engramId) {
11
+ super(`Engram "${engramId}" not found locally. Use "relic claw pull" to create or update it.`);
12
+ this.engramId = engramId;
13
+ this.name = "ClawPullEngramNotFoundError";
14
+ }
15
+ }
16
+ export class ClawPullWorkspaceNotFoundError extends Error {
17
+ path;
18
+ constructor(path) {
19
+ super(`Claw workspace not found at ${path}`);
20
+ this.path = path;
21
+ this.name = "ClawPullWorkspaceNotFoundError";
22
+ }
23
+ }
24
+ export class ClawPullPersonaMissingError extends Error {
25
+ path;
26
+ constructor(path) {
27
+ super(`No persona files (SOUL.md, IDENTITY.md) found in ${path}`);
28
+ this.path = path;
29
+ this.name = "ClawPullPersonaMissingError";
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Usecase
34
+ // ---------------------------------------------------------------------------
35
+ export class ClawPull {
36
+ localRepo;
37
+ constructor(localRepo) {
38
+ this.localRepo = localRepo;
39
+ }
40
+ /**
41
+ * Phase 1: read workspace + diff. Does not write anything yet.
42
+ *
43
+ * Returns an `apply` closure when there are changes to write.
44
+ */
45
+ async check(agentName, options) {
46
+ const sourcePath = resolveWorkspacePath(agentName, options?.openclawDir);
47
+ if (!existsSync(sourcePath)) {
48
+ throw new ClawPullWorkspaceNotFoundError(sourcePath);
49
+ }
50
+ // 1. Local Engram must exist
51
+ const local = await this.localRepo.get(agentName);
52
+ if (!local) {
53
+ throw new ClawPullEngramNotFoundError(agentName);
54
+ }
55
+ // 2. Read persona files from workspace
56
+ const remoteSoul = await readOptionalFile(join(sourcePath, RELIC_FILE_MAP.soul));
57
+ const remoteIdentity = await readOptionalFile(join(sourcePath, RELIC_FILE_MAP.identity));
58
+ if (!remoteSoul && !remoteIdentity) {
59
+ throw new ClawPullPersonaMissingError(sourcePath);
60
+ }
61
+ // 3. Diff
62
+ const soulDiff = diffFile(local.files.soul, remoteSoul);
63
+ const identityDiff = diffFile(local.files.identity, remoteIdentity);
64
+ if (soulDiff !== "different" && identityDiff !== "different") {
65
+ return {
66
+ result: {
67
+ outcome: "already_synced",
68
+ engramId: agentName,
69
+ engramName: local.meta.name,
70
+ sourcePath,
71
+ },
72
+ };
73
+ }
74
+ const diff = {
75
+ soulDiff,
76
+ identityDiff,
77
+ sourcePath,
78
+ remoteSoul,
79
+ remoteIdentity,
80
+ };
81
+ // 4. Apply closure
82
+ const apply = async () => {
83
+ const fresh = await this.localRepo.get(agentName);
84
+ if (!fresh)
85
+ throw new ClawPullEngramNotFoundError(agentName);
86
+ const updatedFiles = { ...fresh.files };
87
+ if (soulDiff === "different" && remoteSoul)
88
+ updatedFiles.soul = remoteSoul;
89
+ if (identityDiff === "different" && remoteIdentity)
90
+ updatedFiles.identity = remoteIdentity;
91
+ await this.localRepo.save({
92
+ meta: { ...fresh.meta, updatedAt: new Date().toISOString() },
93
+ files: updatedFiles,
94
+ });
95
+ };
96
+ return { result: { outcome: "pulled", engramId: agentName, engramName: local.meta.name, sourcePath, diff }, apply };
97
+ }
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Helpers
101
+ // ---------------------------------------------------------------------------
102
+ async function readOptionalFile(filePath) {
103
+ if (!existsSync(filePath))
104
+ return undefined;
105
+ return readFile(filePath, "utf-8");
106
+ }
107
+ function diffFile(localContent, remoteContent) {
108
+ if (!remoteContent)
109
+ return "missing";
110
+ if (!localContent)
111
+ return "different";
112
+ return normalizeContent(localContent) === normalizeContent(remoteContent) ? "same" : "different";
113
+ }
114
+ /** Trim trailing whitespace so minor formatting differences don't cause false diffs. */
115
+ function normalizeContent(content) {
116
+ return content.trimEnd();
117
+ }
@@ -32,7 +32,6 @@ export declare class Extract {
32
32
  execute(agentName: string, options?: {
33
33
  name?: string;
34
34
  openclawDir?: string;
35
- force?: boolean;
36
35
  }): Promise<ExtractResult>;
37
36
  private comparePersonaFile;
38
37
  private readFiles;
@@ -55,7 +55,7 @@ export class Extract {
55
55
  throw new WorkspaceNotFoundError(sourcePath);
56
56
  }
57
57
  const existing = await this.repository.get(agentName);
58
- if (existing && !options?.force) {
58
+ if (existing) {
59
59
  throw new AlreadyExtractedError(agentName);
60
60
  }
61
61
  const { files, filesRead } = await this.readFiles(sourcePath);
@@ -63,31 +63,17 @@ export class Extract {
63
63
  throw new WorkspaceEmptyError(sourcePath);
64
64
  }
65
65
  const now = new Date().toISOString();
66
- const engram = existing && options?.force
67
- ? {
68
- meta: {
69
- ...existing.meta,
70
- name: options?.name ?? existing.meta.name,
71
- updatedAt: now,
72
- },
73
- // Force extract only replaces persona files from Claw.
74
- files: {
75
- ...existing.files,
76
- soul: files.soul ?? existing.files.soul,
77
- identity: files.identity ?? existing.files.identity,
78
- },
79
- }
80
- : {
81
- meta: {
82
- id: agentName,
83
- name: options?.name ?? agentName,
84
- description: `Extracted from OpenClaw workspace (${agentName})`,
85
- createdAt: now,
86
- updatedAt: now,
87
- tags: ["extracted", "openclaw"],
88
- },
89
- files,
90
- };
66
+ const engram = {
67
+ meta: {
68
+ id: agentName,
69
+ name: options?.name ?? agentName,
70
+ description: `Extracted from OpenClaw workspace (${agentName})`,
71
+ createdAt: now,
72
+ updatedAt: now,
73
+ tags: ["extracted", "openclaw"],
74
+ },
75
+ files,
76
+ };
91
77
  await this.repository.save(engram);
92
78
  return {
93
79
  engramId: agentName,
@@ -142,7 +128,7 @@ export class WorkspaceEmptyError extends Error {
142
128
  }
143
129
  export class AlreadyExtractedError extends Error {
144
130
  constructor(id) {
145
- super(`Engram "${id}" already exists. Re-run with "--force" option to overwrite local persona files from the Claw workspace.`);
131
+ super(`Engram "${id}" already exists. Use "relic claw pull" to update it.`);
146
132
  this.name = "AlreadyExtractedError";
147
133
  }
148
134
  }
@@ -6,6 +6,14 @@ export { RefreshSamples, type RefreshSamplesResult, } from "./refresh-samples.js
6
6
  export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, type InjectPersonaFileDiff, type InjectPersonaDiffResult, type InjectResult, } from "./inject.js";
7
7
  export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, type ExtractPersonaFileDiff, type ExtractPersonaDiffResult, type ExtractResult, } from "./extract.js";
8
8
  export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
9
+ export { ClawPull, ClawPullEngramNotFoundError, ClawPullWorkspaceNotFoundError, ClawPullPersonaMissingError, type ClawPullResult, type ClawPullDiff, } from "./claw-pull.js";
9
10
  export { Sync, SyncOpenclawDirNotFoundError, type SyncTarget, type SyncResult, type SyncInitialResult, } from "./sync.js";
10
11
  export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
11
12
  export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";
13
+ export { MikoshiStatus, MikoshiStatusEngramNotFoundError, MikoshiStatusCloudNotFoundError, type MikoshiStatusResult, } from "./mikoshi-status.js";
14
+ export { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaHashError, MikoshiPushPersonaConflictError, type MikoshiPushResult, type MikoshiPushApplyResult, } from "./mikoshi-push.js";
15
+ export { MikoshiDownload, MikoshiDownloadAlreadyExistsError, MikoshiDownloadCloudNotFoundError, MikoshiDownloadPersonaMissingError, type MikoshiDownloadResult, } from "./mikoshi-download.js";
16
+ export { MikoshiPull, MikoshiPullEngramNotFoundError, MikoshiPullCloudNotFoundError, MikoshiPullPersonaMissingError, type MikoshiPullResult, type MikoshiPullDiff, } from "./mikoshi-pull.js";
17
+ export { MikoshiMemoryPush, MikoshiMemoryPushEngramNotFoundError, MikoshiMemoryPushNoFilesError, MikoshiMemoryPushCloudNotFoundError, type MikoshiMemoryPushResult, } from "./mikoshi-memory-push.js";
18
+ export { MikoshiMemoryPull, MikoshiMemoryPullEngramNotFoundError, MikoshiMemoryPullCloudNotFoundError, MikoshiMemoryPullDecryptError, type MikoshiMemoryPullResult, type MikoshiMemoryPullDiff, } from "./mikoshi-memory-pull.js";
19
+ export { MikoshiMemorySync, MikoshiMemorySyncEngramNotFoundError, MikoshiMemorySyncCloudNotFoundError, MikoshiMemorySyncDecryptError, type MikoshiMemorySyncResult, } from "./mikoshi-memory-sync.js";
@@ -6,6 +6,14 @@ export { RefreshSamples, } from "./refresh-samples.js";
6
6
  export { Inject, InjectEngramNotFoundError, InjectClawDirNotFoundError, InjectWorkspaceNotFoundError, } from "./inject.js";
7
7
  export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, AlreadyExtractedError, } from "./extract.js";
8
8
  export { MemoryWrite, MemoryWriteEngramNotFoundError, } from "./memory-write.js";
9
+ export { ClawPull, ClawPullEngramNotFoundError, ClawPullWorkspaceNotFoundError, ClawPullPersonaMissingError, } from "./claw-pull.js";
9
10
  export { Sync, SyncOpenclawDirNotFoundError, } from "./sync.js";
10
11
  export { ArchivePending, ArchivePendingEngramNotFoundError, } from "./archive-pending.js";
11
12
  export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, } from "./archive-cursor-update.js";
13
+ export { MikoshiStatus, MikoshiStatusEngramNotFoundError, MikoshiStatusCloudNotFoundError, } from "./mikoshi-status.js";
14
+ export { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaHashError, MikoshiPushPersonaConflictError, } from "./mikoshi-push.js";
15
+ export { MikoshiDownload, MikoshiDownloadAlreadyExistsError, MikoshiDownloadCloudNotFoundError, MikoshiDownloadPersonaMissingError, } from "./mikoshi-download.js";
16
+ export { MikoshiPull, MikoshiPullEngramNotFoundError, MikoshiPullCloudNotFoundError, MikoshiPullPersonaMissingError, } from "./mikoshi-pull.js";
17
+ export { MikoshiMemoryPush, MikoshiMemoryPushEngramNotFoundError, MikoshiMemoryPushNoFilesError, MikoshiMemoryPushCloudNotFoundError, } from "./mikoshi-memory-push.js";
18
+ export { MikoshiMemoryPull, MikoshiMemoryPullEngramNotFoundError, MikoshiMemoryPullCloudNotFoundError, MikoshiMemoryPullDecryptError, } from "./mikoshi-memory-pull.js";
19
+ export { MikoshiMemorySync, MikoshiMemorySyncEngramNotFoundError, MikoshiMemorySyncCloudNotFoundError, MikoshiMemorySyncDecryptError, } from "./mikoshi-memory-sync.js";
@@ -4,6 +4,7 @@ export interface InjectPersonaDiffResult {
4
4
  engramId: string;
5
5
  engramName: string;
6
6
  targetPath: string;
7
+ targetExists: boolean;
7
8
  soul: InjectPersonaFileDiff;
8
9
  identity: InjectPersonaFileDiff;
9
10
  overwriteRequired: boolean;
@@ -17,6 +17,7 @@ export class Inject {
17
17
  async inspectPersona(engramId, options) {
18
18
  const { engram, targetPath } = await this.loadInjectTarget(engramId, options);
19
19
  const mergeIdentity = options?.mergeIdentity ?? false;
20
+ const targetExists = existsSync(targetPath);
20
21
  const soul = await this.compareTargetFile(join(targetPath, INJECT_FILE_MAP.soul), this.resolveInjectedContent("soul", engram.files, mergeIdentity));
21
22
  const identity = mergeIdentity
22
23
  ? "skipped"
@@ -25,6 +26,7 @@ export class Inject {
25
26
  engramId: engram.meta.id,
26
27
  engramName: engram.meta.name,
27
28
  targetPath,
29
+ targetExists,
28
30
  soul,
29
31
  identity,
30
32
  overwriteRequired: soul === "different" || identity === "different",
@@ -0,0 +1,29 @@
1
+ import type { EngramRepository } from "../ports/engram-repository.js";
2
+ import type { MikoshiClient } from "../ports/mikoshi.js";
3
+ export interface MikoshiCloneResult {
4
+ engramId: string;
5
+ engramName: string;
6
+ cloudEngramId: string;
7
+ }
8
+ export declare class MikoshiCloneAlreadyExistsError extends Error {
9
+ readonly engramId: string;
10
+ constructor(engramId: string);
11
+ }
12
+ export declare class MikoshiCloneCloudNotFoundError extends Error {
13
+ readonly sourceEngramId: string;
14
+ constructor(sourceEngramId: string);
15
+ }
16
+ export declare class MikoshiClonePersonaMissingError extends Error {
17
+ readonly cloudEngramId: string;
18
+ constructor(cloudEngramId: string);
19
+ }
20
+ export declare class MikoshiClone {
21
+ private readonly localRepo;
22
+ private readonly mikoshi;
23
+ constructor(localRepo: EngramRepository, mikoshi: MikoshiClient);
24
+ /**
25
+ * Clone a remote Engram from Mikoshi to local.
26
+ * Fails if the local Engram already exists.
27
+ */
28
+ execute(engramId: string): Promise<MikoshiCloneResult>;
29
+ }
@@ -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
+ }