@ectplsm/relic 0.1.1 → 0.1.3
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 +172 -49
- package/dist/adapters/shells/claude-hook.d.ts +12 -0
- package/dist/adapters/shells/claude-hook.js +160 -0
- package/dist/adapters/shells/claude-shell.d.ts +5 -2
- package/dist/adapters/shells/claude-shell.js +17 -3
- package/dist/adapters/shells/codex-hook.d.ts +12 -0
- package/dist/adapters/shells/codex-hook.js +141 -0
- package/dist/adapters/shells/codex-shell.d.ts +7 -4
- package/dist/adapters/shells/codex-shell.js +22 -7
- package/dist/adapters/shells/copilot-shell.d.ts +2 -2
- package/dist/adapters/shells/copilot-shell.js +3 -3
- package/dist/adapters/shells/gemini-hook.d.ts +12 -0
- package/dist/adapters/shells/gemini-hook.js +108 -0
- package/dist/adapters/shells/gemini-shell.d.ts +6 -4
- package/dist/adapters/shells/gemini-shell.js +103 -14
- package/dist/adapters/shells/index.d.ts +0 -1
- package/dist/adapters/shells/index.js +0 -1
- package/dist/adapters/shells/spawn-shell.d.ts +1 -1
- package/dist/adapters/shells/spawn-shell.js +10 -3
- package/dist/adapters/shells/trust-registrar.d.ts +19 -0
- package/dist/adapters/shells/trust-registrar.js +141 -0
- package/dist/core/ports/shell-launcher.d.ts +17 -2
- package/dist/core/usecases/archive-cursor-update.d.ts +21 -0
- package/dist/core/usecases/archive-cursor-update.js +44 -0
- package/dist/core/usecases/archive-pending.d.ts +25 -0
- package/dist/core/usecases/archive-pending.js +61 -0
- package/dist/core/usecases/archive-search.d.ts +20 -0
- package/dist/core/usecases/archive-search.js +46 -0
- package/dist/core/usecases/inbox-search.d.ts +20 -0
- package/dist/core/usecases/inbox-search.js +46 -0
- package/dist/core/usecases/inbox-write.d.ts +27 -0
- package/dist/core/usecases/inbox-write.js +72 -0
- package/dist/core/usecases/index.d.ts +2 -1
- package/dist/core/usecases/index.js +2 -1
- package/dist/core/usecases/summon.d.ts +6 -1
- package/dist/core/usecases/summon.js +8 -2
- package/dist/interfaces/cli/commands/config.d.ts +2 -0
- package/dist/interfaces/cli/commands/config.js +83 -0
- package/dist/interfaces/cli/commands/extract.js +3 -2
- package/dist/interfaces/cli/commands/init.js +47 -0
- package/dist/interfaces/cli/commands/inject.js +3 -2
- package/dist/interfaces/cli/commands/shell.js +13 -11
- package/dist/interfaces/cli/index.js +8 -1
- package/dist/interfaces/mcp/index.js +68 -305
- package/dist/shared/config.d.ts +31 -0
- package/dist/shared/config.js +86 -16
- package/dist/shared/engram-composer.d.ts +16 -3
- package/dist/shared/engram-composer.js +50 -5
- package/dist/shared/memory-inbox.d.ts +78 -0
- package/dist/shared/memory-inbox.js +168 -0
- package/dist/shared/session-recorder.d.ts +47 -0
- package/dist/shared/session-recorder.js +112 -0
- package/package.json +5 -5
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { EngramFiles } from "../core/entities/engram.js";
|
|
1
|
+
import type { EngramFiles, EngramMeta } from "../core/entities/engram.js";
|
|
2
|
+
/**
|
|
3
|
+
* Engram結合オプション
|
|
4
|
+
*/
|
|
5
|
+
export interface ComposeOptions {
|
|
6
|
+
/** Engramメタデータ(RELIC systemセクション生成用) */
|
|
7
|
+
meta?: EngramMeta;
|
|
8
|
+
/** 現在日付の上書き(テスト用、デフォルト: today) */
|
|
9
|
+
currentDate?: string;
|
|
10
|
+
/** システムプロンプトに含める直近メモリエントリ数(デフォルト: 2) */
|
|
11
|
+
memoryWindowSize?: number;
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* EngramFilesの各Markdownを、Shell注入用の単一テキストに結合する。
|
|
4
15
|
*
|
|
@@ -9,9 +20,11 @@ import type { EngramFiles } from "../core/entities/engram.js";
|
|
|
9
20
|
* 4. AGENTS.md — エージェント設定
|
|
10
21
|
* 5. MEMORY.md — 記憶インデックス(常にロード)
|
|
11
22
|
* 6. memory/*.md — 直近2日分のみロード(OpenClaw互換スライディングウィンドウ)
|
|
12
|
-
* 7.
|
|
23
|
+
* 7. RELIC — システム情報(Engram ID、日付、Archive Protocol)
|
|
24
|
+
*
|
|
25
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
13
26
|
*/
|
|
14
|
-
export declare function composeEngram(files: EngramFiles): string;
|
|
27
|
+
export declare function composeEngram(files: EngramFiles, options?: ComposeOptions): string;
|
|
15
28
|
/**
|
|
16
29
|
* 直近N日分のメモリエントリを返す(日付降順でソートし、最新N件を日付昇順で返す)
|
|
17
30
|
*/
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* 4. AGENTS.md — エージェント設定
|
|
9
9
|
* 5. MEMORY.md — 記憶インデックス(常にロード)
|
|
10
10
|
* 6. memory/*.md — 直近2日分のみロード(OpenClaw互換スライディングウィンドウ)
|
|
11
|
-
* 7.
|
|
11
|
+
* 7. RELIC — システム情報(Engram ID、日付、Archive Protocol)
|
|
12
|
+
*
|
|
13
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
12
14
|
*/
|
|
13
|
-
export function composeEngram(files) {
|
|
15
|
+
export function composeEngram(files, options) {
|
|
14
16
|
const sections = [];
|
|
15
17
|
sections.push(wrapSection("SOUL", files.soul));
|
|
16
18
|
sections.push(wrapSection("IDENTITY", files.identity));
|
|
@@ -24,16 +26,59 @@ export function composeEngram(files) {
|
|
|
24
26
|
sections.push(wrapSection("MEMORY", files.memory));
|
|
25
27
|
}
|
|
26
28
|
if (files.memoryEntries) {
|
|
27
|
-
const
|
|
29
|
+
const windowSize = options?.memoryWindowSize ?? 2;
|
|
30
|
+
const recentEntries = getRecentMemoryEntries(files.memoryEntries, windowSize);
|
|
28
31
|
for (const [date, content] of recentEntries) {
|
|
29
32
|
sections.push(wrapSection(`MEMORY: ${date}`, content));
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
// RELIC system section
|
|
36
|
+
if (options?.meta) {
|
|
37
|
+
sections.push(wrapSection("RELIC", composeRelicSection(options.meta, options.currentDate)));
|
|
34
38
|
}
|
|
35
39
|
return sections.join("\n\n");
|
|
36
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* RELICシステムセクションを生成する。
|
|
43
|
+
*
|
|
44
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
45
|
+
* [memory] タグ付きエントリだけが memory/*.md に永続化される。
|
|
46
|
+
*/
|
|
47
|
+
function composeRelicSection(meta, currentDate) {
|
|
48
|
+
const today = currentDate ?? new Date().toISOString().split("T")[0];
|
|
49
|
+
return `# Relic System
|
|
50
|
+
|
|
51
|
+
- engramId: ${meta.id}
|
|
52
|
+
- engramName: ${meta.name}
|
|
53
|
+
- currentDate: ${today}
|
|
54
|
+
|
|
55
|
+
# Archive Protocol (MCP)
|
|
56
|
+
|
|
57
|
+
You have an archive via the Relic MCP tools for persistent memory.
|
|
58
|
+
Your memories persist across ALL sessions and ALL LLM shells (Claude, Gemini, GPT, etc.).
|
|
59
|
+
|
|
60
|
+
Session logs are written automatically by a background hook — do NOT write them yourself.
|
|
61
|
+
|
|
62
|
+
## Recall
|
|
63
|
+
|
|
64
|
+
To recall past context, use \`relic_archive_search\` to search your archive by keyword.
|
|
65
|
+
|
|
66
|
+
## Distillation
|
|
67
|
+
|
|
68
|
+
When the user asks you to organize or distill memories:
|
|
69
|
+
1. Call \`relic_archive_pending\` **once** to get un-distilled session entries (up to 30)
|
|
70
|
+
2. Review and distill them into:
|
|
71
|
+
- **content**: key facts, decisions, and insights for \`memory/YYYY-MM-DD.md\`
|
|
72
|
+
- **long_term** (optional): only the most important, enduring facts for \`MEMORY.md\` (e.g. user preferences, project architecture decisions, key constraints)
|
|
73
|
+
3. Call \`relic_memory_write\` with \`content\`, \`count\`, and optionally \`long_term\`
|
|
74
|
+
4. If \`remaining > 0\`, inform the user how many entries are still pending — do NOT fetch more automatically
|
|
75
|
+
|
|
76
|
+
**Important:**
|
|
77
|
+
- Write distilled memories in the **same language the user is using** in the current conversation
|
|
78
|
+
- Do NOT loop or repeat the distillation process — one round per user request
|
|
79
|
+
- \`long_term\` should be highly selective — only facts that matter across all future sessions
|
|
80
|
+
- Distilled memories are loaded into your prompt on future sessions`;
|
|
81
|
+
}
|
|
37
82
|
/**
|
|
38
83
|
* 直近N日分のメモリエントリを返す(日付降順でソートし、最新N件を日付昇順で返す)
|
|
39
84
|
*/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryInbox — ファイルベースのメモリ受付口 + セッションログ
|
|
3
|
+
*
|
|
4
|
+
* LLMが inbox.md に追記すると、fs.watchで検知して処理する。
|
|
5
|
+
* inbox.md は書き溜め式で、セッションを跨いで追記され続ける。
|
|
6
|
+
* 処理済みエントリ数は inbox.cursor に永続化され、
|
|
7
|
+
* クラッシュ後の再起動でも正確に未処理分だけを回収できる。
|
|
8
|
+
*
|
|
9
|
+
* エントリの種別:
|
|
10
|
+
* - `[memory] ...` → memory/*.md に永続化される記憶
|
|
11
|
+
* - タグ無し → inbox.md にログとして残るだけ(会話サマリー等)
|
|
12
|
+
*
|
|
13
|
+
* フォーマット:
|
|
14
|
+
* 各エントリは `---` (前後に改行) で区切る。
|
|
15
|
+
*
|
|
16
|
+
* 例:
|
|
17
|
+
* Discussed improving RELIC's memory system.
|
|
18
|
+
* ---
|
|
19
|
+
* Implemented inbox-based approach for cross-LLM persistence.
|
|
20
|
+
* ---
|
|
21
|
+
* [memory] User prefers Bun over Node.js for all TypeScript projects.
|
|
22
|
+
* ---
|
|
23
|
+
* [memory] Project RELIC uses clean architecture with Zod for validation.
|
|
24
|
+
*/
|
|
25
|
+
export declare class MemoryInbox {
|
|
26
|
+
private readonly engramId;
|
|
27
|
+
private readonly engramsPath;
|
|
28
|
+
private watcher;
|
|
29
|
+
private processedCount;
|
|
30
|
+
private processing;
|
|
31
|
+
private memorySaved;
|
|
32
|
+
private logCount;
|
|
33
|
+
private savedEntries;
|
|
34
|
+
readonly inboxPath: string;
|
|
35
|
+
private readonly cursorPath;
|
|
36
|
+
constructor(engramId: string, engramsPath: string);
|
|
37
|
+
/**
|
|
38
|
+
* Inboxを起動する。
|
|
39
|
+
*
|
|
40
|
+
* 1. cursorファイルから前回の処理済み位置を復元
|
|
41
|
+
* 2. inbox.md の未処理エントリを回収(クラッシュ回復)
|
|
42
|
+
* 3. fs.watch で監視開始
|
|
43
|
+
*/
|
|
44
|
+
start(): Promise<MemoryInboxRecoveryResult>;
|
|
45
|
+
/**
|
|
46
|
+
* 監視を停止し、最終掃引を行う。
|
|
47
|
+
* inbox.md はそのまま残す(書き溜め式)。
|
|
48
|
+
*/
|
|
49
|
+
stop(): Promise<MemoryInboxResult>;
|
|
50
|
+
/**
|
|
51
|
+
* ファイル全体をパースし、未処理のエントリだけを処理する。
|
|
52
|
+
*
|
|
53
|
+
* - `[memory]` タグ付き → MemoryWrite で永続化
|
|
54
|
+
* - タグ無し → カウントのみ(inbox.md にログとして残る)
|
|
55
|
+
*
|
|
56
|
+
* 処理成功ごとにカーソルを永続化するため、
|
|
57
|
+
* 途中でクラッシュしても処理済み分は再処理されない。
|
|
58
|
+
*/
|
|
59
|
+
private processNewEntries;
|
|
60
|
+
private loadCursor;
|
|
61
|
+
private saveCursor;
|
|
62
|
+
}
|
|
63
|
+
export interface MemoryInboxResult {
|
|
64
|
+
/** memory/*.md に永続化されたエントリ数 */
|
|
65
|
+
memories: number;
|
|
66
|
+
/** ログとして記録されたエントリ数 */
|
|
67
|
+
logs: number;
|
|
68
|
+
/** 永続化されたメモリの内容 */
|
|
69
|
+
entries: string[];
|
|
70
|
+
}
|
|
71
|
+
export interface MemoryInboxRecoveryResult {
|
|
72
|
+
/** 回収されたメモリ数 */
|
|
73
|
+
recoveredMemories: number;
|
|
74
|
+
/** 回収されたログ数 */
|
|
75
|
+
recoveredLogs: number;
|
|
76
|
+
/** 回収されたメモリの内容 */
|
|
77
|
+
entries: string[];
|
|
78
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { watch, existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MemoryWrite } from "../core/usecases/memory-write.js";
|
|
5
|
+
const INBOX_FILE = "inbox.md";
|
|
6
|
+
const CURSOR_FILE = "inbox.cursor";
|
|
7
|
+
const ENTRY_SEPARATOR = /\n---\n/;
|
|
8
|
+
const MEMORY_TAG = /^\[memory\]\s*/i;
|
|
9
|
+
/**
|
|
10
|
+
* MemoryInbox — ファイルベースのメモリ受付口 + セッションログ
|
|
11
|
+
*
|
|
12
|
+
* LLMが inbox.md に追記すると、fs.watchで検知して処理する。
|
|
13
|
+
* inbox.md は書き溜め式で、セッションを跨いで追記され続ける。
|
|
14
|
+
* 処理済みエントリ数は inbox.cursor に永続化され、
|
|
15
|
+
* クラッシュ後の再起動でも正確に未処理分だけを回収できる。
|
|
16
|
+
*
|
|
17
|
+
* エントリの種別:
|
|
18
|
+
* - `[memory] ...` → memory/*.md に永続化される記憶
|
|
19
|
+
* - タグ無し → inbox.md にログとして残るだけ(会話サマリー等)
|
|
20
|
+
*
|
|
21
|
+
* フォーマット:
|
|
22
|
+
* 各エントリは `---` (前後に改行) で区切る。
|
|
23
|
+
*
|
|
24
|
+
* 例:
|
|
25
|
+
* Discussed improving RELIC's memory system.
|
|
26
|
+
* ---
|
|
27
|
+
* Implemented inbox-based approach for cross-LLM persistence.
|
|
28
|
+
* ---
|
|
29
|
+
* [memory] User prefers Bun over Node.js for all TypeScript projects.
|
|
30
|
+
* ---
|
|
31
|
+
* [memory] Project RELIC uses clean architecture with Zod for validation.
|
|
32
|
+
*/
|
|
33
|
+
export class MemoryInbox {
|
|
34
|
+
engramId;
|
|
35
|
+
engramsPath;
|
|
36
|
+
watcher = null;
|
|
37
|
+
processedCount = 0;
|
|
38
|
+
processing = false;
|
|
39
|
+
memorySaved = 0;
|
|
40
|
+
logCount = 0;
|
|
41
|
+
savedEntries = [];
|
|
42
|
+
inboxPath;
|
|
43
|
+
cursorPath;
|
|
44
|
+
constructor(engramId, engramsPath) {
|
|
45
|
+
this.engramId = engramId;
|
|
46
|
+
this.engramsPath = engramsPath;
|
|
47
|
+
const engramDir = join(engramsPath, engramId);
|
|
48
|
+
this.inboxPath = join(engramDir, INBOX_FILE);
|
|
49
|
+
this.cursorPath = join(engramDir, CURSOR_FILE);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Inboxを起動する。
|
|
53
|
+
*
|
|
54
|
+
* 1. cursorファイルから前回の処理済み位置を復元
|
|
55
|
+
* 2. inbox.md の未処理エントリを回収(クラッシュ回復)
|
|
56
|
+
* 3. fs.watch で監視開始
|
|
57
|
+
*/
|
|
58
|
+
async start() {
|
|
59
|
+
if (!existsSync(this.inboxPath)) {
|
|
60
|
+
writeFileSync(this.inboxPath, "", "utf-8");
|
|
61
|
+
}
|
|
62
|
+
this.processedCount = this.loadCursor();
|
|
63
|
+
const recovered = await this.processNewEntries();
|
|
64
|
+
this.watcher = watch(this.inboxPath, async (eventType) => {
|
|
65
|
+
if (eventType === "change") {
|
|
66
|
+
await this.processNewEntries();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
this.watcher.on("error", () => { });
|
|
70
|
+
return {
|
|
71
|
+
recoveredMemories: recovered.memories,
|
|
72
|
+
recoveredLogs: recovered.logs,
|
|
73
|
+
entries: recovered.savedEntries,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 監視を停止し、最終掃引を行う。
|
|
78
|
+
* inbox.md はそのまま残す(書き溜め式)。
|
|
79
|
+
*/
|
|
80
|
+
async stop() {
|
|
81
|
+
if (this.watcher) {
|
|
82
|
+
this.watcher.close();
|
|
83
|
+
this.watcher = null;
|
|
84
|
+
}
|
|
85
|
+
await this.processNewEntries();
|
|
86
|
+
return {
|
|
87
|
+
memories: this.memorySaved,
|
|
88
|
+
logs: this.logCount,
|
|
89
|
+
entries: this.savedEntries,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* ファイル全体をパースし、未処理のエントリだけを処理する。
|
|
94
|
+
*
|
|
95
|
+
* - `[memory]` タグ付き → MemoryWrite で永続化
|
|
96
|
+
* - タグ無し → カウントのみ(inbox.md にログとして残る)
|
|
97
|
+
*
|
|
98
|
+
* 処理成功ごとにカーソルを永続化するため、
|
|
99
|
+
* 途中でクラッシュしても処理済み分は再処理されない。
|
|
100
|
+
*/
|
|
101
|
+
async processNewEntries() {
|
|
102
|
+
if (this.processing)
|
|
103
|
+
return { memories: 0, logs: 0, savedEntries: [] };
|
|
104
|
+
this.processing = true;
|
|
105
|
+
const result = { memories: 0, logs: 0, savedEntries: [] };
|
|
106
|
+
try {
|
|
107
|
+
if (!existsSync(this.inboxPath))
|
|
108
|
+
return result;
|
|
109
|
+
const fullContent = await readFile(this.inboxPath, "utf-8");
|
|
110
|
+
if (!fullContent.trim())
|
|
111
|
+
return result;
|
|
112
|
+
const allEntries = fullContent
|
|
113
|
+
.split(ENTRY_SEPARATOR)
|
|
114
|
+
.map((e) => e.trim())
|
|
115
|
+
.filter((e) => e.length > 0);
|
|
116
|
+
const newEntries = allEntries.slice(this.processedCount);
|
|
117
|
+
if (newEntries.length === 0)
|
|
118
|
+
return result;
|
|
119
|
+
const writer = new MemoryWrite(this.engramsPath);
|
|
120
|
+
for (const raw of newEntries) {
|
|
121
|
+
const isMemory = MEMORY_TAG.test(raw);
|
|
122
|
+
if (isMemory) {
|
|
123
|
+
const content = raw.replace(MEMORY_TAG, "").trim();
|
|
124
|
+
try {
|
|
125
|
+
await writer.execute(this.engramId, content);
|
|
126
|
+
this.memorySaved++;
|
|
127
|
+
this.savedEntries.push(content);
|
|
128
|
+
result.memories++;
|
|
129
|
+
result.savedEntries.push(content);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(`[relic:inbox] Failed to save memory: ${err}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.logCount++;
|
|
137
|
+
result.logs++;
|
|
138
|
+
}
|
|
139
|
+
this.processedCount++;
|
|
140
|
+
this.saveCursor();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.processing = false;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
loadCursor() {
|
|
149
|
+
if (!existsSync(this.cursorPath))
|
|
150
|
+
return 0;
|
|
151
|
+
try {
|
|
152
|
+
const raw = readFileSync(this.cursorPath, "utf-8").trim();
|
|
153
|
+
const n = parseInt(raw, 10);
|
|
154
|
+
return Number.isNaN(n) ? 0 : n;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
saveCursor() {
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(this.cursorPath, String(this.processedCount), "utf-8");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* best effort */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionRecorder — PTYストリームから会話履歴とメモリを記録する
|
|
3
|
+
*
|
|
4
|
+
* 役割:
|
|
5
|
+
* 1. full_history.md に全会話を蓄積(ANSI除去済みテキスト)
|
|
6
|
+
* 2. ストリーム中の [memory] タグを検出し memory/*.md に永続化
|
|
7
|
+
*
|
|
8
|
+
* full_history.md は無限に蓄積され続ける。セッションごとにヘッダーが挿入される。
|
|
9
|
+
*/
|
|
10
|
+
export declare class SessionRecorder {
|
|
11
|
+
private readonly engramId;
|
|
12
|
+
private readonly engramsPath;
|
|
13
|
+
readonly historyPath: string;
|
|
14
|
+
private memoryBuffer;
|
|
15
|
+
private memorySaved;
|
|
16
|
+
private savedEntries;
|
|
17
|
+
constructor(engramId: string, engramsPath: string);
|
|
18
|
+
/**
|
|
19
|
+
* セッション開始。ヘッダーを full_history.md に追記する。
|
|
20
|
+
*/
|
|
21
|
+
start(shellName: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* PTYストリームのデータを処理する。
|
|
24
|
+
* spawnShellWithPty の onData コールバックから呼ぶ。
|
|
25
|
+
*
|
|
26
|
+
* - ANSI除去してfull_history.mdに追記
|
|
27
|
+
* - [memory] タグを検出してバッファに溜める
|
|
28
|
+
*/
|
|
29
|
+
onData(rawData: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* セッション終了。残りのバッファを処理する。
|
|
32
|
+
*/
|
|
33
|
+
stop(): Promise<SessionRecorderResult>;
|
|
34
|
+
/**
|
|
35
|
+
* memoryBuffer から完成した行を取り出し、[memory] タグを検出して永続化する。
|
|
36
|
+
* final=true の場合は残りのバッファも全て処理する。
|
|
37
|
+
*/
|
|
38
|
+
private flushMemoryBuffer;
|
|
39
|
+
}
|
|
40
|
+
export interface SessionRecorderResult {
|
|
41
|
+
/** memory/*.md に永続化されたメモリ数 */
|
|
42
|
+
memories: number;
|
|
43
|
+
/** 永続化されたメモリの内容 */
|
|
44
|
+
entries: string[];
|
|
45
|
+
/** full_history.md のパス */
|
|
46
|
+
historyPath: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { MemoryWrite } from "../core/usecases/memory-write.js";
|
|
4
|
+
const HISTORY_FILE = "full_history.md";
|
|
5
|
+
const MEMORY_TAG = /\[memory\]\s*/gi;
|
|
6
|
+
/**
|
|
7
|
+
* ANSIエスケープシーケンスを除去する
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line no-control-regex
|
|
10
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[>=<]|\x08/g;
|
|
11
|
+
function stripAnsi(text) {
|
|
12
|
+
return text.replace(ANSI_REGEX, "");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* SessionRecorder — PTYストリームから会話履歴とメモリを記録する
|
|
16
|
+
*
|
|
17
|
+
* 役割:
|
|
18
|
+
* 1. full_history.md に全会話を蓄積(ANSI除去済みテキスト)
|
|
19
|
+
* 2. ストリーム中の [memory] タグを検出し memory/*.md に永続化
|
|
20
|
+
*
|
|
21
|
+
* full_history.md は無限に蓄積され続ける。セッションごとにヘッダーが挿入される。
|
|
22
|
+
*/
|
|
23
|
+
export class SessionRecorder {
|
|
24
|
+
engramId;
|
|
25
|
+
engramsPath;
|
|
26
|
+
historyPath;
|
|
27
|
+
memoryBuffer = "";
|
|
28
|
+
memorySaved = 0;
|
|
29
|
+
savedEntries = [];
|
|
30
|
+
constructor(engramId, engramsPath) {
|
|
31
|
+
this.engramId = engramId;
|
|
32
|
+
this.engramsPath = engramsPath;
|
|
33
|
+
this.historyPath = join(engramsPath, engramId, HISTORY_FILE);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* セッション開始。ヘッダーを full_history.md に追記する。
|
|
37
|
+
*/
|
|
38
|
+
start(shellName) {
|
|
39
|
+
const header = [
|
|
40
|
+
"",
|
|
41
|
+
"---",
|
|
42
|
+
`## Session: ${new Date().toISOString()}`,
|
|
43
|
+
`Shell: ${shellName}`,
|
|
44
|
+
`Engram: ${this.engramId}`,
|
|
45
|
+
"---",
|
|
46
|
+
"",
|
|
47
|
+
].join("\n");
|
|
48
|
+
appendFileSync(this.historyPath, header, "utf-8");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* PTYストリームのデータを処理する。
|
|
52
|
+
* spawnShellWithPty の onData コールバックから呼ぶ。
|
|
53
|
+
*
|
|
54
|
+
* - ANSI除去してfull_history.mdに追記
|
|
55
|
+
* - [memory] タグを検出してバッファに溜める
|
|
56
|
+
*/
|
|
57
|
+
onData(rawData) {
|
|
58
|
+
const clean = stripAnsi(rawData);
|
|
59
|
+
if (clean) {
|
|
60
|
+
appendFileSync(this.historyPath, clean, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
// [memory] タグの検出
|
|
63
|
+
// PTYは断片的にデータが来るので、バッファに溜めて行単位で処理
|
|
64
|
+
this.memoryBuffer += clean;
|
|
65
|
+
this.flushMemoryBuffer(false);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* セッション終了。残りのバッファを処理する。
|
|
69
|
+
*/
|
|
70
|
+
async stop() {
|
|
71
|
+
// 末尾の改行を追加
|
|
72
|
+
appendFileSync(this.historyPath, "\n", "utf-8");
|
|
73
|
+
// バッファ内の残りを処理
|
|
74
|
+
await this.flushMemoryBuffer(true);
|
|
75
|
+
return {
|
|
76
|
+
memories: this.memorySaved,
|
|
77
|
+
entries: this.savedEntries,
|
|
78
|
+
historyPath: this.historyPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* memoryBuffer から完成した行を取り出し、[memory] タグを検出して永続化する。
|
|
83
|
+
* final=true の場合は残りのバッファも全て処理する。
|
|
84
|
+
*/
|
|
85
|
+
async flushMemoryBuffer(final) {
|
|
86
|
+
const lines = this.memoryBuffer.split("\n");
|
|
87
|
+
// 最終行は未完成の可能性があるので、final でない限り保持
|
|
88
|
+
if (!final && lines.length > 0) {
|
|
89
|
+
this.memoryBuffer = lines.pop() || "";
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.memoryBuffer = "";
|
|
93
|
+
}
|
|
94
|
+
const writer = new MemoryWrite(this.engramsPath);
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (MEMORY_TAG.test(trimmed)) {
|
|
98
|
+
const content = trimmed.replace(MEMORY_TAG, "").trim();
|
|
99
|
+
if (content) {
|
|
100
|
+
try {
|
|
101
|
+
await writer.execute(this.engramId, content);
|
|
102
|
+
this.memorySaved++;
|
|
103
|
+
this.savedEntries.push(content);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`[relic:session] Failed to save memory: ${err}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ectplsm/relic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "PROJECT RELIC — Engram injection system for AI constructs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
37
37
|
"commander": "^13.0.0",
|
|
38
|
-
"
|
|
38
|
+
"zod": "^3.23.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
42
|
"tsx": "^4.19.0",
|
|
43
|
-
"
|
|
43
|
+
"typescript": "^5.7.0"
|
|
44
44
|
}
|
|
45
45
|
}
|