@ectplsm/relic 0.4.0 → 0.5.1
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 +5 -3
- package/dist/adapters/local/local-engram-repository.d.ts +3 -1
- package/dist/adapters/local/local-engram-repository.js +25 -0
- package/dist/adapters/mikoshi/client.d.ts +3 -1
- package/dist/adapters/mikoshi/client.js +33 -3
- package/dist/core/entities/engram.d.ts +28 -0
- package/dist/core/entities/engram.js +4 -0
- package/dist/core/ports/engram-repository.d.ts +17 -1
- package/dist/core/ports/mikoshi.d.ts +24 -0
- package/dist/core/ports/mikoshi.js +17 -0
- package/dist/core/sync/avatar.d.ts +121 -0
- package/dist/core/sync/avatar.js +352 -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/index.d.ts +3 -3
- package/dist/core/usecases/index.js +2 -2
- package/dist/core/usecases/memory-write.d.ts +5 -0
- package/dist/core/usecases/memory-write.js +26 -16
- package/dist/core/usecases/mikoshi-download.d.ts +5 -0
- package/dist/core/usecases/mikoshi-download.js +17 -1
- package/dist/core/usecases/mikoshi-pull.d.ts +26 -0
- package/dist/core/usecases/mikoshi-pull.js +47 -3
- package/dist/core/usecases/mikoshi-push.d.ts +129 -2
- package/dist/core/usecases/mikoshi-push.js +316 -11
- package/dist/interfaces/cli/commands/config.js +1 -1
- package/dist/interfaces/cli/commands/mikoshi.js +150 -13
- package/dist/interfaces/mcp/index.js +109 -38
- package/dist/shared/config.d.ts +2 -2
- package/dist/shared/config.js +3 -3
- package/dist/shared/engram-composer.d.ts +1 -1
- package/dist/shared/engram-composer.js +9 -5
- package/package.json +1 -1
|
@@ -142,7 +142,7 @@ export function registerConfigCommand(program) {
|
|
|
142
142
|
// relic config distillation-batch-size [n]
|
|
143
143
|
config
|
|
144
144
|
.command("distillation-batch-size [n]")
|
|
145
|
-
.description("Get or set the number of archive entries to distill at once (default:
|
|
145
|
+
.description("Get or set the number of archive entries to distill at once (default: 100)")
|
|
146
146
|
.action(async (n) => {
|
|
147
147
|
await ensureInitialized();
|
|
148
148
|
const cfg = await loadConfig();
|
|
@@ -3,7 +3,7 @@ import { MikoshiApiClient } from "../../../adapters/mikoshi/client.js";
|
|
|
3
3
|
import { printBlank, printDetail, printErrorDetail, printErrorLine, printLine } from "../output.js";
|
|
4
4
|
import { startSpinner } from "../spinner.js";
|
|
5
5
|
import { MikoshiStatus, MikoshiStatusEngramNotFoundError, MikoshiStatusCloudNotFoundError, } from "../../../core/usecases/mikoshi-status.js";
|
|
6
|
-
import { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaConflictError, MikoshiPushPersonaHashError, } from "../../../core/usecases/mikoshi-push.js";
|
|
6
|
+
import { MikoshiPush, MikoshiPushEngramNotFoundError, MikoshiPushPersonaConflictError, MikoshiPushPersonaHashError, MikoshiPushAvatarInvalidMimeError, MikoshiPushAvatarHttpError, MikoshiPushAvatarPrivateHostError, MikoshiPushAvatarFetchError, MikoshiPushAvatarReadError, MikoshiPushAvatarTooLargeError, } from "../../../core/usecases/mikoshi-push.js";
|
|
7
7
|
import { MikoshiDownload, MikoshiDownloadAlreadyExistsError, MikoshiDownloadCloudNotFoundError, MikoshiDownloadPersonaMissingError, } from "../../../core/usecases/mikoshi-download.js";
|
|
8
8
|
import { MikoshiPull, MikoshiPullEngramNotFoundError, MikoshiPullCloudNotFoundError, MikoshiPullPersonaMissingError, } from "../../../core/usecases/mikoshi-pull.js";
|
|
9
9
|
import { MikoshiMemorySync, MikoshiMemorySyncEngramNotFoundError, MikoshiMemorySyncCloudNotFoundError, MikoshiMemorySyncDecryptError, } from "../../../core/usecases/mikoshi-memory-sync.js";
|
|
@@ -152,33 +152,105 @@ export function registerMikoshiCommand(program) {
|
|
|
152
152
|
const { result, apply } = await usecase.check(engramId);
|
|
153
153
|
checkSpinner.stop();
|
|
154
154
|
checkSpinner = undefined;
|
|
155
|
-
|
|
155
|
+
const needsAvatarUpload = result.avatar?.outcome === "upload_required";
|
|
156
|
+
if (!apply) {
|
|
157
|
+
// already_synced と avatar 変更なし
|
|
156
158
|
printLine(`✅ Persona already in sync (${result.engramName})`);
|
|
159
|
+
printAvatarSkipReason(result.avatar);
|
|
157
160
|
}
|
|
158
161
|
else {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
// Avatar URL drift の情報提示 (Phase 3):
|
|
163
|
+
// IDENTITY.md の diff が Avatar 行の値だけなら、ユーザーが
|
|
164
|
+
// 「自分は変更してないのに push_required と言われる」状態を
|
|
165
|
+
// 把握できるよう、確認プロンプトの前に理由を説明する。
|
|
166
|
+
if (result.avatarDrift) {
|
|
167
|
+
printErrorLine("⚠ IDENTITY.md differs only in the Avatar line.");
|
|
168
|
+
printErrorDetail(`local: ${result.avatarDrift.localValue}`);
|
|
169
|
+
printErrorDetail(`remote: ${result.avatarDrift.remoteValue}`);
|
|
170
|
+
printErrorLine(" This typically happens after 'relic mikoshi pull' rewrote the Avatar line to the R2 URL.");
|
|
171
|
+
printErrorLine(" Pushing will overwrite the remote IDENTITY.md with the local version.");
|
|
172
|
+
printBlank();
|
|
173
|
+
}
|
|
174
|
+
// Persona 確認 (already_synced のときは avatar-only なので persona 確認はスキップ)
|
|
175
|
+
if (result.outcome !== "already_synced" && !opts.yes) {
|
|
176
|
+
const prompt = result.avatarDrift
|
|
177
|
+
? `Push the Avatar URL change to Mikoshi for "${engramId}"? [y/N] `
|
|
178
|
+
: result.outcome === "create_required"
|
|
179
|
+
? `Engram "${engramId}" does not exist on Mikoshi. Create it? [y/N] `
|
|
180
|
+
: `Overwrite Mikoshi persona with the local Relic version for "${engramId}"? [y/N] `;
|
|
181
|
+
if (!(await confirm(prompt))) {
|
|
162
182
|
printLine("Skipped.");
|
|
163
183
|
return;
|
|
164
184
|
}
|
|
165
185
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
// Avatar 確認
|
|
187
|
+
if (needsAvatarUpload && !opts.yes) {
|
|
188
|
+
const av = result.avatar;
|
|
189
|
+
printLine(`Avatar to upload for "${engramId}":`);
|
|
190
|
+
if (av.source === "url" && av.sourceUrl) {
|
|
191
|
+
printDetail(`url: ${av.sourceUrl}`);
|
|
192
|
+
}
|
|
193
|
+
if (av.localPath)
|
|
194
|
+
printDetail(`path: ${av.localPath}`);
|
|
195
|
+
if (av.localMimeType)
|
|
196
|
+
printDetail(`mime: ${av.localMimeType}`);
|
|
197
|
+
if (av.localSize !== undefined)
|
|
198
|
+
printDetail(`size: ${formatBytes(av.localSize)}`);
|
|
199
|
+
if (av.localHash)
|
|
200
|
+
printDetail(`hash: ${av.localHash}`);
|
|
201
|
+
if (!(await confirm(`Upload this avatar? [y/N] `))) {
|
|
202
|
+
printLine("Skipped.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
170
205
|
}
|
|
171
|
-
|
|
172
|
-
|
|
206
|
+
const spinnerMessage = result.outcome === "already_synced"
|
|
207
|
+
? result.avatar?.source === "url"
|
|
208
|
+
? "Fetching avatar from URL and uploading to Mikoshi..."
|
|
209
|
+
: "Uploading avatar to Mikoshi..."
|
|
210
|
+
: result.avatar?.source === "url" && result.avatar?.outcome === "upload_required"
|
|
211
|
+
? "Pushing persona, then fetching avatar from URL..."
|
|
212
|
+
: "Pushing persona to Mikoshi...";
|
|
213
|
+
applySpinner = startSpinner(spinnerMessage);
|
|
214
|
+
const applied = await apply({
|
|
215
|
+
onAvatarProgress: (stage) => {
|
|
216
|
+
if (!applySpinner)
|
|
217
|
+
return;
|
|
218
|
+
if (stage === "fetching") {
|
|
219
|
+
applySpinner.update("Fetching avatar from URL...");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
applySpinner.update("Uploading avatar to Mikoshi...");
|
|
223
|
+
},
|
|
224
|
+
});
|
|
173
225
|
if (applied.action === "created") {
|
|
174
226
|
applySpinner.stop(`✅ Created "${result.engramName}" on Mikoshi.`);
|
|
175
227
|
applySpinner = undefined;
|
|
176
228
|
printLine(`Cloud ID: ${applied.cloudEngramId}`);
|
|
177
229
|
}
|
|
178
|
-
else {
|
|
230
|
+
else if (applied.action === "updated") {
|
|
179
231
|
applySpinner.stop(`✅ Persona updated for "${result.engramName}".`);
|
|
180
232
|
applySpinner = undefined;
|
|
181
|
-
|
|
233
|
+
if (applied.newPersonaHash)
|
|
234
|
+
printLine(`Hash: ${applied.newPersonaHash}`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// avatar_only
|
|
238
|
+
applySpinner.stop(`✅ Avatar updated for "${result.engramName}".`);
|
|
239
|
+
applySpinner = undefined;
|
|
240
|
+
}
|
|
241
|
+
// Avatar の結果を最後に報告
|
|
242
|
+
if (applied.avatarAction === "uploaded" && applied.newAvatarUrl) {
|
|
243
|
+
printLine(`Avatar: ${applied.newAvatarUrl}`);
|
|
244
|
+
}
|
|
245
|
+
else if (applied.avatarAction === "skipped" && applied.avatarSkipReason) {
|
|
246
|
+
printAvatarSkipReason({ outcome: "skip", skipReason: applied.avatarSkipReason });
|
|
247
|
+
}
|
|
248
|
+
else if (applied.avatarAction === "failed") {
|
|
249
|
+
const failureLabel = applied.avatarFailureStage === "fetch"
|
|
250
|
+
? "Avatar fetch failed"
|
|
251
|
+
: "Avatar upload failed";
|
|
252
|
+
printErrorLine(`⚠ ${failureLabel}: ${applied.avatarError?.message ?? "unknown error"}`);
|
|
253
|
+
printErrorLine(" The persona change was saved. Retry with 'relic mikoshi push' to upload the avatar.");
|
|
182
254
|
}
|
|
183
255
|
}
|
|
184
256
|
}
|
|
@@ -202,6 +274,43 @@ export function registerMikoshiCommand(program) {
|
|
|
202
274
|
printErrorLine("Re-run 'relic mikoshi status' to review the current state.");
|
|
203
275
|
process.exit(1);
|
|
204
276
|
}
|
|
277
|
+
if (err instanceof MikoshiPushAvatarInvalidMimeError) {
|
|
278
|
+
printError(`Error: Avatar format is not supported.`);
|
|
279
|
+
printErrorDetail(`Source: ${err.avatarPath}`);
|
|
280
|
+
printErrorDetail("Allowed: JPEG, PNG, WebP");
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
if (err instanceof MikoshiPushAvatarTooLargeError) {
|
|
284
|
+
printError(`Error: Avatar exceeds ${formatBytes(err.maxBytes)} (actual: ${formatBytes(err.actualBytes)}).`);
|
|
285
|
+
printErrorDetail(`Source: ${err.avatarPath}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
if (err instanceof MikoshiPushAvatarReadError) {
|
|
289
|
+
printError(`Error: Failed to read avatar file.`);
|
|
290
|
+
printErrorDetail(`Path: ${err.avatarPath}`);
|
|
291
|
+
if (err.cause instanceof Error) {
|
|
292
|
+
printErrorDetail(err.cause.message);
|
|
293
|
+
}
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
if (err instanceof MikoshiPushAvatarHttpError) {
|
|
297
|
+
printError("Error: Avatar URL must use HTTPS.");
|
|
298
|
+
printErrorDetail(`URL: ${err.avatarUrl}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
if (err instanceof MikoshiPushAvatarPrivateHostError) {
|
|
302
|
+
printError("Error: Avatar URL points to a private host.");
|
|
303
|
+
printErrorDetail(`URL: ${err.avatarUrl}`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
if (err instanceof MikoshiPushAvatarFetchError) {
|
|
307
|
+
printError("Error: Failed to fetch avatar from URL.");
|
|
308
|
+
printErrorDetail(`URL: ${err.avatarUrl}`);
|
|
309
|
+
if (err.cause instanceof Error) {
|
|
310
|
+
printErrorDetail(err.cause.message);
|
|
311
|
+
}
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
205
314
|
if (err instanceof MikoshiApiError) {
|
|
206
315
|
handleMikoshiApiError(err);
|
|
207
316
|
}
|
|
@@ -250,6 +359,9 @@ export function registerMikoshiCommand(program) {
|
|
|
250
359
|
const result = await download.execute(engramId);
|
|
251
360
|
applySpinner.stop(`✅ Pulled "${result.engramName}" from Mikoshi.`);
|
|
252
361
|
applySpinner = undefined;
|
|
362
|
+
if (result.rewrittenAvatarUrl) {
|
|
363
|
+
printDetail(`Avatar line rewritten to Mikoshi URL: ${result.rewrittenAvatarUrl}`);
|
|
364
|
+
}
|
|
253
365
|
if (opts.sync) {
|
|
254
366
|
const passphrase = await resolvePassphraseForSync();
|
|
255
367
|
const syncUsecase = new MikoshiMemorySync(repo, client);
|
|
@@ -278,6 +390,9 @@ export function registerMikoshiCommand(program) {
|
|
|
278
390
|
printDetail("SOUL.md — differs");
|
|
279
391
|
if (diff.identityDiffers)
|
|
280
392
|
printDetail("IDENTITY.md — differs");
|
|
393
|
+
if (diff.rewrittenAvatarUrl) {
|
|
394
|
+
printDetail(`Avatar line will be rewritten to Mikoshi URL: ${diff.rewrittenAvatarUrl}`);
|
|
395
|
+
}
|
|
281
396
|
printBlank();
|
|
282
397
|
// 確認プロンプト
|
|
283
398
|
if (!opts.yes) {
|
|
@@ -415,6 +530,21 @@ export function registerMikoshiCommand(program) {
|
|
|
415
530
|
}
|
|
416
531
|
});
|
|
417
532
|
}
|
|
533
|
+
function printAvatarSkipReason(avatar) {
|
|
534
|
+
if (!avatar || avatar.outcome !== "skip" || !avatar.skipReason)
|
|
535
|
+
return;
|
|
536
|
+
switch (avatar.skipReason) {
|
|
537
|
+
case "remote_avatar_url_matches_identity":
|
|
538
|
+
printLine("Avatar upload skipped: remote avatar URL already matches the local Avatar field.");
|
|
539
|
+
return;
|
|
540
|
+
case "source_url_unchanged":
|
|
541
|
+
printLine("Avatar upload skipped: avatar source URL is unchanged from the last successful push.");
|
|
542
|
+
return;
|
|
543
|
+
case "local_file_hash_unchanged":
|
|
544
|
+
printLine("Avatar upload skipped: local avatar file hash matches the last successful push.");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
418
548
|
// ---------------------------------------------------------------------------
|
|
419
549
|
// Shared error handler
|
|
420
550
|
// ---------------------------------------------------------------------------
|
|
@@ -457,6 +587,13 @@ function statusLabel(status) {
|
|
|
457
587
|
default: return status;
|
|
458
588
|
}
|
|
459
589
|
}
|
|
590
|
+
function formatBytes(bytes) {
|
|
591
|
+
if (bytes < 1024)
|
|
592
|
+
return `${bytes} B`;
|
|
593
|
+
if (bytes < 1024 * 1024)
|
|
594
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
595
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
596
|
+
}
|
|
460
597
|
function confirm(prompt) {
|
|
461
598
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
462
599
|
return new Promise((resolve) => {
|
|
@@ -3,8 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { ArchiveSearch, ArchiveSearchEngramNotFoundError, } from "../../core/usecases/archive-search.js";
|
|
6
|
-
import { ArchivePending, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
|
|
7
|
-
import { ArchiveCursorUpdate, } from "../../core/usecases/archive-cursor-update.js";
|
|
6
|
+
import { ArchivePending, ArchiveCursorCorruptedError, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
|
|
7
|
+
import { ArchiveCursorUpdate, ArchiveCursorAdvanceOverflowError, } from "../../core/usecases/archive-cursor-update.js";
|
|
8
8
|
import { MemoryWrite, MemoryWriteEngramNotFoundError, } from "../../core/usecases/memory-write.js";
|
|
9
9
|
import { CreateEngram, EngramAlreadyExistsError, InvalidEngramIdError, } from "../../core/usecases/create-engram.js";
|
|
10
10
|
import { DEFAULT_SOUL, DEFAULT_IDENTITY } from "../../shared/templates.js";
|
|
@@ -13,6 +13,63 @@ import { join } from "node:path";
|
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { readFile, writeFile } from "node:fs/promises";
|
|
15
15
|
import { resolveDistillationBatchSize, resolveEngramsPath, } from "../../shared/config.js";
|
|
16
|
+
async function appendLongTermAndUserProfile(engramsPath, id, long_term, user_profile) {
|
|
17
|
+
let longTermWritten = false;
|
|
18
|
+
if (long_term) {
|
|
19
|
+
const memoryMdPath = join(engramsPath, id, "MEMORY.md");
|
|
20
|
+
if (existsSync(memoryMdPath)) {
|
|
21
|
+
const existing = await readFile(memoryMdPath, "utf-8");
|
|
22
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
23
|
+
await writeFile(memoryMdPath, existing + separator + long_term + "\n", "utf-8");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
await writeFile(memoryMdPath, long_term + "\n", "utf-8");
|
|
27
|
+
}
|
|
28
|
+
longTermWritten = true;
|
|
29
|
+
}
|
|
30
|
+
let userProfileWritten = false;
|
|
31
|
+
if (user_profile) {
|
|
32
|
+
const userMdPath = join(engramsPath, id, "USER.md");
|
|
33
|
+
await writeFile(userMdPath, user_profile + "\n", "utf-8");
|
|
34
|
+
userProfileWritten = true;
|
|
35
|
+
}
|
|
36
|
+
return { longTermWritten, userProfileWritten };
|
|
37
|
+
}
|
|
38
|
+
function assertDateCoverage(writes, expectedDates, skippedDates) {
|
|
39
|
+
const writeDates = writes.map((write) => write.date);
|
|
40
|
+
const duplicateWriteDates = findDuplicates(writeDates);
|
|
41
|
+
if (duplicateWriteDates.length > 0) {
|
|
42
|
+
throw new Error(`Duplicate write dates are not allowed: ${duplicateWriteDates.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
const duplicateSkippedDates = findDuplicates(skippedDates);
|
|
45
|
+
if (duplicateSkippedDates.length > 0) {
|
|
46
|
+
throw new Error(`Duplicate skipped_dates are not allowed: ${duplicateSkippedDates.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
const overlappedDates = writeDates.filter((date) => skippedDates.includes(date));
|
|
49
|
+
if (overlappedDates.length > 0) {
|
|
50
|
+
throw new Error(`Dates cannot appear in both writes and skipped_dates: ${[...new Set(overlappedDates)].join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
const expectedSet = new Set(expectedDates);
|
|
53
|
+
const coveredDates = [...writeDates, ...skippedDates];
|
|
54
|
+
const unexpectedDates = coveredDates.filter((date) => !expectedSet.has(date));
|
|
55
|
+
if (unexpectedDates.length > 0) {
|
|
56
|
+
throw new Error(`Unexpected dates: ${[...new Set(unexpectedDates)].join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
const coveredSet = new Set(coveredDates);
|
|
59
|
+
const missingDates = expectedDates.filter((date) => !coveredSet.has(date));
|
|
60
|
+
if (missingDates.length > 0) {
|
|
61
|
+
throw new Error(`Missing dates: ${missingDates.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function findDuplicates(values) {
|
|
65
|
+
const counts = new Map();
|
|
66
|
+
for (const value of values) {
|
|
67
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
return [...counts.entries()]
|
|
70
|
+
.filter(([, count]) => count > 1)
|
|
71
|
+
.map(([value]) => value);
|
|
72
|
+
}
|
|
16
73
|
const server = new McpServer({
|
|
17
74
|
name: "relic",
|
|
18
75
|
version: "0.1.0",
|
|
@@ -153,16 +210,34 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
|
|
|
153
210
|
],
|
|
154
211
|
};
|
|
155
212
|
}
|
|
156
|
-
const
|
|
213
|
+
const dates = [...new Set(result.entries.map((entry) => entry.date).filter(Boolean))];
|
|
214
|
+
const header = [
|
|
215
|
+
`cursor: ${result.cursor}`,
|
|
216
|
+
`total: ${result.total}`,
|
|
217
|
+
`returned: ${result.entries.length}`,
|
|
218
|
+
`remaining: ${result.remaining}`,
|
|
219
|
+
`dates: ${dates.length > 0 ? dates.join(", ") : "unknown"}`,
|
|
220
|
+
].join(" | ");
|
|
157
221
|
const body = result.entries
|
|
158
|
-
.map((
|
|
222
|
+
.map((entry, i) => {
|
|
223
|
+
const lines = [`[entry ${result.cursor + i + 1}]`];
|
|
224
|
+
if (entry.date) {
|
|
225
|
+
lines.push(`date: ${entry.date}`);
|
|
226
|
+
}
|
|
227
|
+
if (entry.summary) {
|
|
228
|
+
lines.push(`summary: ${entry.summary}`);
|
|
229
|
+
}
|
|
230
|
+
lines.push(entry.raw);
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
})
|
|
159
233
|
.join("\n\n---\n\n");
|
|
160
234
|
return {
|
|
161
235
|
content: [{ type: "text", text: `${header}\n\n${body}` }],
|
|
162
236
|
};
|
|
163
237
|
}
|
|
164
238
|
catch (err) {
|
|
165
|
-
if (err instanceof ArchivePendingEngramNotFoundError
|
|
239
|
+
if (err instanceof ArchivePendingEngramNotFoundError ||
|
|
240
|
+
err instanceof ArchiveCursorCorruptedError) {
|
|
166
241
|
return {
|
|
167
242
|
content: [{ type: "text", text: err.message }],
|
|
168
243
|
isError: true,
|
|
@@ -174,7 +249,19 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
|
|
|
174
249
|
// --- relic_memory_write ---
|
|
175
250
|
server.tool("relic_memory_write", "Write distilled memory to an Engram's memory file and advance the archive cursor. Call this after reviewing pending archive entries and distilling them into key insights.", {
|
|
176
251
|
id: z.string().describe("Engram ID"),
|
|
177
|
-
|
|
252
|
+
writes: z
|
|
253
|
+
.array(z.object({
|
|
254
|
+
date: z.string().describe("Archive entry date for this memory file (YYYY-MM-DD)"),
|
|
255
|
+
content: z.string().describe("Distilled memory content for memory/YYYY-MM-DD.md"),
|
|
256
|
+
}))
|
|
257
|
+
.min(0)
|
|
258
|
+
.describe("Distilled memory writes grouped by actual archive dates"),
|
|
259
|
+
expected_dates: z
|
|
260
|
+
.array(z.string())
|
|
261
|
+
.describe("All archive dates that must be explicitly covered by writes or skipped_dates"),
|
|
262
|
+
skipped_dates: z
|
|
263
|
+
.array(z.string())
|
|
264
|
+
.describe("Archive dates intentionally skipped because they produced no durable memory"),
|
|
178
265
|
count: z
|
|
179
266
|
.number()
|
|
180
267
|
.describe("Number of archive entries distilled (from relic_archive_pending returned count)"),
|
|
@@ -186,10 +273,6 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
|
|
|
186
273
|
.string()
|
|
187
274
|
.optional()
|
|
188
275
|
.describe("User profile updates to write to USER.md (preferences, tendencies, work style — about the human, not the project)"),
|
|
189
|
-
date: z
|
|
190
|
-
.string()
|
|
191
|
-
.optional()
|
|
192
|
-
.describe("Date for memory file (YYYY-MM-DD, default: today)"),
|
|
193
276
|
path: z
|
|
194
277
|
.string()
|
|
195
278
|
.optional()
|
|
@@ -197,36 +280,21 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
|
|
|
197
280
|
}, async (args) => {
|
|
198
281
|
const engramsPath = await resolveEngramsPath(args.path);
|
|
199
282
|
try {
|
|
200
|
-
// Step 1: Write daily memory
|
|
201
283
|
const memoryWrite = new MemoryWrite(engramsPath);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await writeFile(memoryMdPath, existing + separator + args.long_term + "\n", "utf-8");
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
await writeFile(memoryMdPath, args.long_term + "\n", "utf-8");
|
|
214
|
-
}
|
|
215
|
-
longTermWritten = true;
|
|
216
|
-
}
|
|
217
|
-
// Step 2.5: Write USER.md if user_profile is provided
|
|
218
|
-
let userProfileWritten = false;
|
|
219
|
-
if (args.user_profile) {
|
|
220
|
-
const userMdPath = join(engramsPath, args.id, "USER.md");
|
|
221
|
-
await writeFile(userMdPath, args.user_profile + "\n", "utf-8");
|
|
222
|
-
userProfileWritten = true;
|
|
223
|
-
}
|
|
224
|
-
// Step 3: Advance cursor by the number of distilled entries
|
|
284
|
+
assertDateCoverage(args.writes, args.expected_dates, args.skipped_dates);
|
|
285
|
+
const writeResults = await memoryWrite.executeBatch(args.id, args.writes);
|
|
286
|
+
const writeSummary = writeResults.length > 0
|
|
287
|
+
? writeResults
|
|
288
|
+
.map((result) => `${result.date} (${result.appended ? "appended" : "created"})`)
|
|
289
|
+
.join(", ")
|
|
290
|
+
: "no files";
|
|
291
|
+
const { longTermWritten, userProfileWritten } = await appendLongTermAndUserProfile(engramsPath, args.id, args.long_term, args.user_profile);
|
|
225
292
|
const cursorUpdate = new ArchiveCursorUpdate(engramsPath);
|
|
226
293
|
const cursorResult = await cursorUpdate.execute(args.id, args.count);
|
|
227
|
-
const parts = [
|
|
228
|
-
|
|
229
|
-
|
|
294
|
+
const parts = [`Memory written to ${writeSummary}.`];
|
|
295
|
+
if (args.skipped_dates.length > 0) {
|
|
296
|
+
parts.push(`Skipped dates: ${args.skipped_dates.join(", ")}.`);
|
|
297
|
+
}
|
|
230
298
|
if (longTermWritten) {
|
|
231
299
|
parts.push("Long-term memory (MEMORY.md) updated.");
|
|
232
300
|
}
|
|
@@ -244,7 +312,10 @@ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory
|
|
|
244
312
|
};
|
|
245
313
|
}
|
|
246
314
|
catch (err) {
|
|
247
|
-
if (err instanceof MemoryWriteEngramNotFoundError
|
|
315
|
+
if (err instanceof MemoryWriteEngramNotFoundError ||
|
|
316
|
+
err instanceof ArchiveCursorAdvanceOverflowError ||
|
|
317
|
+
err instanceof ArchiveCursorCorruptedError ||
|
|
318
|
+
err instanceof Error) {
|
|
248
319
|
return {
|
|
249
320
|
content: [{ type: "text", text: err.message }],
|
|
250
321
|
isError: true,
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const RelicConfigSchema: z.ZodObject<{
|
|
|
17
17
|
clawPath: z.ZodOptional<z.ZodString>;
|
|
18
18
|
/** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
|
|
19
19
|
memoryWindowSize: z.ZodDefault<z.ZodNumber>;
|
|
20
|
-
/** 一度の記憶蒸留で扱う archive エントリ数 (default:
|
|
20
|
+
/** 一度の記憶蒸留で扱う archive エントリ数 (default: 100) */
|
|
21
21
|
distillationBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
22
22
|
}, "strip", z.ZodTypeAny, {
|
|
23
23
|
memoryWindowSize: number;
|
|
@@ -80,7 +80,7 @@ export declare function resolveClawPath(cliOverride?: string): Promise<string |
|
|
|
80
80
|
export declare function resolveMemoryWindowSize(): Promise<number>;
|
|
81
81
|
/**
|
|
82
82
|
* 記憶蒸留のバッチ件数を解決する。
|
|
83
|
-
* 優先順位: config.distillationBatchSize >
|
|
83
|
+
* 優先順位: config.distillationBatchSize > 100 (デフォルト)
|
|
84
84
|
*/
|
|
85
85
|
export declare function resolveDistillationBatchSize(): Promise<number>;
|
|
86
86
|
/**
|
package/dist/shared/config.js
CHANGED
|
@@ -24,8 +24,8 @@ export const RelicConfigSchema = z.object({
|
|
|
24
24
|
clawPath: z.string().optional(),
|
|
25
25
|
/** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
|
|
26
26
|
memoryWindowSize: z.number().int().min(1).default(2),
|
|
27
|
-
/** 一度の記憶蒸留で扱う archive エントリ数 (default:
|
|
28
|
-
distillationBatchSize: z.number().int().min(1).default(
|
|
27
|
+
/** 一度の記憶蒸留で扱う archive エントリ数 (default: 100) */
|
|
28
|
+
distillationBatchSize: z.number().int().min(1).default(100),
|
|
29
29
|
});
|
|
30
30
|
/**
|
|
31
31
|
* 設定ファイルを読み込む。存在しなければデフォルト値を返す。
|
|
@@ -163,7 +163,7 @@ export async function resolveMemoryWindowSize() {
|
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
165
|
* 記憶蒸留のバッチ件数を解決する。
|
|
166
|
-
* 優先順位: config.distillationBatchSize >
|
|
166
|
+
* 優先順位: config.distillationBatchSize > 100 (デフォルト)
|
|
167
167
|
*/
|
|
168
168
|
export async function resolveDistillationBatchSize() {
|
|
169
169
|
await ensureInitialized();
|
|
@@ -9,7 +9,7 @@ export interface ComposeOptions {
|
|
|
9
9
|
currentDate?: string;
|
|
10
10
|
/** システムプロンプトに含める直近メモリエントリ数(デフォルト: 2) */
|
|
11
11
|
memoryWindowSize?: number;
|
|
12
|
-
/** 一度の記憶蒸留で扱う archive エントリ数(デフォルト:
|
|
12
|
+
/** 一度の記憶蒸留で扱う archive エントリ数(デフォルト: 100) */
|
|
13
13
|
distillationBatchSize?: number;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -46,7 +46,7 @@ export function composeEngram(files, options) {
|
|
|
46
46
|
*/
|
|
47
47
|
function composeRelicSection(meta, distillationBatchSize, currentDate) {
|
|
48
48
|
const today = currentDate ?? new Date().toISOString().split("T")[0];
|
|
49
|
-
const batchSize = distillationBatchSize ??
|
|
49
|
+
const batchSize = distillationBatchSize ?? 100;
|
|
50
50
|
return `# Relic System
|
|
51
51
|
|
|
52
52
|
- engramId: ${meta.id}
|
|
@@ -68,16 +68,20 @@ To recall past context, use \`relic_archive_search\` to search your archive by k
|
|
|
68
68
|
|
|
69
69
|
When the user asks you to organize or distill memories:
|
|
70
70
|
1. Call \`relic_archive_pending\` **once** to get un-distilled session entries (up to ${batchSize})
|
|
71
|
-
2. Review and
|
|
72
|
-
|
|
71
|
+
2. Review the archive entry dates and group the distilled memory by the **actual archive entry date**, not the distillation date
|
|
72
|
+
3. Distill them into:
|
|
73
|
+
- **writes**: one item per date, each with \`date\` and \`content\`, for \`memory/YYYY-MM-DD.md\`
|
|
74
|
+
- **expected_dates**: every archive date present in the pending batch
|
|
75
|
+
- **skipped_dates**: dates that had pending entries but produced no durable memory worth saving
|
|
73
76
|
- **long_term** (optional): only the most important, enduring facts for \`MEMORY.md\` (e.g. project architecture decisions, key constraints, hard rules)
|
|
74
77
|
- **user_profile** (optional): full updated user profile for \`USER.md\` (preferences, tendencies, work style — about the human, not the project)
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
4. Call \`relic_memory_write\` with \`writes\`, \`expected_dates\`, \`skipped_dates\`, \`count\`, and optionally \`long_term\` and/or \`user_profile\`
|
|
79
|
+
5. If \`remaining > 0\`, inform the user how many entries are still pending — do NOT fetch more automatically
|
|
77
80
|
|
|
78
81
|
**Important:**
|
|
79
82
|
- Write distilled memories in the **same language the user is using** in the current conversation
|
|
80
83
|
- Do NOT loop or repeat the distillation process — one round per user request
|
|
84
|
+
- Every archive date in the pending batch must appear in exactly one of \`writes.date\` or \`skipped_dates\`
|
|
81
85
|
- \`long_term\` should be highly selective — only facts that matter across all future sessions (objective facts and rules)
|
|
82
86
|
- \`user_profile\` is a complete replacement, not an append — write the full profile each time you update it (user tendencies, preferences, work style)
|
|
83
87
|
- Distilled memories are loaded into your prompt on future sessions`;
|