@ectplsm/relic 0.2.3 → 0.3.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 +61 -496
- package/dist/adapters/local/local-engram-repository.d.ts +1 -0
- package/dist/adapters/local/local-engram-repository.js +18 -1
- package/dist/adapters/shells/claude-shell.js +6 -5
- package/dist/adapters/shells/codex-shell.js +6 -5
- package/dist/adapters/shells/gemini-shell.js +8 -0
- package/dist/adapters/shells/resume-detect.d.ts +19 -0
- package/dist/adapters/shells/resume-detect.js +51 -0
- package/dist/core/entities/engram.d.ts +6 -6
- package/dist/core/entities/engram.js +2 -2
- package/dist/core/ports/engram-repository.d.ts +2 -0
- package/dist/core/ports/shell-launcher.d.ts +6 -0
- package/dist/core/usecases/refresh-samples.d.ts +15 -0
- package/dist/core/usecases/refresh-samples.js +101 -10
- package/dist/core/usecases/summon.d.ts +1 -0
- package/dist/core/usecases/summon.js +1 -0
- package/dist/interfaces/cli/banner.d.ts +1 -0
- package/dist/interfaces/cli/banner.js +13 -0
- package/dist/interfaces/cli/commands/config.js +20 -0
- package/dist/interfaces/cli/commands/init.js +8 -6
- package/dist/interfaces/cli/commands/refresh-samples.js +19 -4
- package/dist/interfaces/cli/commands/shell.js +23 -5
- package/dist/interfaces/mcp/index.js +4 -3
- package/dist/shared/config.d.ts +9 -0
- package/dist/shared/config.js +28 -17
- package/dist/shared/engram-composer.d.ts +2 -0
- package/dist/shared/engram-composer.js +4 -3
- package/dist/shared/openclaw.d.ts +1 -1
- package/dist/shared/openclaw.js +1 -1
- package/package.json +1 -1
- package/templates/engrams/commander/IDENTITY.md +43 -0
- package/templates/engrams/{motoko → commander}/SOUL.md +5 -5
- package/templates/engrams/commander/engram.json +5 -0
- package/templates/engrams/{johnny → rebel}/IDENTITY.md +8 -8
- package/templates/engrams/{johnny → rebel}/SOUL.md +6 -6
- package/templates/engrams/rebel/engram.json +5 -0
- package/templates/engrams/motoko/IDENTITY.md +0 -43
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, writeFile, mkdir, rm, copyFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { EngramManifestSchema, EngramMetaSchema, EngramProfileSchema, } from "../../core/entities/engram.js";
|
|
@@ -91,6 +91,23 @@ export class LocalEngramRepository {
|
|
|
91
91
|
await rm(engramDir, { recursive: true });
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
async copyArchiveFiles(fromId, toId) {
|
|
95
|
+
const ARCHIVE_FILES = ["archive.md", "archive.cursor"];
|
|
96
|
+
const fromDir = join(this.basePath, fromId);
|
|
97
|
+
const toDir = join(this.basePath, toId);
|
|
98
|
+
if (!existsSync(fromDir) || !existsSync(toDir)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let copied = false;
|
|
102
|
+
for (const filename of ARCHIVE_FILES) {
|
|
103
|
+
const src = join(fromDir, filename);
|
|
104
|
+
if (existsSync(src)) {
|
|
105
|
+
await copyFile(src, join(toDir, filename));
|
|
106
|
+
copied = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return copied;
|
|
110
|
+
}
|
|
94
111
|
// --- private ---
|
|
95
112
|
async readMeta(engramDir, options) {
|
|
96
113
|
const profilePath = join(engramDir, PROFILE_FILE);
|
|
@@ -36,11 +36,12 @@ export class ClaudeShell {
|
|
|
36
36
|
console.log("Hook registered to ~/.claude/settings.json");
|
|
37
37
|
console.log();
|
|
38
38
|
}
|
|
39
|
-
const args = [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const args = [];
|
|
40
|
+
// resume 系操作時は injection をスキップ(前回セッションに焼き付き済み)
|
|
41
|
+
if (!options?.skipInjection) {
|
|
42
|
+
args.push("--system-prompt", prompt);
|
|
43
|
+
}
|
|
44
|
+
args.push(...(options?.extraArgs ?? []));
|
|
44
45
|
const env = {};
|
|
45
46
|
if (options?.engramId)
|
|
46
47
|
env.RELIC_ENGRAM_ID = options.engramId;
|
|
@@ -38,11 +38,12 @@ export class CodexShell {
|
|
|
38
38
|
console.log("Hook registered to ~/.codex/hooks.json");
|
|
39
39
|
console.log();
|
|
40
40
|
}
|
|
41
|
-
const args = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const args = [];
|
|
42
|
+
// resume 系操作時は injection をスキップ(前回セッションに焼き付き済み)
|
|
43
|
+
if (!options?.skipInjection) {
|
|
44
|
+
args.push("-c", `developer_instructions=${JSON.stringify(wrapWithOverride(prompt))}`);
|
|
45
|
+
}
|
|
46
|
+
args.push("-c", "features.codex_hooks=true", ...(options?.extraArgs ?? []));
|
|
46
47
|
const env = {};
|
|
47
48
|
if (options?.engramId)
|
|
48
49
|
env.RELIC_ENGRAM_ID = options.engramId;
|
|
@@ -106,6 +106,14 @@ export class GeminiShell {
|
|
|
106
106
|
console.log("Hook registered to ~/.gemini/settings.json");
|
|
107
107
|
console.log();
|
|
108
108
|
}
|
|
109
|
+
// resume 系操作時は injection をスキップ(前回セッションに焼き付き済み)
|
|
110
|
+
if (options?.skipInjection) {
|
|
111
|
+
const env = {};
|
|
112
|
+
if (options?.engramId)
|
|
113
|
+
env.RELIC_ENGRAM_ID = options.engramId;
|
|
114
|
+
await spawnShell(this.command, [...(options?.extraArgs ?? [])], options?.cwd, Object.keys(env).length > 0 ? env : undefined);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
109
117
|
// 2. デフォルトシステムプロンプトをキャッシュから読む、なければキャプチャ
|
|
110
118
|
let defaultPrompt;
|
|
111
119
|
if (existsSync(GEMINI_DEFAULT_CACHE)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Detection — Shell の extraArgs から resume 系操作を検出する
|
|
3
|
+
*
|
|
4
|
+
* resume 時は Engram injection をスキップし、Shell をそのまま起動する。
|
|
5
|
+
* 各 CLI の resume 引数仕様:
|
|
6
|
+
* - Claude: --resume, -r, --continue, -c, --from-pr (options)
|
|
7
|
+
* - Codex: resume, fork (subcommands)
|
|
8
|
+
* - Gemini: --resume, -r (options)
|
|
9
|
+
*
|
|
10
|
+
* 注意: Claude の -c は --continue の短縮。Codex の -c は --config の短縮。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* extraArgs が resume 系の操作を含むかを判定する。
|
|
14
|
+
*
|
|
15
|
+
* @param shellName - Shell の表示名 ("Claude Code", "Codex CLI", "Gemini CLI")
|
|
16
|
+
* @param extraArgs - Shell に渡される追加引数
|
|
17
|
+
* @returns resume 系操作が検出された場合 true
|
|
18
|
+
*/
|
|
19
|
+
export declare function isResumeArgs(shellName: string, extraArgs: string[]): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Detection — Shell の extraArgs から resume 系操作を検出する
|
|
3
|
+
*
|
|
4
|
+
* resume 時は Engram injection をスキップし、Shell をそのまま起動する。
|
|
5
|
+
* 各 CLI の resume 引数仕様:
|
|
6
|
+
* - Claude: --resume, -r, --continue, -c, --from-pr (options)
|
|
7
|
+
* - Codex: resume, fork (subcommands)
|
|
8
|
+
* - Gemini: --resume, -r (options)
|
|
9
|
+
*
|
|
10
|
+
* 注意: Claude の -c は --continue の短縮。Codex の -c は --config の短縮。
|
|
11
|
+
*/
|
|
12
|
+
/** Claude Code の resume 系オプション */
|
|
13
|
+
const CLAUDE_RESUME_FLAGS = new Set([
|
|
14
|
+
"--resume",
|
|
15
|
+
"-r",
|
|
16
|
+
"--continue",
|
|
17
|
+
"-c",
|
|
18
|
+
"--from-pr",
|
|
19
|
+
]);
|
|
20
|
+
/** Gemini CLI の resume 系オプション */
|
|
21
|
+
const GEMINI_RESUME_FLAGS = new Set([
|
|
22
|
+
"--resume",
|
|
23
|
+
"-r",
|
|
24
|
+
]);
|
|
25
|
+
/** Codex CLI の resume 系サブコマンド */
|
|
26
|
+
const CODEX_RESUME_SUBCOMMANDS = new Set([
|
|
27
|
+
"resume",
|
|
28
|
+
"fork",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* extraArgs が resume 系の操作を含むかを判定する。
|
|
32
|
+
*
|
|
33
|
+
* @param shellName - Shell の表示名 ("Claude Code", "Codex CLI", "Gemini CLI")
|
|
34
|
+
* @param extraArgs - Shell に渡される追加引数
|
|
35
|
+
* @returns resume 系操作が検出された場合 true
|
|
36
|
+
*/
|
|
37
|
+
export function isResumeArgs(shellName, extraArgs) {
|
|
38
|
+
if (extraArgs.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
switch (shellName) {
|
|
41
|
+
case "Claude Code":
|
|
42
|
+
return extraArgs.some((arg) => CLAUDE_RESUME_FLAGS.has(arg));
|
|
43
|
+
case "Codex CLI":
|
|
44
|
+
// Codex は resume/fork がサブコマンド(先頭引数)
|
|
45
|
+
return CODEX_RESUME_SUBCOMMANDS.has(extraArgs[0]);
|
|
46
|
+
case "Gemini CLI":
|
|
47
|
+
return extraArgs.some((arg) => GEMINI_RESUME_FLAGS.has(arg));
|
|
48
|
+
default:
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -40,7 +40,7 @@ export type EngramFiles = z.infer<typeof EngramFileSchema>;
|
|
|
40
40
|
* Engramプロフィール — ユーザーが編集可能な表示用メタデータ
|
|
41
41
|
*/
|
|
42
42
|
export declare const EngramProfileSchema: z.ZodObject<{
|
|
43
|
-
/** 表示名 (例: "
|
|
43
|
+
/** 表示名 (例: "Commander") */
|
|
44
44
|
name: z.ZodString;
|
|
45
45
|
/** 説明 */
|
|
46
46
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -60,7 +60,7 @@ export type EngramProfile = z.infer<typeof EngramProfileSchema>;
|
|
|
60
60
|
* Engramマニフェスト — システム管理の不変識別子と監査情報
|
|
61
61
|
*/
|
|
62
62
|
export declare const EngramManifestSchema: z.ZodObject<{
|
|
63
|
-
/** 一意識別子 (例: "
|
|
63
|
+
/** 一意識別子 (例: "commander") */
|
|
64
64
|
id: z.ZodString;
|
|
65
65
|
/** 作成日時 */
|
|
66
66
|
createdAt: z.ZodString;
|
|
@@ -80,14 +80,14 @@ export type EngramManifest = z.infer<typeof EngramManifestSchema>;
|
|
|
80
80
|
* Engramメタデータ — プロフィールとマニフェストを結合した利用時ビュー
|
|
81
81
|
*/
|
|
82
82
|
export declare const EngramMetaSchema: z.ZodObject<{
|
|
83
|
-
/** 表示名 (例: "
|
|
83
|
+
/** 表示名 (例: "Commander") */
|
|
84
84
|
name: z.ZodString;
|
|
85
85
|
/** 説明 */
|
|
86
86
|
description: z.ZodOptional<z.ZodString>;
|
|
87
87
|
/** タグ */
|
|
88
88
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
89
89
|
} & {
|
|
90
|
-
/** 一意識別子 (例: "
|
|
90
|
+
/** 一意識別子 (例: "commander") */
|
|
91
91
|
id: z.ZodString;
|
|
92
92
|
/** 作成日時 */
|
|
93
93
|
createdAt: z.ZodString;
|
|
@@ -115,14 +115,14 @@ export type EngramMeta = z.infer<typeof EngramMetaSchema>;
|
|
|
115
115
|
*/
|
|
116
116
|
export declare const EngramSchema: z.ZodObject<{
|
|
117
117
|
meta: z.ZodObject<{
|
|
118
|
-
/** 表示名 (例: "
|
|
118
|
+
/** 表示名 (例: "Commander") */
|
|
119
119
|
name: z.ZodString;
|
|
120
120
|
/** 説明 */
|
|
121
121
|
description: z.ZodOptional<z.ZodString>;
|
|
122
122
|
/** タグ */
|
|
123
123
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
124
124
|
} & {
|
|
125
|
-
/** 一意識別子 (例: "
|
|
125
|
+
/** 一意識別子 (例: "commander") */
|
|
126
126
|
id: z.ZodString;
|
|
127
127
|
/** 作成日時 */
|
|
128
128
|
createdAt: z.ZodString;
|
|
@@ -26,7 +26,7 @@ export const EngramFileSchema = z.object({
|
|
|
26
26
|
* Engramプロフィール — ユーザーが編集可能な表示用メタデータ
|
|
27
27
|
*/
|
|
28
28
|
export const EngramProfileSchema = z.object({
|
|
29
|
-
/** 表示名 (例: "
|
|
29
|
+
/** 表示名 (例: "Commander") */
|
|
30
30
|
name: z.string(),
|
|
31
31
|
/** 説明 */
|
|
32
32
|
description: z.string().optional(),
|
|
@@ -37,7 +37,7 @@ export const EngramProfileSchema = z.object({
|
|
|
37
37
|
* Engramマニフェスト — システム管理の不変識別子と監査情報
|
|
38
38
|
*/
|
|
39
39
|
export const EngramManifestSchema = z.object({
|
|
40
|
-
/** 一意識別子 (例: "
|
|
40
|
+
/** 一意識別子 (例: "commander") */
|
|
41
41
|
id: z.string(),
|
|
42
42
|
/** 作成日時 */
|
|
43
43
|
createdAt: z.string().datetime(),
|
|
@@ -14,4 +14,6 @@ export interface EngramRepository {
|
|
|
14
14
|
save(engram: Engram): Promise<void>;
|
|
15
15
|
/** Engramを削除 */
|
|
16
16
|
delete(id: string): Promise<void>;
|
|
17
|
+
/** アーカイブファイル (archive.md, archive.cursor) を別Engramへコピー */
|
|
18
|
+
copyArchiveFiles(fromId: string, toId: string): Promise<boolean>;
|
|
17
19
|
}
|
|
@@ -17,6 +17,12 @@ export interface ShellLaunchOptions {
|
|
|
17
17
|
cwd?: string;
|
|
18
18
|
/** 注入するEngram ID(Shell固有のセットアップに使用) */
|
|
19
19
|
engramId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* true の場合、Engram prompt の injection をスキップする。
|
|
22
|
+
* resume 系操作では前回セッションに injection 済みのため再注入は不要。
|
|
23
|
+
* hook セットアップと RELIC_ENGRAM_ID は skipInjection 時も維持される。
|
|
24
|
+
*/
|
|
25
|
+
skipInjection?: boolean;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* ShellLauncher — AI CLIにEngramを注入して起動する抽象ポート
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
2
2
|
export interface RefreshSamplesResult {
|
|
3
3
|
refreshed: string[];
|
|
4
|
+
seeded: string[];
|
|
4
5
|
skipped: Array<{
|
|
5
6
|
id: string;
|
|
6
7
|
reason: string;
|
|
7
8
|
}>;
|
|
9
|
+
migratedMemory: Array<{
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
}>;
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
15
|
* RefreshSamples — サンプルEngramの人格ファイルを最新テンプレートで上書きする
|
|
11
16
|
*
|
|
12
17
|
* memory / USER / archive などの運用データは保持し、
|
|
13
18
|
* SOUL.md / IDENTITY.md と updatedAt のみを更新する。
|
|
19
|
+
*
|
|
20
|
+
* ローカルに存在しないテンプレートは新規 seed する。
|
|
21
|
+
* seed 時、旧サンプルの記憶データがあれば対応する新サンプルにコピーする。
|
|
14
22
|
*/
|
|
15
23
|
export declare class RefreshSamples {
|
|
16
24
|
private readonly repository;
|
|
17
25
|
private readonly templatesDir;
|
|
18
26
|
constructor(repository: EngramRepository, templatesDir: string);
|
|
19
27
|
execute(targetIds?: string[]): Promise<RefreshSamplesResult>;
|
|
28
|
+
private seedFromTemplate;
|
|
29
|
+
/**
|
|
30
|
+
* 旧サンプルEngramから記憶データをコピーする。
|
|
31
|
+
* LEGACY_MAPPING に定義されたマッピングに基づき、
|
|
32
|
+
* USER.md / MEMORY.md / memory/*.md を引き継ぐ。
|
|
33
|
+
*/
|
|
34
|
+
private migrateMemoryFromLegacy;
|
|
20
35
|
private listTemplateIds;
|
|
21
36
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Legacy sample Engram ID → new sample Engram ID mapping.
|
|
6
|
+
* Used during seed to copy memory data from old samples.
|
|
7
|
+
*/
|
|
8
|
+
const LEGACY_MAPPING = {
|
|
9
|
+
johnny: "rebel",
|
|
10
|
+
motoko: "commander",
|
|
11
|
+
};
|
|
4
12
|
/**
|
|
5
13
|
* RefreshSamples — サンプルEngramの人格ファイルを最新テンプレートで上書きする
|
|
6
14
|
*
|
|
7
15
|
* memory / USER / archive などの運用データは保持し、
|
|
8
16
|
* SOUL.md / IDENTITY.md と updatedAt のみを更新する。
|
|
17
|
+
*
|
|
18
|
+
* ローカルに存在しないテンプレートは新規 seed する。
|
|
19
|
+
* seed 時、旧サンプルの記憶データがあれば対応する新サンプルにコピーする。
|
|
9
20
|
*/
|
|
10
21
|
export class RefreshSamples {
|
|
11
22
|
repository;
|
|
@@ -20,7 +31,9 @@ export class RefreshSamples {
|
|
|
20
31
|
: await this.listTemplateIds();
|
|
21
32
|
const result = {
|
|
22
33
|
refreshed: [],
|
|
34
|
+
seeded: [],
|
|
23
35
|
skipped: [],
|
|
36
|
+
migratedMemory: [],
|
|
24
37
|
};
|
|
25
38
|
for (const id of ids) {
|
|
26
39
|
const templateDir = join(this.templatesDir, id);
|
|
@@ -28,25 +41,103 @@ export class RefreshSamples {
|
|
|
28
41
|
result.skipped.push({ id, reason: "sample template not found" });
|
|
29
42
|
continue;
|
|
30
43
|
}
|
|
31
|
-
const engram = await this.repository.get(id);
|
|
32
|
-
if (!engram) {
|
|
33
|
-
result.skipped.push({ id, reason: "local Engram not found" });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
44
|
const soulPath = join(templateDir, "SOUL.md");
|
|
37
45
|
const identityPath = join(templateDir, "IDENTITY.md");
|
|
38
46
|
if (!existsSync(soulPath) || !existsSync(identityPath)) {
|
|
39
47
|
result.skipped.push({ id, reason: "template persona files are incomplete" });
|
|
40
48
|
continue;
|
|
41
49
|
}
|
|
42
|
-
engram
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const engram = await this.repository.get(id);
|
|
51
|
+
if (engram) {
|
|
52
|
+
// Existing engram — refresh persona files only
|
|
53
|
+
engram.files.soul = await readFile(soulPath, "utf-8");
|
|
54
|
+
engram.files.identity = await readFile(identityPath, "utf-8");
|
|
55
|
+
engram.meta.updatedAt = new Date().toISOString();
|
|
56
|
+
await this.repository.save(engram);
|
|
57
|
+
result.refreshed.push(id);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// New sample — seed it
|
|
61
|
+
const seeded = await this.seedFromTemplate(id, templateDir, soulPath, identityPath);
|
|
62
|
+
if (seeded) {
|
|
63
|
+
result.seeded.push(id);
|
|
64
|
+
// Migrate memory from legacy sample if available
|
|
65
|
+
const migrated = await this.migrateMemoryFromLegacy(id);
|
|
66
|
+
if (migrated) {
|
|
67
|
+
result.migratedMemory.push(migrated);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
result.skipped.push({ id, reason: "template engram.json not found (cannot seed)" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
return result;
|
|
49
76
|
}
|
|
77
|
+
async seedFromTemplate(id, templateDir, soulPath, identityPath) {
|
|
78
|
+
const engramJsonPath = join(templateDir, "engram.json");
|
|
79
|
+
if (!existsSync(engramJsonPath)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const profile = JSON.parse(await readFile(engramJsonPath, "utf-8"));
|
|
83
|
+
const soul = await readFile(soulPath, "utf-8");
|
|
84
|
+
const identity = await readFile(identityPath, "utf-8");
|
|
85
|
+
const now = new Date().toISOString();
|
|
86
|
+
const engram = {
|
|
87
|
+
meta: {
|
|
88
|
+
id,
|
|
89
|
+
name: profile.name ?? id,
|
|
90
|
+
description: profile.description,
|
|
91
|
+
tags: profile.tags,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
},
|
|
95
|
+
files: {
|
|
96
|
+
soul,
|
|
97
|
+
identity,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
await this.repository.save(engram);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 旧サンプルEngramから記憶データをコピーする。
|
|
105
|
+
* LEGACY_MAPPING に定義されたマッピングに基づき、
|
|
106
|
+
* USER.md / MEMORY.md / memory/*.md を引き継ぐ。
|
|
107
|
+
*/
|
|
108
|
+
async migrateMemoryFromLegacy(newId) {
|
|
109
|
+
// Find legacy ID that maps to this new ID
|
|
110
|
+
const legacyId = Object.entries(LEGACY_MAPPING)
|
|
111
|
+
.find(([, v]) => v === newId)?.[0];
|
|
112
|
+
if (!legacyId)
|
|
113
|
+
return null;
|
|
114
|
+
const legacy = await this.repository.get(legacyId);
|
|
115
|
+
if (!legacy)
|
|
116
|
+
return null;
|
|
117
|
+
// Check if legacy has any memory data worth copying
|
|
118
|
+
const hasMemory = legacy.files.user
|
|
119
|
+
|| legacy.files.memory
|
|
120
|
+
|| (legacy.files.memoryEntries && Object.keys(legacy.files.memoryEntries).length > 0);
|
|
121
|
+
if (!hasMemory)
|
|
122
|
+
return null;
|
|
123
|
+
// Load the newly seeded engram and merge memory data
|
|
124
|
+
const newEngram = await this.repository.get(newId);
|
|
125
|
+
if (!newEngram)
|
|
126
|
+
return null;
|
|
127
|
+
if (legacy.files.user)
|
|
128
|
+
newEngram.files.user = legacy.files.user;
|
|
129
|
+
if (legacy.files.memory)
|
|
130
|
+
newEngram.files.memory = legacy.files.memory;
|
|
131
|
+
if (legacy.files.memoryEntries) {
|
|
132
|
+
newEngram.files.memoryEntries = { ...legacy.files.memoryEntries };
|
|
133
|
+
}
|
|
134
|
+
newEngram.meta.updatedAt = new Date().toISOString();
|
|
135
|
+
await this.repository.save(newEngram);
|
|
136
|
+
// Archive files (archive.md, archive.cursor) are outside the Engram entity
|
|
137
|
+
// but still need to be carried over during legacy migration.
|
|
138
|
+
await this.repository.copyArchiveFiles(legacyId, newId);
|
|
139
|
+
return { from: legacyId, to: newId };
|
|
140
|
+
}
|
|
50
141
|
async listTemplateIds() {
|
|
51
142
|
if (!existsSync(this.templatesDir)) {
|
|
52
143
|
return [];
|
|
@@ -21,6 +21,7 @@ export declare class Summon {
|
|
|
21
21
|
constructor(repository: EngramRepository);
|
|
22
22
|
execute(engramId: string, options?: {
|
|
23
23
|
memoryWindowSize?: number;
|
|
24
|
+
distillationBatchSize?: number;
|
|
24
25
|
}): Promise<SummonResult>;
|
|
25
26
|
}
|
|
26
27
|
export declare class EngramNotFoundError extends Error {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function printRelicBanner(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function printRelicBanner() {
|
|
2
|
+
const lines = [
|
|
3
|
+
" ____ ________ ____________",
|
|
4
|
+
" / __ \\/ ____/ / / _/ ____/",
|
|
5
|
+
" / /_/ / __/ / / / // /",
|
|
6
|
+
" / _, _/ /___/ /____/ // /___",
|
|
7
|
+
"/_/ |_/_____/_____/___/\\____/",
|
|
8
|
+
];
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
console.log(line);
|
|
11
|
+
}
|
|
12
|
+
console.log();
|
|
13
|
+
}
|
|
@@ -80,4 +80,24 @@ export function registerConfigCommand(program) {
|
|
|
80
80
|
await saveConfig(cfg);
|
|
81
81
|
console.log(`Memory window set to: ${parsed}`);
|
|
82
82
|
});
|
|
83
|
+
// relic config distillation-batch-size [n]
|
|
84
|
+
config
|
|
85
|
+
.command("distillation-batch-size [n]")
|
|
86
|
+
.description("Get or set the number of archive entries to distill at once (default: 30)")
|
|
87
|
+
.action(async (n) => {
|
|
88
|
+
await ensureInitialized();
|
|
89
|
+
const cfg = await loadConfig();
|
|
90
|
+
if (n === undefined) {
|
|
91
|
+
console.log(String(cfg.distillationBatchSize));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const parsed = parseInt(n, 10);
|
|
95
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
96
|
+
console.error("Error: distillation-batch-size must be a positive integer");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
cfg.distillationBatchSize = parsed;
|
|
100
|
+
await saveConfig(cfg);
|
|
101
|
+
console.log(`Distillation batch size set to: ${parsed}`);
|
|
102
|
+
});
|
|
83
103
|
}
|
|
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline";
|
|
|
2
2
|
import { Init } from "../../../core/usecases/init.js";
|
|
3
3
|
import { setDefaultEngram, loadConfig } from "../../../shared/config.js";
|
|
4
4
|
import { LocalEngramRepository } from "../../../adapters/local/index.js";
|
|
5
|
+
import { printRelicBanner } from "../banner.js";
|
|
5
6
|
function ask(question) {
|
|
6
7
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
8
|
return new Promise((resolve) => {
|
|
@@ -19,6 +20,7 @@ export function registerInitCommand(program) {
|
|
|
19
20
|
const init = new Init();
|
|
20
21
|
const result = await init.execute();
|
|
21
22
|
if (result.created) {
|
|
23
|
+
printRelicBanner();
|
|
22
24
|
console.log("Initialized RELIC:");
|
|
23
25
|
console.log(` Config: ${result.configPath}`);
|
|
24
26
|
console.log(` Engrams: ${result.engramsPath}`);
|
|
@@ -34,14 +36,14 @@ export function registerInitCommand(program) {
|
|
|
34
36
|
console.log(` ${e.id.padEnd(12)} ${e.name}`);
|
|
35
37
|
}
|
|
36
38
|
console.log();
|
|
37
|
-
const
|
|
38
|
-
const prompt =
|
|
39
|
-
? `Set a default Engram? (press Enter for "${
|
|
39
|
+
const rebel = engrams.find((e) => e.id === "rebel");
|
|
40
|
+
const prompt = rebel
|
|
41
|
+
? `Set a default Engram? (press Enter for "${rebel.id}", or enter ID, or "n" to skip): `
|
|
40
42
|
: "Set a default Engram? (Enter ID, or press Enter to skip): ";
|
|
41
43
|
const answer = await ask(prompt);
|
|
42
|
-
if (answer === "" &&
|
|
43
|
-
await setDefaultEngram(
|
|
44
|
-
console.log(`Default Engram set to: ${
|
|
44
|
+
if (answer === "" && rebel) {
|
|
45
|
+
await setDefaultEngram(rebel.id);
|
|
46
|
+
console.log(`Default Engram set to: ${rebel.name} (${rebel.id})`);
|
|
45
47
|
}
|
|
46
48
|
else if (answer === "" || answer.toLowerCase() === "n") {
|
|
47
49
|
console.log(`Skipped. Run: relic config default-engram <id> to configure later.`);
|
|
@@ -12,13 +12,28 @@ export function registerRefreshSamplesCommand(program) {
|
|
|
12
12
|
const repo = new LocalEngramRepository(engramsPath);
|
|
13
13
|
const refreshSamples = new RefreshSamples(repo, TEMPLATES_DIR);
|
|
14
14
|
const result = await refreshSamples.execute(opts.engram ? [opts.engram] : undefined);
|
|
15
|
-
console.log(`Refreshed: ${result.refreshed.length}`);
|
|
16
15
|
if (result.refreshed.length > 0) {
|
|
16
|
+
console.log(`Refreshed: ${result.refreshed.length}`);
|
|
17
17
|
console.log(` IDs: ${result.refreshed.join(", ")}`);
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(`
|
|
19
|
+
if (result.seeded.length > 0) {
|
|
20
|
+
console.log(`Seeded: ${result.seeded.length}`);
|
|
21
|
+
console.log(` IDs: ${result.seeded.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
if (result.migratedMemory.length > 0) {
|
|
24
|
+
console.log(`Memory migrated:`);
|
|
25
|
+
for (const m of result.migratedMemory) {
|
|
26
|
+
console.log(` ${m.from} → ${m.to}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (result.skipped.length > 0) {
|
|
30
|
+
console.log(`Skipped: ${result.skipped.length}`);
|
|
31
|
+
for (const skipped of result.skipped) {
|
|
32
|
+
console.log(` ${skipped.id}: ${skipped.reason}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (result.refreshed.length === 0 && result.seeded.length === 0 && result.skipped.length === 0) {
|
|
36
|
+
console.log("No sample templates found.");
|
|
22
37
|
}
|
|
23
38
|
});
|
|
24
39
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { LocalEngramRepository } from "../../../adapters/local/index.js";
|
|
3
3
|
import { Summon, EngramNotFoundError } from "../../../core/usecases/index.js";
|
|
4
|
-
import { resolveEngramsPath, resolveDefaultEngram, resolveMemoryWindowSize } from "../../../shared/config.js";
|
|
4
|
+
import { resolveEngramsPath, resolveDefaultEngram, resolveMemoryWindowSize, resolveDistillationBatchSize, } from "../../../shared/config.js";
|
|
5
5
|
import { ClaudeShell } from "../../../adapters/shells/claude-shell.js";
|
|
6
6
|
import { GeminiShell } from "../../../adapters/shells/gemini-shell.js";
|
|
7
7
|
import { CodexShell } from "../../../adapters/shells/codex-shell.js";
|
|
8
|
+
import { isResumeArgs } from "../../../adapters/shells/resume-detect.js";
|
|
8
9
|
const SHELLS = [
|
|
9
10
|
{
|
|
10
11
|
name: "claude",
|
|
@@ -31,6 +32,7 @@ export function registerShellCommands(program) {
|
|
|
31
32
|
.option("-p, --path <dir>", "Override engrams directory path")
|
|
32
33
|
.option("--cwd <dir>", "Working directory for the Shell (default: current directory)")
|
|
33
34
|
.allowUnknownOption(true)
|
|
35
|
+
.allowExcessArguments(true)
|
|
34
36
|
.action(async (opts, cmd) => {
|
|
35
37
|
const launcher = shell.create();
|
|
36
38
|
// Shell利用可能チェック
|
|
@@ -50,14 +52,30 @@ export function registerShellCommands(program) {
|
|
|
50
52
|
const repo = new LocalEngramRepository(engramsPath);
|
|
51
53
|
const summon = new Summon(repo);
|
|
52
54
|
const memoryWindowSize = await resolveMemoryWindowSize();
|
|
55
|
+
const distillationBatchSize = await resolveDistillationBatchSize();
|
|
53
56
|
try {
|
|
54
|
-
const result = await summon.execute(engramId, {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const result = await summon.execute(engramId, {
|
|
58
|
+
memoryWindowSize,
|
|
59
|
+
distillationBatchSize,
|
|
60
|
+
});
|
|
57
61
|
// --engram, --path, --cwd 以外の引数をShellにパススルー
|
|
58
62
|
const extraArgs = cmd.args;
|
|
59
63
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
60
|
-
|
|
64
|
+
// resume 系操作の検出
|
|
65
|
+
const skipInjection = isResumeArgs(launcher.name, extraArgs);
|
|
66
|
+
if (skipInjection) {
|
|
67
|
+
console.log(`Resuming ${launcher.name} session (${result.engramName})...`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(`Summoning "${result.engramName}" into ${launcher.name}...`);
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
await launcher.launch(result.prompt, {
|
|
74
|
+
extraArgs,
|
|
75
|
+
cwd,
|
|
76
|
+
engramId,
|
|
77
|
+
skipInjection,
|
|
78
|
+
});
|
|
61
79
|
}
|
|
62
80
|
catch (err) {
|
|
63
81
|
if (err instanceof EngramNotFoundError) {
|
|
@@ -12,7 +12,7 @@ import { LocalEngramRepository } from "../../adapters/local/index.js";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { readFile, writeFile } from "node:fs/promises";
|
|
15
|
-
import { resolveEngramsPath } from "../../shared/config.js";
|
|
15
|
+
import { resolveDistillationBatchSize, resolveEngramsPath, } from "../../shared/config.js";
|
|
16
16
|
const server = new McpServer({
|
|
17
17
|
name: "relic",
|
|
18
18
|
version: "0.1.0",
|
|
@@ -132,7 +132,7 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
|
|
|
132
132
|
limit: z
|
|
133
133
|
.number()
|
|
134
134
|
.optional()
|
|
135
|
-
.describe("Max entries to return (default:
|
|
135
|
+
.describe("Max entries to return (default: config.distillationBatchSize)"),
|
|
136
136
|
path: z
|
|
137
137
|
.string()
|
|
138
138
|
.optional()
|
|
@@ -140,8 +140,9 @@ server.tool("relic_archive_pending", "Get un-distilled archive entries since the
|
|
|
140
140
|
}, async (args) => {
|
|
141
141
|
const engramsPath = await resolveEngramsPath(args.path);
|
|
142
142
|
const archivePending = new ArchivePending(engramsPath);
|
|
143
|
+
const defaultLimit = await resolveDistillationBatchSize();
|
|
143
144
|
try {
|
|
144
|
-
const result = await archivePending.execute(args.id, args.limit);
|
|
145
|
+
const result = await archivePending.execute(args.id, args.limit ?? defaultLimit);
|
|
145
146
|
if (result.entries.length === 0) {
|
|
146
147
|
return {
|
|
147
148
|
content: [
|