@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
export async function registerTrustedFolders(relicEngramsPath) {
|
|
6
|
+
const result = { registered: [], skipped: [] };
|
|
7
|
+
await Promise.all([
|
|
8
|
+
registerClaude(relicEngramsPath, result),
|
|
9
|
+
registerCodex(relicEngramsPath, result),
|
|
10
|
+
registerGemini(relicEngramsPath, result),
|
|
11
|
+
]);
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Claude Code — ~/.claude/settings.json
|
|
16
|
+
// { "sandbox": { "filesystem": { "allowWrite": ["~/.relic/engrams"] } } }
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
async function registerClaude(engramsPath, result) {
|
|
19
|
+
const claudeDir = join(homedir(), ".claude");
|
|
20
|
+
const configPath = join(claudeDir, "settings.json");
|
|
21
|
+
if (!existsSync(claudeDir)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
let settings = {};
|
|
27
|
+
if (existsSync(configPath)) {
|
|
28
|
+
const raw = await readFile(configPath, "utf-8");
|
|
29
|
+
settings = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
// sandbox.filesystem.allowWrite を確保
|
|
32
|
+
settings.sandbox ??= {};
|
|
33
|
+
settings.sandbox.filesystem ??= {};
|
|
34
|
+
settings.sandbox.filesystem.allowWrite ??= [];
|
|
35
|
+
const allowWrite = settings.sandbox.filesystem.allowWrite;
|
|
36
|
+
// permissions.allow — ツール承認の自動化
|
|
37
|
+
settings.permissions ??= {};
|
|
38
|
+
settings.permissions.allow ??= [];
|
|
39
|
+
const allow = settings.permissions.allow;
|
|
40
|
+
const tildeForm = engramsPath.replace(homedir(), "~");
|
|
41
|
+
// sandbox: allowWrite
|
|
42
|
+
const sandboxNeeded = !allowWrite.includes(engramsPath) && !allowWrite.includes(tildeForm);
|
|
43
|
+
if (sandboxNeeded) {
|
|
44
|
+
allowWrite.push(tildeForm);
|
|
45
|
+
}
|
|
46
|
+
// permissions: Edit allow
|
|
47
|
+
const editRule = `Edit(${tildeForm}/**)`;
|
|
48
|
+
const permNeeded = !allow.includes(editRule);
|
|
49
|
+
if (permNeeded) {
|
|
50
|
+
allow.push(editRule);
|
|
51
|
+
}
|
|
52
|
+
if (!sandboxNeeded && !permNeeded) {
|
|
53
|
+
result.skipped.push("Claude Code");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await writeFile(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
57
|
+
result.registered.push("Claude Code");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// best effort
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Codex CLI — ~/.codex/config.toml
|
|
65
|
+
// [sandbox]
|
|
66
|
+
// writable_roots = ["~/.relic/engrams"]
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
async function registerCodex(engramsPath, result) {
|
|
69
|
+
const codexDir = join(homedir(), ".codex");
|
|
70
|
+
const configPath = join(codexDir, "config.toml");
|
|
71
|
+
if (!existsSync(codexDir)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
let content = "";
|
|
76
|
+
if (existsSync(configPath)) {
|
|
77
|
+
content = await readFile(configPath, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
const tildeForm = engramsPath.replace(homedir(), "~");
|
|
80
|
+
// 既に writable_roots に含まれているかチェック
|
|
81
|
+
if (content.includes(engramsPath) || content.includes(tildeForm)) {
|
|
82
|
+
result.skipped.push("Codex CLI");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// [sandbox] セクションが存在するか
|
|
86
|
+
const sandboxMatch = content.match(/^\[sandbox\]\s*$/m);
|
|
87
|
+
if (sandboxMatch) {
|
|
88
|
+
// [sandbox] セクション内の writable_roots を探す
|
|
89
|
+
const writableMatch = content.match(/^(writable_roots\s*=\s*\[)(.*?)(\])/m);
|
|
90
|
+
if (writableMatch) {
|
|
91
|
+
// 既存の writable_roots に追加
|
|
92
|
+
const existing = writableMatch[2].trim();
|
|
93
|
+
const newValue = existing
|
|
94
|
+
? `${existing}, "${tildeForm}"`
|
|
95
|
+
: `"${tildeForm}"`;
|
|
96
|
+
content = content.replace(/^(writable_roots\s*=\s*\[)(.*?)(\])/m, `$1${newValue}$3`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// [sandbox] セクションに writable_roots を追加
|
|
100
|
+
content = content.replace(/^\[sandbox\]\s*$/m, `[sandbox]\nwritable_roots = ["${tildeForm}"]`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// [sandbox] セクションごと追加
|
|
105
|
+
content =
|
|
106
|
+
content.trimEnd() + `\n\n[sandbox]\nwritable_roots = ["${tildeForm}"]\n`;
|
|
107
|
+
}
|
|
108
|
+
await writeFile(configPath, content, "utf-8");
|
|
109
|
+
result.registered.push("Codex CLI");
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// best effort
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Gemini CLI — ~/.gemini/trustedFolders.json
|
|
117
|
+
// { "/path/to/engrams": "TRUST_FOLDER" }
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
async function registerGemini(engramsPath, result) {
|
|
120
|
+
const configPath = join(homedir(), ".gemini", "trustedFolders.json");
|
|
121
|
+
if (!existsSync(join(homedir(), ".gemini"))) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
let folders = {};
|
|
126
|
+
if (existsSync(configPath)) {
|
|
127
|
+
const raw = await readFile(configPath, "utf-8");
|
|
128
|
+
folders = JSON.parse(raw);
|
|
129
|
+
}
|
|
130
|
+
if (folders[engramsPath]) {
|
|
131
|
+
result.skipped.push("Gemini CLI");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
folders[engramsPath] = "TRUST_FOLDER";
|
|
135
|
+
await writeFile(configPath, JSON.stringify(folders, null, 2), "utf-8");
|
|
136
|
+
result.registered.push("Gemini CLI");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// best effort
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -2,15 +2,30 @@
|
|
|
2
2
|
* 注入モード — ShellがEngramをどの経路で受け取るか
|
|
3
3
|
*
|
|
4
4
|
* - system-prompt: システムプロンプトとして直接上書き(最も強力)
|
|
5
|
+
* - developer-message: developerロールのメッセージとして注入(system-promptに準じる強度)
|
|
5
6
|
* - instruction-file: 設定ファイルに書き出して補助指示として注入
|
|
6
7
|
* - user-message: 初回ユーザーメッセージとして送り込む
|
|
7
8
|
*/
|
|
8
|
-
export type InjectionMode = "system-prompt" | "instruction-file" | "user-message";
|
|
9
|
+
export type InjectionMode = "system-prompt" | "developer-message" | "instruction-file" | "user-message";
|
|
10
|
+
/**
|
|
11
|
+
* Shell起動オプション
|
|
12
|
+
*/
|
|
13
|
+
export interface ShellLaunchOptions {
|
|
14
|
+
/** Shell に追加で渡す引数 */
|
|
15
|
+
extraArgs?: string[];
|
|
16
|
+
/** Shell の作業ディレクトリ */
|
|
17
|
+
cwd?: string;
|
|
18
|
+
/** 注入するEngram ID(Shell固有のセットアップに使用) */
|
|
19
|
+
engramId?: string;
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
22
|
* ShellLauncher — AI CLIにEngramを注入して起動する抽象ポート
|
|
11
23
|
*
|
|
12
24
|
* 各Shell(LLM CLI)ごとにシステムプロンプトの渡し方が異なるため、
|
|
13
25
|
* 具象実装はadapters/shells/に配置される。
|
|
26
|
+
*
|
|
27
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
28
|
+
* ShellLauncherは注入に特化する。
|
|
14
29
|
*/
|
|
15
30
|
export interface ShellLauncher {
|
|
16
31
|
/** Shell種別の表示名 */
|
|
@@ -23,5 +38,5 @@ export interface ShellLauncher {
|
|
|
23
38
|
* EngramプロンプトをShellに注入して起動する。
|
|
24
39
|
* プロセスはフォアグラウンドで実行され、ユーザーが終了するまでブロックする。
|
|
25
40
|
*/
|
|
26
|
-
launch(prompt: string,
|
|
41
|
+
launch(prompt: string, options?: ShellLaunchOptions): Promise<void>;
|
|
27
42
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ArchiveCursorUpdateResult {
|
|
2
|
+
/** 更新前のcursor位置 */
|
|
3
|
+
previousCursor: number;
|
|
4
|
+
/** 更新後のcursor位置 */
|
|
5
|
+
newCursor: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* ArchiveCursorUpdate — archive.cursor を指定件数分だけ進める
|
|
9
|
+
*
|
|
10
|
+
* relic_memory_write 時に呼ばれ、蒸留済みエントリ数だけcursorを前進させる。
|
|
11
|
+
* 実際に蒸留した件数(= relic_archive_pending で返した件数)を受け取る。
|
|
12
|
+
*/
|
|
13
|
+
export declare class ArchiveCursorUpdate {
|
|
14
|
+
private readonly engramsPath;
|
|
15
|
+
constructor(engramsPath: string);
|
|
16
|
+
execute(engramId: string, count: number): Promise<ArchiveCursorUpdateResult>;
|
|
17
|
+
private readCursor;
|
|
18
|
+
}
|
|
19
|
+
export declare class ArchiveCursorUpdateEngramNotFoundError extends Error {
|
|
20
|
+
constructor(id: string);
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
/**
|
|
5
|
+
* ArchiveCursorUpdate — archive.cursor を指定件数分だけ進める
|
|
6
|
+
*
|
|
7
|
+
* relic_memory_write 時に呼ばれ、蒸留済みエントリ数だけcursorを前進させる。
|
|
8
|
+
* 実際に蒸留した件数(= relic_archive_pending で返した件数)を受け取る。
|
|
9
|
+
*/
|
|
10
|
+
export class ArchiveCursorUpdate {
|
|
11
|
+
engramsPath;
|
|
12
|
+
constructor(engramsPath) {
|
|
13
|
+
this.engramsPath = engramsPath;
|
|
14
|
+
}
|
|
15
|
+
async execute(engramId, count) {
|
|
16
|
+
const engramDir = join(this.engramsPath, engramId);
|
|
17
|
+
if (!existsSync(engramDir)) {
|
|
18
|
+
throw new ArchiveCursorUpdateEngramNotFoundError(engramId);
|
|
19
|
+
}
|
|
20
|
+
const cursorPath = join(engramDir, "archive.cursor");
|
|
21
|
+
const previousCursor = await this.readCursor(cursorPath);
|
|
22
|
+
const newCursor = previousCursor + count;
|
|
23
|
+
await writeFile(cursorPath, String(newCursor), "utf-8");
|
|
24
|
+
return { previousCursor, newCursor };
|
|
25
|
+
}
|
|
26
|
+
async readCursor(cursorPath) {
|
|
27
|
+
if (!existsSync(cursorPath))
|
|
28
|
+
return 0;
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readFile(cursorPath, "utf-8");
|
|
31
|
+
const value = parseInt(raw.trim(), 10);
|
|
32
|
+
return Number.isNaN(value) ? 0 : value;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class ArchiveCursorUpdateEngramNotFoundError extends Error {
|
|
40
|
+
constructor(id) {
|
|
41
|
+
super(`Engram "${id}" archive not found`);
|
|
42
|
+
this.name = "ArchiveCursorUpdateEngramNotFoundError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ArchivePendingResult {
|
|
2
|
+
/** 未蒸留エントリ(cursor以降) */
|
|
3
|
+
entries: string[];
|
|
4
|
+
/** 現在のcursor位置 */
|
|
5
|
+
cursor: number;
|
|
6
|
+
/** archive内の総エントリ数 */
|
|
7
|
+
total: number;
|
|
8
|
+
/** 今回返さなかった残りの未蒸留エントリ数 */
|
|
9
|
+
remaining: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* ArchivePending — archive.md の未蒸留エントリを取得する
|
|
13
|
+
*
|
|
14
|
+
* archive.cursor に記録された位置以降のエントリを返す。
|
|
15
|
+
* 読み取り専用 — cursorの更新は ArchiveCursorUpdate が担う。
|
|
16
|
+
*/
|
|
17
|
+
export declare class ArchivePending {
|
|
18
|
+
private readonly engramsPath;
|
|
19
|
+
constructor(engramsPath: string);
|
|
20
|
+
execute(engramId: string, limit?: number): Promise<ArchivePendingResult>;
|
|
21
|
+
private readCursor;
|
|
22
|
+
}
|
|
23
|
+
export declare class ArchivePendingEngramNotFoundError extends Error {
|
|
24
|
+
constructor(id: string);
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
const ENTRY_SEPARATOR = /\n---\n/;
|
|
5
|
+
const DEFAULT_LIMIT = 30;
|
|
6
|
+
/**
|
|
7
|
+
* ArchivePending — archive.md の未蒸留エントリを取得する
|
|
8
|
+
*
|
|
9
|
+
* archive.cursor に記録された位置以降のエントリを返す。
|
|
10
|
+
* 読み取り専用 — cursorの更新は ArchiveCursorUpdate が担う。
|
|
11
|
+
*/
|
|
12
|
+
export class ArchivePending {
|
|
13
|
+
engramsPath;
|
|
14
|
+
constructor(engramsPath) {
|
|
15
|
+
this.engramsPath = engramsPath;
|
|
16
|
+
}
|
|
17
|
+
async execute(engramId, limit) {
|
|
18
|
+
const engramDir = join(this.engramsPath, engramId);
|
|
19
|
+
const archivePath = join(engramDir, "archive.md");
|
|
20
|
+
if (!existsSync(archivePath)) {
|
|
21
|
+
throw new ArchivePendingEngramNotFoundError(engramId);
|
|
22
|
+
}
|
|
23
|
+
const raw = await readFile(archivePath, "utf-8");
|
|
24
|
+
if (!raw.trim()) {
|
|
25
|
+
return { entries: [], cursor: 0, total: 0, remaining: 0 };
|
|
26
|
+
}
|
|
27
|
+
const allEntries = raw
|
|
28
|
+
.split(ENTRY_SEPARATOR)
|
|
29
|
+
.map((e) => e.trim())
|
|
30
|
+
.filter((e) => e.length > 0);
|
|
31
|
+
const cursor = await this.readCursor(engramDir);
|
|
32
|
+
const pendingEntries = allEntries.slice(cursor);
|
|
33
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
34
|
+
const returned = pendingEntries.slice(0, effectiveLimit);
|
|
35
|
+
return {
|
|
36
|
+
entries: returned,
|
|
37
|
+
cursor,
|
|
38
|
+
total: allEntries.length,
|
|
39
|
+
remaining: pendingEntries.length - returned.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async readCursor(engramDir) {
|
|
43
|
+
const cursorPath = join(engramDir, "archive.cursor");
|
|
44
|
+
if (!existsSync(cursorPath))
|
|
45
|
+
return 0;
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readFile(cursorPath, "utf-8");
|
|
48
|
+
const value = parseInt(raw.trim(), 10);
|
|
49
|
+
return Number.isNaN(value) ? 0 : value;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class ArchivePendingEngramNotFoundError extends Error {
|
|
57
|
+
constructor(id) {
|
|
58
|
+
super(`Engram "${id}" archive not found`);
|
|
59
|
+
this.name = "ArchivePendingEngramNotFoundError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ArchiveSearchResult {
|
|
2
|
+
/** マッチしたエントリの内容 */
|
|
3
|
+
entry: string;
|
|
4
|
+
/** エントリのインデックス(新しい順: 0が最新) */
|
|
5
|
+
index: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* ArchiveSearch — archive.md をキーワード検索する
|
|
9
|
+
*
|
|
10
|
+
* archive.md は全セッションログ + [memory] エントリを含む生データ。
|
|
11
|
+
* memory/*.md(蒸留済み)より情報量が多いため、検索先として優れている。
|
|
12
|
+
*/
|
|
13
|
+
export declare class ArchiveSearch {
|
|
14
|
+
private readonly engramsPath;
|
|
15
|
+
constructor(engramsPath: string);
|
|
16
|
+
search(engramId: string, query: string, limit?: number): Promise<ArchiveSearchResult[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare class ArchiveSearchEngramNotFoundError extends Error {
|
|
19
|
+
constructor(id: string);
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
const ENTRY_SEPARATOR = /\n---\n/;
|
|
5
|
+
/**
|
|
6
|
+
* ArchiveSearch — archive.md をキーワード検索する
|
|
7
|
+
*
|
|
8
|
+
* archive.md は全セッションログ + [memory] エントリを含む生データ。
|
|
9
|
+
* memory/*.md(蒸留済み)より情報量が多いため、検索先として優れている。
|
|
10
|
+
*/
|
|
11
|
+
export class ArchiveSearch {
|
|
12
|
+
engramsPath;
|
|
13
|
+
constructor(engramsPath) {
|
|
14
|
+
this.engramsPath = engramsPath;
|
|
15
|
+
}
|
|
16
|
+
async search(engramId, query, limit = 5) {
|
|
17
|
+
const archivePath = join(this.engramsPath, engramId, "archive.md");
|
|
18
|
+
if (!existsSync(archivePath)) {
|
|
19
|
+
throw new ArchiveSearchEngramNotFoundError(engramId);
|
|
20
|
+
}
|
|
21
|
+
const raw = await readFile(archivePath, "utf-8");
|
|
22
|
+
if (!raw.trim())
|
|
23
|
+
return [];
|
|
24
|
+
const entries = raw
|
|
25
|
+
.split(ENTRY_SEPARATOR)
|
|
26
|
+
.map((e) => e.trim())
|
|
27
|
+
.filter((e) => e.length > 0)
|
|
28
|
+
.reverse(); // 新しい順
|
|
29
|
+
const queryLower = query.toLowerCase();
|
|
30
|
+
const results = [];
|
|
31
|
+
for (let i = 0; i < entries.length; i++) {
|
|
32
|
+
if (entries[i].toLowerCase().includes(queryLower)) {
|
|
33
|
+
results.push({ entry: entries[i], index: i });
|
|
34
|
+
}
|
|
35
|
+
if (results.length >= limit)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ArchiveSearchEngramNotFoundError extends Error {
|
|
42
|
+
constructor(id) {
|
|
43
|
+
super(`Engram "${id}" archive not found`);
|
|
44
|
+
this.name = "ArchiveSearchEngramNotFoundError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface InboxSearchResult {
|
|
2
|
+
/** マッチしたエントリの内容 */
|
|
3
|
+
entry: string;
|
|
4
|
+
/** エントリのインデックス(新しい順: 0が最新) */
|
|
5
|
+
index: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* InboxSearch — inbox.md をキーワード検索する
|
|
9
|
+
*
|
|
10
|
+
* inbox.md は全セッションログ + [memory] エントリを含む生データ。
|
|
11
|
+
* memory/*.md(蒸留済み)より情報量が多いため、検索先として優れている。
|
|
12
|
+
*/
|
|
13
|
+
export declare class InboxSearch {
|
|
14
|
+
private readonly engramsPath;
|
|
15
|
+
constructor(engramsPath: string);
|
|
16
|
+
search(engramId: string, query: string, limit?: number): Promise<InboxSearchResult[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare class InboxSearchEngramNotFoundError extends Error {
|
|
19
|
+
constructor(id: string);
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
const ENTRY_SEPARATOR = /\n---\n/;
|
|
5
|
+
/**
|
|
6
|
+
* InboxSearch — inbox.md をキーワード検索する
|
|
7
|
+
*
|
|
8
|
+
* inbox.md は全セッションログ + [memory] エントリを含む生データ。
|
|
9
|
+
* memory/*.md(蒸留済み)より情報量が多いため、検索先として優れている。
|
|
10
|
+
*/
|
|
11
|
+
export class InboxSearch {
|
|
12
|
+
engramsPath;
|
|
13
|
+
constructor(engramsPath) {
|
|
14
|
+
this.engramsPath = engramsPath;
|
|
15
|
+
}
|
|
16
|
+
async search(engramId, query, limit = 5) {
|
|
17
|
+
const inboxPath = join(this.engramsPath, engramId, "inbox.md");
|
|
18
|
+
if (!existsSync(inboxPath)) {
|
|
19
|
+
throw new InboxSearchEngramNotFoundError(engramId);
|
|
20
|
+
}
|
|
21
|
+
const raw = await readFile(inboxPath, "utf-8");
|
|
22
|
+
if (!raw.trim())
|
|
23
|
+
return [];
|
|
24
|
+
const entries = raw
|
|
25
|
+
.split(ENTRY_SEPARATOR)
|
|
26
|
+
.map((e) => e.trim())
|
|
27
|
+
.filter((e) => e.length > 0)
|
|
28
|
+
.reverse(); // 新しい順
|
|
29
|
+
const queryLower = query.toLowerCase();
|
|
30
|
+
const results = [];
|
|
31
|
+
for (let i = 0; i < entries.length; i++) {
|
|
32
|
+
if (entries[i].toLowerCase().includes(queryLower)) {
|
|
33
|
+
results.push({ entry: entries[i], index: i });
|
|
34
|
+
}
|
|
35
|
+
if (results.length >= limit)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class InboxSearchEngramNotFoundError extends Error {
|
|
42
|
+
constructor(id) {
|
|
43
|
+
super(`Engram "${id}" inbox not found`);
|
|
44
|
+
this.name = "InboxSearchEngramNotFoundError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface InboxWriteResult {
|
|
2
|
+
engramId: string;
|
|
3
|
+
/** inbox.md に追記されたエントリ数 */
|
|
4
|
+
totalEntries: number;
|
|
5
|
+
/** memory/*.md に永続化されたメモリ数 */
|
|
6
|
+
memoriesSaved: number;
|
|
7
|
+
/** ログとして記録されたエントリ数 */
|
|
8
|
+
logsRecorded: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* InboxWrite — MCP経由で inbox.md にエントリを追記する
|
|
12
|
+
*
|
|
13
|
+
* CLI の MemoryInbox (fs.watch) と同じフォーマットで inbox.md に書き込む。
|
|
14
|
+
* `[memory]` タグ付きエントリは即座に memory/*.md にも永続化する。
|
|
15
|
+
*
|
|
16
|
+
* これにより CLI Shell と MCP Desktop で同一の inbox.md フォーマットを共有し、
|
|
17
|
+
* セッションログ + メモリ永続化の二重の役割を果たす。
|
|
18
|
+
*/
|
|
19
|
+
export declare class InboxWrite {
|
|
20
|
+
private readonly engramsPath;
|
|
21
|
+
constructor(engramsPath: string);
|
|
22
|
+
execute(engramId: string, content: string): Promise<InboxWriteResult>;
|
|
23
|
+
private appendToInbox;
|
|
24
|
+
}
|
|
25
|
+
export declare class InboxWriteEngramNotFoundError extends Error {
|
|
26
|
+
constructor(id: string);
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { MemoryWrite } from "./memory-write.js";
|
|
5
|
+
const ENTRY_SEPARATOR = /\n---\n/;
|
|
6
|
+
const MEMORY_TAG = /^\[memory\]\s*/i;
|
|
7
|
+
/**
|
|
8
|
+
* InboxWrite — MCP経由で inbox.md にエントリを追記する
|
|
9
|
+
*
|
|
10
|
+
* CLI の MemoryInbox (fs.watch) と同じフォーマットで inbox.md に書き込む。
|
|
11
|
+
* `[memory]` タグ付きエントリは即座に memory/*.md にも永続化する。
|
|
12
|
+
*
|
|
13
|
+
* これにより CLI Shell と MCP Desktop で同一の inbox.md フォーマットを共有し、
|
|
14
|
+
* セッションログ + メモリ永続化の二重の役割を果たす。
|
|
15
|
+
*/
|
|
16
|
+
export class InboxWrite {
|
|
17
|
+
engramsPath;
|
|
18
|
+
constructor(engramsPath) {
|
|
19
|
+
this.engramsPath = engramsPath;
|
|
20
|
+
}
|
|
21
|
+
async execute(engramId, content) {
|
|
22
|
+
const engramDir = join(this.engramsPath, engramId);
|
|
23
|
+
if (!existsSync(engramDir)) {
|
|
24
|
+
throw new InboxWriteEngramNotFoundError(engramId);
|
|
25
|
+
}
|
|
26
|
+
// inbox.md に追記
|
|
27
|
+
const inboxPath = join(engramDir, "inbox.md");
|
|
28
|
+
await this.appendToInbox(inboxPath, content);
|
|
29
|
+
// エントリをパースして [memory] タグ付きを永続化
|
|
30
|
+
const entries = content
|
|
31
|
+
.split(ENTRY_SEPARATOR)
|
|
32
|
+
.map((e) => e.trim())
|
|
33
|
+
.filter((e) => e.length > 0);
|
|
34
|
+
const writer = new MemoryWrite(this.engramsPath);
|
|
35
|
+
let memoriesSaved = 0;
|
|
36
|
+
let logsRecorded = 0;
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (MEMORY_TAG.test(entry)) {
|
|
39
|
+
const memContent = entry.replace(MEMORY_TAG, "").trim();
|
|
40
|
+
await writer.execute(engramId, memContent);
|
|
41
|
+
memoriesSaved++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logsRecorded++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
engramId,
|
|
49
|
+
totalEntries: entries.length,
|
|
50
|
+
memoriesSaved,
|
|
51
|
+
logsRecorded,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async appendToInbox(inboxPath, content) {
|
|
55
|
+
await mkdir(join(inboxPath, ".."), { recursive: true });
|
|
56
|
+
let existing = "";
|
|
57
|
+
if (existsSync(inboxPath)) {
|
|
58
|
+
existing = await readFile(inboxPath, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
// 既存内容があれば --- で区切って追記
|
|
61
|
+
const separator = existing.length > 0 && !existing.endsWith("\n---\n")
|
|
62
|
+
? "\n---\n"
|
|
63
|
+
: "";
|
|
64
|
+
await writeFile(inboxPath, existing + separator + content + "\n", "utf-8");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class InboxWriteEngramNotFoundError extends Error {
|
|
68
|
+
constructor(id) {
|
|
69
|
+
super(`Engram "${id}" not found`);
|
|
70
|
+
this.name = "InboxWriteEngramNotFoundError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -3,6 +3,7 @@ export { ListEngrams } from "./list-engrams.js";
|
|
|
3
3
|
export { Init, type InitResult } from "./init.js";
|
|
4
4
|
export { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, type InjectResult, } from "./inject.js";
|
|
5
5
|
export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, type ExtractResult, } from "./extract.js";
|
|
6
|
-
export { MemorySearch, MemoryEngramNotFoundError, type MemorySearchResult, } from "./memory-search.js";
|
|
7
6
|
export { MemoryWrite, MemoryWriteEngramNotFoundError, type MemoryWriteResult, } from "./memory-write.js";
|
|
8
7
|
export { Sync, SyncAgentsDirNotFoundError, type SyncTarget, type SyncInitialResult, } from "./sync.js";
|
|
8
|
+
export { ArchivePending, ArchivePendingEngramNotFoundError, type ArchivePendingResult, } from "./archive-pending.js";
|
|
9
|
+
export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, type ArchiveCursorUpdateResult, } from "./archive-cursor-update.js";
|
|
@@ -3,6 +3,7 @@ export { ListEngrams } from "./list-engrams.js";
|
|
|
3
3
|
export { Init } from "./init.js";
|
|
4
4
|
export { Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, } from "./inject.js";
|
|
5
5
|
export { Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, } from "./extract.js";
|
|
6
|
-
export { MemorySearch, MemoryEngramNotFoundError, } from "./memory-search.js";
|
|
7
6
|
export { MemoryWrite, MemoryWriteEngramNotFoundError, } from "./memory-write.js";
|
|
8
7
|
export { Sync, SyncAgentsDirNotFoundError, } from "./sync.js";
|
|
8
|
+
export { ArchivePending, ArchivePendingEngramNotFoundError, } from "./archive-pending.js";
|
|
9
|
+
export { ArchiveCursorUpdate, ArchiveCursorUpdateEngramNotFoundError, } from "./archive-cursor-update.js";
|
|
@@ -12,11 +12,16 @@ export interface SummonResult {
|
|
|
12
12
|
*
|
|
13
13
|
* これがRELICの中核オペレーション。
|
|
14
14
|
* Engramの取得 → Markdown結合 → 注入可能なプロンプト生成 を行う。
|
|
15
|
+
*
|
|
16
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
17
|
+
* CLIはEngramの注入に特化する。
|
|
15
18
|
*/
|
|
16
19
|
export declare class Summon {
|
|
17
20
|
private readonly repository;
|
|
18
21
|
constructor(repository: EngramRepository);
|
|
19
|
-
execute(engramId: string
|
|
22
|
+
execute(engramId: string, options?: {
|
|
23
|
+
memoryWindowSize?: number;
|
|
24
|
+
}): Promise<SummonResult>;
|
|
20
25
|
}
|
|
21
26
|
export declare class EngramNotFoundError extends Error {
|
|
22
27
|
constructor(id: string);
|
|
@@ -4,18 +4,24 @@ import { composeEngram } from "../../shared/engram-composer.js";
|
|
|
4
4
|
*
|
|
5
5
|
* これがRELICの中核オペレーション。
|
|
6
6
|
* Engramの取得 → Markdown結合 → 注入可能なプロンプト生成 を行う。
|
|
7
|
+
*
|
|
8
|
+
* archiveへの書き込みはバックグラウンドhookが自動で行う。
|
|
9
|
+
* CLIはEngramの注入に特化する。
|
|
7
10
|
*/
|
|
8
11
|
export class Summon {
|
|
9
12
|
repository;
|
|
10
13
|
constructor(repository) {
|
|
11
14
|
this.repository = repository;
|
|
12
15
|
}
|
|
13
|
-
async execute(engramId) {
|
|
16
|
+
async execute(engramId, options) {
|
|
14
17
|
const engram = await this.repository.get(engramId);
|
|
15
18
|
if (!engram) {
|
|
16
19
|
throw new EngramNotFoundError(engramId);
|
|
17
20
|
}
|
|
18
|
-
const prompt = composeEngram(engram.files
|
|
21
|
+
const prompt = composeEngram(engram.files, {
|
|
22
|
+
meta: engram.meta,
|
|
23
|
+
memoryWindowSize: options?.memoryWindowSize,
|
|
24
|
+
});
|
|
19
25
|
return {
|
|
20
26
|
engramId: engram.meta.id,
|
|
21
27
|
engramName: engram.meta.name,
|