@ectplsm/relic 0.2.2 → 0.3.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 +69 -493
- package/dist/adapters/local/local-engram-repository.d.ts +1 -0
- package/dist/adapters/local/local-engram-repository.js +18 -1
- 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/usecases/create-engram.d.ts +26 -0
- package/dist/core/usecases/create-engram.js +54 -0
- package/dist/core/usecases/delete-engram.d.ts +24 -0
- package/dist/core/usecases/delete-engram.js +46 -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/create.d.ts +2 -0
- package/dist/interfaces/cli/commands/create.js +143 -0
- package/dist/interfaces/cli/commands/delete.d.ts +2 -0
- package/dist/interfaces/cli/commands/delete.js +113 -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 +6 -2
- package/dist/interfaces/cli/index.js +4 -0
- package/dist/interfaces/mcp/index.js +72 -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/dist/shared/templates.d.ts +6 -0
- package/dist/shared/templates.js +65 -0
- 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);
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Engram } from "../entities/engram.js";
|
|
2
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
3
|
+
export declare class EngramAlreadyExistsError extends Error {
|
|
4
|
+
constructor(id: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class InvalidEngramIdError extends Error {
|
|
7
|
+
constructor(id: string);
|
|
8
|
+
}
|
|
9
|
+
export interface CreateEngramParams {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
soul: string;
|
|
15
|
+
identity: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CreateEngramResult {
|
|
18
|
+
engram: Engram;
|
|
19
|
+
}
|
|
20
|
+
/** ID format: lowercase alphanumeric + hyphens, no leading/trailing hyphen */
|
|
21
|
+
export declare const ENGRAM_ID_PATTERN: RegExp;
|
|
22
|
+
export declare class CreateEngram {
|
|
23
|
+
private readonly repository;
|
|
24
|
+
constructor(repository: EngramRepository);
|
|
25
|
+
execute(params: CreateEngramParams): Promise<CreateEngramResult>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Errors
|
|
3
|
+
// ============================================================
|
|
4
|
+
export class EngramAlreadyExistsError extends Error {
|
|
5
|
+
constructor(id) {
|
|
6
|
+
super(`Engram "${id}" already exists. Delete it first or choose a different ID.`);
|
|
7
|
+
this.name = "EngramAlreadyExistsError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class InvalidEngramIdError extends Error {
|
|
11
|
+
constructor(id) {
|
|
12
|
+
super(`Invalid Engram ID "${id}". Use lowercase letters, numbers, and hyphens only (e.g. "my-agent").`);
|
|
13
|
+
this.name = "InvalidEngramIdError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Usecase
|
|
18
|
+
// ============================================================
|
|
19
|
+
/** ID format: lowercase alphanumeric + hyphens, no leading/trailing hyphen */
|
|
20
|
+
export const ENGRAM_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
21
|
+
export class CreateEngram {
|
|
22
|
+
repository;
|
|
23
|
+
constructor(repository) {
|
|
24
|
+
this.repository = repository;
|
|
25
|
+
}
|
|
26
|
+
async execute(params) {
|
|
27
|
+
// Validate ID format
|
|
28
|
+
if (!ENGRAM_ID_PATTERN.test(params.id)) {
|
|
29
|
+
throw new InvalidEngramIdError(params.id);
|
|
30
|
+
}
|
|
31
|
+
// Check for duplicates
|
|
32
|
+
const existing = await this.repository.get(params.id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
throw new EngramAlreadyExistsError(params.id);
|
|
35
|
+
}
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const engram = {
|
|
38
|
+
meta: {
|
|
39
|
+
id: params.id,
|
|
40
|
+
name: params.name,
|
|
41
|
+
description: params.description,
|
|
42
|
+
tags: params.tags,
|
|
43
|
+
createdAt: now,
|
|
44
|
+
updatedAt: now,
|
|
45
|
+
},
|
|
46
|
+
files: {
|
|
47
|
+
soul: params.soul,
|
|
48
|
+
identity: params.identity,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
await this.repository.save(engram);
|
|
52
|
+
return { engram };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Engram } from "../entities/engram.js";
|
|
2
|
+
import type { EngramRepository } from "../ports/engram-repository.js";
|
|
3
|
+
export declare class DeleteEngramNotFoundError extends Error {
|
|
4
|
+
constructor(id: string);
|
|
5
|
+
}
|
|
6
|
+
export interface DeleteEngramInfo {
|
|
7
|
+
engram: Engram;
|
|
8
|
+
hasMemory: boolean;
|
|
9
|
+
hasUser: boolean;
|
|
10
|
+
hasArchive: boolean;
|
|
11
|
+
memoryEntryCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class DeleteEngram {
|
|
14
|
+
private readonly repository;
|
|
15
|
+
constructor(repository: EngramRepository);
|
|
16
|
+
/**
|
|
17
|
+
* Inspect an Engram before deletion — returns info for confirmation UI.
|
|
18
|
+
*/
|
|
19
|
+
inspect(id: string): Promise<DeleteEngramInfo>;
|
|
20
|
+
/**
|
|
21
|
+
* Actually delete the Engram. Call inspect() first for confirmation.
|
|
22
|
+
*/
|
|
23
|
+
execute(id: string): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Errors
|
|
3
|
+
// ============================================================
|
|
4
|
+
export class DeleteEngramNotFoundError extends Error {
|
|
5
|
+
constructor(id) {
|
|
6
|
+
super(`Engram "${id}" not found.`);
|
|
7
|
+
this.name = "DeleteEngramNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Usecase
|
|
12
|
+
// ============================================================
|
|
13
|
+
export class DeleteEngram {
|
|
14
|
+
repository;
|
|
15
|
+
constructor(repository) {
|
|
16
|
+
this.repository = repository;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Inspect an Engram before deletion — returns info for confirmation UI.
|
|
20
|
+
*/
|
|
21
|
+
async inspect(id) {
|
|
22
|
+
const engram = await this.repository.get(id);
|
|
23
|
+
if (!engram) {
|
|
24
|
+
throw new DeleteEngramNotFoundError(id);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
engram,
|
|
28
|
+
hasMemory: !!engram.files.memory,
|
|
29
|
+
hasUser: !!engram.files.user,
|
|
30
|
+
hasArchive: false, // archive.md is not loaded via EngramFiles; CLI checks separately
|
|
31
|
+
memoryEntryCount: engram.files.memoryEntries
|
|
32
|
+
? Object.keys(engram.files.memoryEntries).length
|
|
33
|
+
: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Actually delete the Engram. Call inspect() first for confirmation.
|
|
38
|
+
*/
|
|
39
|
+
async execute(id) {
|
|
40
|
+
const existing = await this.repository.get(id);
|
|
41
|
+
if (!existing) {
|
|
42
|
+
throw new DeleteEngramNotFoundError(id);
|
|
43
|
+
}
|
|
44
|
+
await this.repository.delete(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { loadConfig, ensureInitialized } from "../../../shared/config.js";
|
|
4
|
+
import { LocalEngramRepository } from "../../../adapters/local/index.js";
|
|
5
|
+
import { DEFAULT_SOUL, DEFAULT_IDENTITY } from "../../../shared/templates.js";
|
|
6
|
+
import { CreateEngram, EngramAlreadyExistsError, InvalidEngramIdError, ENGRAM_ID_PATTERN, } from "../../../core/usecases/create-engram.js";
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Interactive prompt helpers
|
|
9
|
+
// ============================================================
|
|
10
|
+
async function ask(rl, question) {
|
|
11
|
+
const answer = await rl.question(question);
|
|
12
|
+
return answer.trim();
|
|
13
|
+
}
|
|
14
|
+
async function askRequired(rl, question, errorMsg) {
|
|
15
|
+
while (true) {
|
|
16
|
+
const answer = await ask(rl, question);
|
|
17
|
+
if (answer)
|
|
18
|
+
return answer;
|
|
19
|
+
console.log(errorMsg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ask for a valid, non-duplicate Engram ID. Retries on bad format or collision.
|
|
24
|
+
*/
|
|
25
|
+
async function askValidId(rl, repo) {
|
|
26
|
+
while (true) {
|
|
27
|
+
const id = await askRequired(rl, "Engram ID (e.g. my-agent): ", " ID is required. Use lowercase letters, numbers, and hyphens.");
|
|
28
|
+
if (!ENGRAM_ID_PATTERN.test(id)) {
|
|
29
|
+
console.log(" Invalid ID. Use lowercase letters, numbers, and hyphens (e.g. my-agent).");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const existing = await repo.get(id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
console.log(` Engram "${id}" already exists. Choose a different ID.`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ============================================================
|
|
41
|
+
// Command
|
|
42
|
+
// ============================================================
|
|
43
|
+
export function registerCreateCommand(program) {
|
|
44
|
+
program
|
|
45
|
+
.command("create")
|
|
46
|
+
.description("Create a new Engram interactively")
|
|
47
|
+
.option("-i, --id <id>", "Engram ID (lowercase alphanumeric + hyphens)")
|
|
48
|
+
.option("-n, --name <name>", "Display name")
|
|
49
|
+
.option("-d, --description <desc>", "Description")
|
|
50
|
+
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
await ensureInitialized();
|
|
53
|
+
const config = await loadConfig();
|
|
54
|
+
const repo = new LocalEngramRepository(config.engramsPath);
|
|
55
|
+
const usecase = new CreateEngram(repo);
|
|
56
|
+
const isTTY = input.isTTY && output.isTTY;
|
|
57
|
+
// If non-interactive, require at least id and name
|
|
58
|
+
if (!isTTY) {
|
|
59
|
+
if (!opts.id || !opts.name) {
|
|
60
|
+
console.error("Error: --id and --name are required in non-interactive mode.");
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let id = opts.id;
|
|
66
|
+
let name = opts.name;
|
|
67
|
+
let description = opts.description;
|
|
68
|
+
let tags;
|
|
69
|
+
if (opts.tags) {
|
|
70
|
+
tags = opts.tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
// Early validation for pre-supplied ID (before interactive flow)
|
|
73
|
+
if (id) {
|
|
74
|
+
if (!ENGRAM_ID_PATTERN.test(id)) {
|
|
75
|
+
console.error(`Error: Invalid Engram ID "${id}". Use lowercase letters, numbers, and hyphens only (e.g. "my-agent").`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const existing = await repo.get(id);
|
|
80
|
+
if (existing) {
|
|
81
|
+
console.error(`Error: Engram "${id}" already exists. Delete it first or choose a different ID.`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Interactive flow for missing fields
|
|
87
|
+
if (isTTY && (!id || !name)) {
|
|
88
|
+
const rl = createInterface({ input, output });
|
|
89
|
+
try {
|
|
90
|
+
if (!id) {
|
|
91
|
+
id = await askValidId(rl, repo);
|
|
92
|
+
}
|
|
93
|
+
if (!name) {
|
|
94
|
+
name = await askRequired(rl, "Name: ", " Name is required.");
|
|
95
|
+
}
|
|
96
|
+
if (description === undefined) {
|
|
97
|
+
description = await ask(rl, "Description (optional): ") || undefined;
|
|
98
|
+
}
|
|
99
|
+
if (tags === undefined) {
|
|
100
|
+
const tagsInput = await ask(rl, "Tags (comma-separated, optional): ");
|
|
101
|
+
if (tagsInput) {
|
|
102
|
+
tags = tagsInput.split(",").map((t) => t.trim()).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rl.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Execute
|
|
111
|
+
try {
|
|
112
|
+
const result = await usecase.execute({
|
|
113
|
+
id: id,
|
|
114
|
+
name: name,
|
|
115
|
+
description,
|
|
116
|
+
tags,
|
|
117
|
+
soul: DEFAULT_SOUL,
|
|
118
|
+
identity: DEFAULT_IDENTITY,
|
|
119
|
+
});
|
|
120
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
121
|
+
const engramDir = `${config.engramsPath}/${result.engram.meta.id}`;
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(green(`Created Engram "${result.engram.meta.name}" (${result.engram.meta.id})`));
|
|
124
|
+
console.log(` → ${engramDir}/`);
|
|
125
|
+
console.log();
|
|
126
|
+
console.log("Files:");
|
|
127
|
+
console.log(" engram.json — metadata (name, description, tags)");
|
|
128
|
+
console.log(" manifest.json — system metadata (id, timestamps)");
|
|
129
|
+
console.log(" SOUL.md — core principles and behavior");
|
|
130
|
+
console.log(" IDENTITY.md — persona identity (fill in during first conversation)");
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(`Customize SOUL.md and IDENTITY.md, then run: relic claude ${result.engram.meta.id}`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof InvalidEngramIdError || err instanceof EngramAlreadyExistsError) {
|
|
136
|
+
console.error(`Error: ${err.message}`);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|