@ectplsm/relic 0.5.0 → 0.5.2

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/README.md CHANGED
@@ -21,6 +21,7 @@ Relic manages AI **Engrams** (memory + personality) and injects them across codi
21
21
  - [Engram Management](#engram-management)
22
22
  - [Cloud Storage and Sharing](#cloud-storage-and-sharing)
23
23
  - [Configuration](#configuration)
24
+ - [Roadmap](#roadmap)
24
25
 
25
26
  ## Requirements
26
27
 
@@ -188,6 +189,8 @@ For LLM-assisted creation, persona authoring, template examples, and deletion ru
188
189
 
189
190
  Relic can push plaintext persona files and (end-to-end) encrypted memory files to [Mikoshi](https://mikoshi.ectplsm.com), so you can keep Engrams in the cloud and move them across machines without turning Mikoshi into your authoring source of truth.
190
191
 
192
+ This includes avatar-aware persona push/pull: local image paths and external `https://` avatar URLs can be snapshotted into Mikoshi-managed storage during `relic mikoshi push`.
193
+
191
194
  For setup, API key configuration, persona push/pull, encrypted memory sync, and the recommended command flow, see [docs/mikoshi.md](docs/mikoshi.md).
192
195
 
193
196
  ## Configuration
@@ -197,6 +200,14 @@ Use `relic config` to manage the default Engram, Claw path, memory window, and d
197
200
 
198
201
  For command examples and precedence rules, see [docs/configuration.md](docs/configuration.md).
199
202
 
203
+ ## Roadmap
204
+
205
+ Upcoming milestones that shape where Relic is heading:
206
+
207
+ - **Tighter OpenClaw integration** — deeper interop with OpenClaw and other Claw-based frameworks, beyond the current push / pull / sync surface.
208
+ - **Structured memory decomposition** — break `MEMORY.md` down into episodic, semantic, and procedural memory so the Construct can recall the right kind of knowledge at the right time.
209
+ - **Memory Wiki on llm-wiki** — build a navigable memory wiki on top of [llm-wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f), turning long-term memory into a browsable, linkable knowledge base instead of flat Markdown.
210
+
200
211
  ## License
201
212
 
202
213
  [MIT](./LICENCE.md)
@@ -1,4 +1,4 @@
1
- import type { Engram, EngramMeta } from "../../core/entities/engram.js";
1
+ import type { Engram, EngramManifest, EngramMeta } from "../../core/entities/engram.js";
2
2
  import type { EngramRepository } from "../../core/ports/engram-repository.js";
3
3
  /**
4
4
  * LocalEngramRepository — ローカルファイルシステム上の
@@ -24,6 +24,8 @@ export declare class LocalEngramRepository implements EngramRepository {
24
24
  get(id: string): Promise<Engram | null>;
25
25
  save(engram: Engram): Promise<void>;
26
26
  delete(id: string): Promise<void>;
27
+ getEngramPath(id: string): string;
28
+ updateManifest(id: string, manifest: EngramManifest): Promise<void>;
27
29
  copyArchiveFiles(fromId: string, toId: string): Promise<boolean>;
28
30
  private readMeta;
29
31
  private toProfile;
@@ -91,6 +91,27 @@ export class LocalEngramRepository {
91
91
  await rm(engramDir, { recursive: true });
92
92
  }
93
93
  }
94
+ getEngramPath(id) {
95
+ return join(this.basePath, id);
96
+ }
97
+ async updateManifest(id, manifest) {
98
+ const engramDir = join(this.basePath, id);
99
+ if (!existsSync(engramDir))
100
+ return;
101
+ const manifestPath = join(engramDir, MANIFEST_FILE);
102
+ const payload = {
103
+ id: manifest.id,
104
+ createdAt: manifest.createdAt,
105
+ updatedAt: manifest.updatedAt,
106
+ ...(manifest.avatarHash !== undefined
107
+ ? { avatarHash: manifest.avatarHash }
108
+ : {}),
109
+ ...(manifest.avatarSourceUrl !== undefined
110
+ ? { avatarSourceUrl: manifest.avatarSourceUrl }
111
+ : {}),
112
+ };
113
+ await writeFile(manifestPath, JSON.stringify(payload, null, 2), "utf-8");
114
+ }
94
115
  async copyArchiveFiles(fromId, toId) {
95
116
  const ARCHIVE_FILES = ["archive.md", "archive.cursor"];
96
117
  const fromDir = join(this.basePath, fromId);
@@ -148,6 +169,10 @@ export class LocalEngramRepository {
148
169
  id: meta.id,
149
170
  createdAt: meta.createdAt,
150
171
  updatedAt: meta.updatedAt,
172
+ ...(meta.avatarHash !== undefined ? { avatarHash: meta.avatarHash } : {}),
173
+ ...(meta.avatarSourceUrl !== undefined
174
+ ? { avatarSourceUrl: meta.avatarSourceUrl }
175
+ : {}),
151
176
  };
152
177
  }
153
178
  async writeMetaFiles(engramDir, meta) {
@@ -1,4 +1,4 @@
1
- import { type MikoshiClient, type MikoshiEngram, type MikoshiEngramDetail, type CreateEngramInput, type CreateEngramResponse, type SyncStatusResponse, type UpdatePersonaInput, type UpdatePersonaResponse, type UploadMemoryInput, type UploadMemoryResponse, type DownloadMemoryResponse } from "../../core/ports/mikoshi.js";
1
+ import { type MikoshiClient, type MikoshiEngram, type MikoshiEngramDetail, type CreateEngramInput, type CreateEngramResponse, type SyncStatusResponse, type UpdatePersonaInput, type UpdatePersonaResponse, type UploadMemoryInput, type UploadMemoryResponse, type DownloadMemoryResponse, type UploadEngramAvatarResponse, type DeleteEngramAvatarResponse } from "../../core/ports/mikoshi.js";
2
2
  /**
3
3
  * MikoshiApiClient — Mikoshi REST API v1 の adapter 実装
4
4
  *
@@ -17,5 +17,7 @@ export declare class MikoshiApiClient implements MikoshiClient {
17
17
  updatePersona(engramId: string, input: UpdatePersonaInput): Promise<UpdatePersonaResponse>;
18
18
  uploadMemory(engramId: string, input: UploadMemoryInput): Promise<UploadMemoryResponse>;
19
19
  downloadMemory(engramId: string): Promise<DownloadMemoryResponse>;
20
+ uploadEngramAvatar(engramId: string, data: Buffer, mimeType: string): Promise<UploadEngramAvatarResponse>;
21
+ deleteEngramAvatar(engramId: string): Promise<DeleteEngramAvatarResponse>;
20
22
  private request;
21
23
  }
@@ -1,4 +1,4 @@
1
- import { MikoshiApiError, MikoshiEngramSchema, MikoshiEngramDetailSchema, CreateEngramResponseSchema, SyncStatusResponseSchema, UpdatePersonaResponseSchema, UploadMemoryResponseSchema, DownloadMemoryResponseSchema, } from "../../core/ports/mikoshi.js";
1
+ import { MikoshiApiError, MikoshiEngramSchema, MikoshiEngramDetailSchema, CreateEngramResponseSchema, SyncStatusResponseSchema, UpdatePersonaResponseSchema, UploadMemoryResponseSchema, DownloadMemoryResponseSchema, UploadEngramAvatarResponseSchema, DeleteEngramAvatarResponseSchema, } from "../../core/ports/mikoshi.js";
2
2
  import { z } from "zod";
3
3
  /**
4
4
  * MikoshiApiClient — Mikoshi REST API v1 の adapter 実装
@@ -49,6 +49,18 @@ export class MikoshiApiClient {
49
49
  const data = await this.request("GET", `/api/v1/engrams/${enc(engramId)}/memory`);
50
50
  return DownloadMemoryResponseSchema.parse(data);
51
51
  }
52
+ async uploadEngramAvatar(engramId, data, mimeType) {
53
+ const form = new FormData();
54
+ // Blob は Uint8Array を受け付ける (Node 18+ 標準)
55
+ const blob = new Blob([new Uint8Array(data)], { type: mimeType });
56
+ form.append("file", blob, filenameForMimeType(mimeType));
57
+ const raw = await this.request("PUT", `/api/v1/engrams/${enc(engramId)}/avatar`, form);
58
+ return UploadEngramAvatarResponseSchema.parse(raw);
59
+ }
60
+ async deleteEngramAvatar(engramId) {
61
+ const raw = await this.request("DELETE", `/api/v1/engrams/${enc(engramId)}/avatar`);
62
+ return DeleteEngramAvatarResponseSchema.parse(raw);
63
+ }
52
64
  // -----------------------------------------------------------------------
53
65
  // Internal
54
66
  // -----------------------------------------------------------------------
@@ -60,8 +72,14 @@ export class MikoshiApiClient {
60
72
  };
61
73
  const init = { method, headers };
62
74
  if (body !== undefined) {
63
- headers["Content-Type"] = "application/json";
64
- init.body = JSON.stringify(body);
75
+ if (body instanceof FormData) {
76
+ // multipart boundary は fetch に自動で決めさせる
77
+ init.body = body;
78
+ }
79
+ else {
80
+ headers["Content-Type"] = "application/json";
81
+ init.body = JSON.stringify(body);
82
+ }
65
83
  }
66
84
  const res = await fetch(url, init);
67
85
  if (!res.ok) {
@@ -82,3 +100,15 @@ export class MikoshiApiClient {
82
100
  function enc(segment) {
83
101
  return encodeURIComponent(segment);
84
102
  }
103
+ /**
104
+ * multipart/form-data 送信時のダミーファイル名。
105
+ * Mikoshi 側は MIME と内容で判定するので拡張子さえ合っていればよい。
106
+ */
107
+ function filenameForMimeType(mimeType) {
108
+ switch (mimeType) {
109
+ case "image/jpeg": return "avatar.jpg";
110
+ case "image/png": return "avatar.png";
111
+ case "image/webp": return "avatar.webp";
112
+ default: return "avatar";
113
+ }
114
+ }
@@ -43,7 +43,7 @@ export class CodexShell {
43
43
  if (!options?.skipInjection) {
44
44
  args.push("-c", `developer_instructions=${JSON.stringify(wrapWithOverride(prompt))}`);
45
45
  }
46
- args.push("-c", "features.codex_hooks=true", ...(options?.extraArgs ?? []));
46
+ args.push("-c", "features.hooks=true", ...(options?.extraArgs ?? []));
47
47
  const env = {};
48
48
  if (options?.engramId)
49
49
  env.RELIC_ENGRAM_ID = options.engramId;
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { exec } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
5
5
  import { join } from "node:path";
6
- import { homedir, tmpdir } from "node:os";
6
+ import { homedir } from "node:os";
7
7
  import { spawnShell, writeTempPrompt } from "./spawn-shell.js";
8
8
  import { setupGeminiHook, isGeminiHookSetup, writeGeminiHookScript } from "./gemini-hook.js";
9
9
  const execAsync = promisify(exec);
@@ -16,45 +16,45 @@ const RELIC_ENGRAM_END = "<!-- RELIC ENGRAM END -->";
16
16
  * 組み込みシステムプロンプトをキャプチャして返す。
17
17
  * キャプチャ結果は ~/.relic/gemini-system-default.md にキャッシュされる。
18
18
  */
19
- async function captureDefaultSystemPrompt(command) {
20
- const tempDir = mkdtempSync(join(tmpdir(), "relic-gemini-capture-"));
21
- const geminiDir = join(tempDir, ".gemini");
19
+ async function captureDefaultSystemPrompt(command, cwd = process.cwd()) {
20
+ const geminiDir = join(cwd, ".gemini");
22
21
  mkdirSync(geminiDir, { recursive: true });
23
- try {
24
- await new Promise((resolve) => {
25
- const child = spawn(command, [], {
26
- cwd: tempDir,
27
- env: { ...process.env, GEMINI_WRITE_SYSTEM_MD: "true" },
28
- // stdin を /dev/null 相当にして TTY なしで起動
29
- stdio: ["ignore", "pipe", "pipe"],
30
- });
31
- // ファイル書き出し猶予として5秒後に強制終了
32
- const timeout = setTimeout(() => {
33
- child.kill("SIGTERM");
34
- }, 5000);
35
- child.on("close", () => {
36
- clearTimeout(timeout);
37
- resolve();
38
- });
39
- child.on("error", () => {
40
- clearTimeout(timeout);
41
- resolve();
42
- });
22
+ await new Promise((resolve) => {
23
+ const child = spawn(command, [], {
24
+ cwd,
25
+ env: { ...process.env, GEMINI_WRITE_SYSTEM_MD: "true" },
26
+ // stdin を /dev/null 相当にして TTY なしで起動
27
+ stdio: ["ignore", "pipe", "pipe"],
43
28
  });
44
- const systemMdPath = join(geminiDir, "system.md");
45
- if (!existsSync(systemMdPath)) {
46
- throw new Error("Failed to capture Gemini default system prompt.\n" +
47
- "Run manually: GEMINI_WRITE_SYSTEM_MD=true gemini\n" +
48
- `Then copy .gemini/system.md to ${GEMINI_DEFAULT_CACHE}`);
49
- }
50
- const content = readFileSync(systemMdPath, "utf-8");
51
- mkdirSync(RELIC_DIR, { recursive: true });
52
- writeFileSync(GEMINI_DEFAULT_CACHE, content, "utf-8");
53
- return content;
29
+ // ファイル書き出し猶予として5秒後に強制終了
30
+ const timeout = setTimeout(() => {
31
+ child.kill("SIGTERM");
32
+ }, 5000);
33
+ child.on("close", () => {
34
+ clearTimeout(timeout);
35
+ resolve();
36
+ });
37
+ child.on("error", () => {
38
+ clearTimeout(timeout);
39
+ resolve();
40
+ });
41
+ });
42
+ const systemMdPath = join(geminiDir, "system.md");
43
+ if (!existsSync(systemMdPath)) {
44
+ throw new Error("Failed to capture Gemini default system prompt.\n" +
45
+ `Run manually from ${cwd}: GEMINI_WRITE_SYSTEM_MD=true gemini\n` +
46
+ `Then copy ${systemMdPath} to ${GEMINI_DEFAULT_CACHE}`);
47
+ }
48
+ const content = readFileSync(systemMdPath, "utf-8");
49
+ mkdirSync(RELIC_DIR, { recursive: true });
50
+ writeFileSync(GEMINI_DEFAULT_CACHE, content, "utf-8");
51
+ try {
52
+ unlinkSync(systemMdPath);
54
53
  }
55
- finally {
56
- rmSync(tempDir, { recursive: true, force: true });
54
+ catch {
55
+ // Cache creation succeeded; cleanup failure should not block launch.
57
56
  }
57
+ return content;
58
58
  }
59
59
  /**
60
60
  * デフォルトプロンプトにEngramセクションを追記 or 置換する(冪等)。
@@ -121,7 +121,7 @@ export class GeminiShell {
121
121
  }
122
122
  else {
123
123
  console.log("Capturing Gemini default system prompt (first run only)...");
124
- defaultPrompt = await captureDefaultSystemPrompt(this.command);
124
+ defaultPrompt = await captureDefaultSystemPrompt(this.command, options?.cwd);
125
125
  console.log(`Cached to: ${GEMINI_DEFAULT_CACHE}`);
126
126
  console.log();
127
127
  }
@@ -66,14 +66,22 @@ export declare const EngramManifestSchema: z.ZodObject<{
66
66
  createdAt: z.ZodString;
67
67
  /** 最終更新日時 */
68
68
  updatedAt: z.ZodString;
69
+ /** 前回 push 時の avatar 画像ファイルの SHA-256 ハッシュ */
70
+ avatarHash: z.ZodOptional<z.ZodString>;
71
+ /** 前回 push 時に使った外部 avatar URL */
72
+ avatarSourceUrl: z.ZodOptional<z.ZodString>;
69
73
  }, "strip", z.ZodTypeAny, {
70
74
  id: string;
71
75
  createdAt: string;
72
76
  updatedAt: string;
77
+ avatarHash?: string | undefined;
78
+ avatarSourceUrl?: string | undefined;
73
79
  }, {
74
80
  id: string;
75
81
  createdAt: string;
76
82
  updatedAt: string;
83
+ avatarHash?: string | undefined;
84
+ avatarSourceUrl?: string | undefined;
77
85
  }>;
78
86
  export type EngramManifest = z.infer<typeof EngramManifestSchema>;
79
87
  /**
@@ -93,6 +101,10 @@ export declare const EngramMetaSchema: z.ZodObject<{
93
101
  createdAt: z.ZodString;
94
102
  /** 最終更新日時 */
95
103
  updatedAt: z.ZodString;
104
+ /** 前回 push 時の avatar 画像ファイルの SHA-256 ハッシュ */
105
+ avatarHash: z.ZodOptional<z.ZodString>;
106
+ /** 前回 push 時に使った外部 avatar URL */
107
+ avatarSourceUrl: z.ZodOptional<z.ZodString>;
96
108
  }, "strip", z.ZodTypeAny, {
97
109
  name: string;
98
110
  id: string;
@@ -100,6 +112,8 @@ export declare const EngramMetaSchema: z.ZodObject<{
100
112
  updatedAt: string;
101
113
  description?: string | undefined;
102
114
  tags?: string[] | undefined;
115
+ avatarHash?: string | undefined;
116
+ avatarSourceUrl?: string | undefined;
103
117
  }, {
104
118
  name: string;
105
119
  id: string;
@@ -107,6 +121,8 @@ export declare const EngramMetaSchema: z.ZodObject<{
107
121
  updatedAt: string;
108
122
  description?: string | undefined;
109
123
  tags?: string[] | undefined;
124
+ avatarHash?: string | undefined;
125
+ avatarSourceUrl?: string | undefined;
110
126
  }>;
111
127
  export type EngramMeta = z.infer<typeof EngramMetaSchema>;
112
128
  /**
@@ -128,6 +144,10 @@ export declare const EngramSchema: z.ZodObject<{
128
144
  createdAt: z.ZodString;
129
145
  /** 最終更新日時 */
130
146
  updatedAt: z.ZodString;
147
+ /** 前回 push 時の avatar 画像ファイルの SHA-256 ハッシュ */
148
+ avatarHash: z.ZodOptional<z.ZodString>;
149
+ /** 前回 push 時に使った外部 avatar URL */
150
+ avatarSourceUrl: z.ZodOptional<z.ZodString>;
131
151
  }, "strip", z.ZodTypeAny, {
132
152
  name: string;
133
153
  id: string;
@@ -135,6 +155,8 @@ export declare const EngramSchema: z.ZodObject<{
135
155
  updatedAt: string;
136
156
  description?: string | undefined;
137
157
  tags?: string[] | undefined;
158
+ avatarHash?: string | undefined;
159
+ avatarSourceUrl?: string | undefined;
138
160
  }, {
139
161
  name: string;
140
162
  id: string;
@@ -142,6 +164,8 @@ export declare const EngramSchema: z.ZodObject<{
142
164
  updatedAt: string;
143
165
  description?: string | undefined;
144
166
  tags?: string[] | undefined;
167
+ avatarHash?: string | undefined;
168
+ avatarSourceUrl?: string | undefined;
145
169
  }>;
146
170
  files: z.ZodObject<{
147
171
  /** 人格の核となる指示・行動原理 */
@@ -183,6 +207,8 @@ export declare const EngramSchema: z.ZodObject<{
183
207
  updatedAt: string;
184
208
  description?: string | undefined;
185
209
  tags?: string[] | undefined;
210
+ avatarHash?: string | undefined;
211
+ avatarSourceUrl?: string | undefined;
186
212
  };
187
213
  files: {
188
214
  soul: string;
@@ -201,6 +227,8 @@ export declare const EngramSchema: z.ZodObject<{
201
227
  updatedAt: string;
202
228
  description?: string | undefined;
203
229
  tags?: string[] | undefined;
230
+ avatarHash?: string | undefined;
231
+ avatarSourceUrl?: string | undefined;
204
232
  };
205
233
  files: {
206
234
  soul: string;
@@ -43,6 +43,10 @@ export const EngramManifestSchema = z.object({
43
43
  createdAt: z.string().datetime(),
44
44
  /** 最終更新日時 */
45
45
  updatedAt: z.string().datetime(),
46
+ /** 前回 push 時の avatar 画像ファイルの SHA-256 ハッシュ */
47
+ avatarHash: z.string().optional(),
48
+ /** 前回 push 時に使った外部 avatar URL */
49
+ avatarSourceUrl: z.string().url().optional(),
46
50
  });
47
51
  /**
48
52
  * Engramメタデータ — プロフィールとマニフェストを結合した利用時ビュー
@@ -1,4 +1,4 @@
1
- import type { Engram, EngramMeta } from "../entities/engram.js";
1
+ import type { Engram, EngramManifest, EngramMeta } from "../entities/engram.js";
2
2
  /**
3
3
  * EngramRepository — Engram永続化層の抽象ポート
4
4
  *
@@ -12,8 +12,24 @@ export interface EngramRepository {
12
12
  get(id: string): Promise<Engram | null>;
13
13
  /** Engramを保存(作成 or 更新) */
14
14
  save(engram: Engram): Promise<void>;
15
+ /**
16
+ * マニフェストだけを差分書き換えする軽量更新。
17
+ *
18
+ * `save()` は全ファイルを書き戻してしまうので、avatarHash のような
19
+ * マニフェスト内のフィールドだけを安全に更新したい時に使う。
20
+ * 対象 Engram が存在しない場合の挙動は実装に委ねる(例外または no-op)。
21
+ */
22
+ updateManifest(id: string, manifest: EngramManifest): Promise<void>;
15
23
  /** Engramを削除 */
16
24
  delete(id: string): Promise<void>;
17
25
  /** アーカイブファイル (archive.md, archive.cursor) を別Engramへコピー */
18
26
  copyArchiveFiles(fromId: string, toId: string): Promise<boolean>;
27
+ /**
28
+ * Engramのディレクトリ絶対パスを返す。
29
+ *
30
+ * 存在判定は行わず、リポジトリが想定するパスを返すだけ。
31
+ * avatar 画像の読み書きなどファイルシステム直接アクセスが必要な
32
+ * usecase から使う。リモート専用リポジトリでは null を返してよい。
33
+ */
34
+ getEngramPath(id: string): string | null;
19
35
  }
@@ -906,6 +906,26 @@ export declare const DownloadMemoryResponseSchema: z.ZodDiscriminatedUnion<"hasM
906
906
  engramId: string;
907
907
  }>]>;
908
908
  export type DownloadMemoryResponse = z.infer<typeof DownloadMemoryResponseSchema>;
909
+ /** クライアントが許可する avatar MIME タイプ */
910
+ export declare const AVATAR_SUPPORTED_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/webp"];
911
+ /** クライアント側事前バリデーションの最大バイト数 (Mikoshi 側と一致) */
912
+ export declare const AVATAR_MAX_BYTES: number;
913
+ export declare const UploadEngramAvatarResponseSchema: z.ZodObject<{
914
+ avatarUrl: z.ZodString;
915
+ }, "strip", z.ZodTypeAny, {
916
+ avatarUrl: string;
917
+ }, {
918
+ avatarUrl: string;
919
+ }>;
920
+ export type UploadEngramAvatarResponse = z.infer<typeof UploadEngramAvatarResponseSchema>;
921
+ export declare const DeleteEngramAvatarResponseSchema: z.ZodObject<{
922
+ avatarUrl: z.ZodNull;
923
+ }, "strip", z.ZodTypeAny, {
924
+ avatarUrl: null;
925
+ }, {
926
+ avatarUrl: null;
927
+ }>;
928
+ export type DeleteEngramAvatarResponse = z.infer<typeof DeleteEngramAvatarResponseSchema>;
909
929
  export declare class MikoshiApiError extends Error {
910
930
  readonly status: number;
911
931
  readonly code: string | undefined;
@@ -934,4 +954,8 @@ export interface MikoshiClient {
934
954
  uploadMemory(engramId: string, input: UploadMemoryInput): Promise<UploadMemoryResponse>;
935
955
  /** 暗号化メモリバンドルをダウンロード */
936
956
  downloadMemory(engramId: string): Promise<DownloadMemoryResponse>;
957
+ /** Avatar 画像をアップロード (multipart/form-data) */
958
+ uploadEngramAvatar(engramId: string, data: Buffer, mimeType: string): Promise<UploadEngramAvatarResponse>;
959
+ /** Avatar 画像を削除 */
960
+ deleteEngramAvatar(engramId: string): Promise<DeleteEngramAvatarResponse>;
937
961
  }
@@ -188,6 +188,23 @@ export const DownloadMemoryResponseSchema = z.discriminatedUnion("hasMemory", [
188
188
  }),
189
189
  ]);
190
190
  // ---------------------------------------------------------------------------
191
+ // Avatar upload / delete
192
+ // ---------------------------------------------------------------------------
193
+ /** クライアントが許可する avatar MIME タイプ */
194
+ export const AVATAR_SUPPORTED_MIME_TYPES = [
195
+ "image/jpeg",
196
+ "image/png",
197
+ "image/webp",
198
+ ];
199
+ /** クライアント側事前バリデーションの最大バイト数 (Mikoshi 側と一致) */
200
+ export const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
201
+ export const UploadEngramAvatarResponseSchema = z.object({
202
+ avatarUrl: z.string(),
203
+ });
204
+ export const DeleteEngramAvatarResponseSchema = z.object({
205
+ avatarUrl: z.null(),
206
+ });
207
+ // ---------------------------------------------------------------------------
191
208
  // Error types
192
209
  // ---------------------------------------------------------------------------
193
210
  export class MikoshiApiError extends Error {
@@ -0,0 +1,121 @@
1
+ export type AvatarRef = {
2
+ kind: "path";
3
+ value: string;
4
+ } | {
5
+ kind: "url";
6
+ value: string;
7
+ };
8
+ export interface FetchedAvatar {
9
+ bytes: Buffer;
10
+ mimeType: string;
11
+ finalUrl: string;
12
+ }
13
+ /**
14
+ * IDENTITY.md から Avatar フィールドの生の値を抽出する。
15
+ *
16
+ * - マッチしなければ null
17
+ * - 値が URL(http://, https://)の場合は Mikoshi 管理外として null
18
+ * - 空白のみの値も null として扱う
19
+ */
20
+ export declare function parseAvatarPath(identity: string): string | null;
21
+ /**
22
+ * IDENTITY.md から Avatar フィールドの参照先を抽出する。
23
+ *
24
+ * - マッチしなければ null
25
+ * - 空白のみの値も null
26
+ * - `http(s)://` は URL として返す
27
+ * - それ以外は path として返す
28
+ */
29
+ export declare function parseAvatarRef(identity: string): AvatarRef | null;
30
+ /**
31
+ * Avatar パスを絶対パスに解決する。
32
+ *
33
+ * - 絶対パス → そのまま
34
+ * - 相対パス → Engram ディレクトリ基準で resolve
35
+ */
36
+ export declare function resolveAvatarPath(rawPath: string, engramDir: string): string;
37
+ /**
38
+ * ファイル内容の SHA-256 ハッシュを計算する。
39
+ *
40
+ * 出力形式: "sha256:<64 lowercase hex>"
41
+ *
42
+ * ストリーム処理なので 2MB 制限を超える巨大ファイルでも OOM しない。
43
+ */
44
+ export declare function computeAvatarHash(filePath: string): Promise<string>;
45
+ export declare function computeAvatarHashFromBytes(bytes: Uint8Array): string;
46
+ /**
47
+ * 拡張子から MIME タイプを判定する。
48
+ *
49
+ * 許可フォーマット: JPEG, PNG, WebP
50
+ * 対応外なら null(呼び出し側でエラーにする)
51
+ *
52
+ * バイナリ判定は Mikoshi 側の sharp に任せる方針。
53
+ * クライアントは軽量な事前バリデーションのみ。
54
+ */
55
+ export declare function detectAvatarMimeType(filePath: string): string | null;
56
+ /**
57
+ * レスポンス header / magic number の両方から MIME を判定する。
58
+ *
59
+ * 両方あって不一致なら null を返す。
60
+ */
61
+ export declare function detectAvatarMimeTypeFromBytes(bytes: Uint8Array, headerMimeType?: string | null): string | null;
62
+ export declare function isHttpsUrl(raw: string): boolean;
63
+ /**
64
+ * ホストが private / loopback / link-local なら true。
65
+ *
66
+ * hostname の場合は DNS lookup して、返ってきた全アドレスを検査する。
67
+ * DNS rebinding の完全対策ではない。事故防止用。
68
+ */
69
+ export declare function isPrivateHost(url: URL): Promise<boolean>;
70
+ export declare function fetchAvatarFromUrl(url: string, maxBytes: number, timeoutMs: number): Promise<FetchedAvatar>;
71
+ /**
72
+ * IDENTITY.md を Avatar 行の前後で分離した結果。
73
+ *
74
+ * Avatar 行が存在しない場合は `rawValue === null`、`before` に全内容、
75
+ * `after` に空文字が入る(呼び出し側が「Avatar 行無し」として判定できる)。
76
+ */
77
+ export interface AvatarSplit {
78
+ /** Avatar 行の値の直前までの文字列(`- **Avatar:** ` prefix を含む) */
79
+ before: string;
80
+ /** Avatar 行の値(行末改行含まず)。Avatar 行が無ければ null */
81
+ rawValue: string | null;
82
+ /** Avatar 行の値の直後以降の文字列(行末改行とその後の内容) */
83
+ after: string;
84
+ }
85
+ /**
86
+ * IDENTITY.md を Avatar 行の値を境に前後分離する。
87
+ *
88
+ * 書き換えと drift 検出の両方で使う基本操作。
89
+ */
90
+ export declare function splitAroundAvatar(identity: string): AvatarSplit;
91
+ /**
92
+ * IDENTITY.md の Avatar 行の値だけを差し替えて新しい文字列を返す。
93
+ *
94
+ * Avatar 行が存在しない場合は元の identity をそのまま返す
95
+ * (新しい Avatar 行は挿入しない — 保守的な挙動)。
96
+ */
97
+ export declare function rewriteAvatarValue(identity: string, newValue: string): string;
98
+ /**
99
+ * avatar-only drift の検出結果。
100
+ *
101
+ * drift === true の場合のみ `localValue` / `remoteValue` が
102
+ * 非 null(両方に Avatar 行が存在し、値だけが違う)。
103
+ */
104
+ export interface AvatarOnlyDrift {
105
+ drift: boolean;
106
+ localValue: string | null;
107
+ remoteValue: string | null;
108
+ }
109
+ /**
110
+ * 2つの IDENTITY.md の差分が Avatar 行の値だけかどうかを判定する。
111
+ *
112
+ * 判定条件:
113
+ * - 両方に Avatar 行が存在する
114
+ * - Avatar 行の前後 (`before` / `after`) が完全一致
115
+ * - Avatar 行の値だけ異なる
116
+ *
117
+ * PATH 前後の format 揺れ(改行数・空白数など)は
118
+ * `before` / `after` の文字列一致にそのまま反映されるため、
119
+ * 揺れがあれば avatar-only drift とは判定されない(false positive を避ける)。
120
+ */
121
+ export declare function isAvatarOnlyDrift(local: string, remote: string): AvatarOnlyDrift;