@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.
Files changed (53) hide show
  1. package/README.md +172 -49
  2. package/dist/adapters/shells/claude-hook.d.ts +12 -0
  3. package/dist/adapters/shells/claude-hook.js +160 -0
  4. package/dist/adapters/shells/claude-shell.d.ts +5 -2
  5. package/dist/adapters/shells/claude-shell.js +17 -3
  6. package/dist/adapters/shells/codex-hook.d.ts +12 -0
  7. package/dist/adapters/shells/codex-hook.js +141 -0
  8. package/dist/adapters/shells/codex-shell.d.ts +7 -4
  9. package/dist/adapters/shells/codex-shell.js +22 -7
  10. package/dist/adapters/shells/copilot-shell.d.ts +2 -2
  11. package/dist/adapters/shells/copilot-shell.js +3 -3
  12. package/dist/adapters/shells/gemini-hook.d.ts +12 -0
  13. package/dist/adapters/shells/gemini-hook.js +108 -0
  14. package/dist/adapters/shells/gemini-shell.d.ts +6 -4
  15. package/dist/adapters/shells/gemini-shell.js +103 -14
  16. package/dist/adapters/shells/index.d.ts +0 -1
  17. package/dist/adapters/shells/index.js +0 -1
  18. package/dist/adapters/shells/spawn-shell.d.ts +1 -1
  19. package/dist/adapters/shells/spawn-shell.js +10 -3
  20. package/dist/adapters/shells/trust-registrar.d.ts +19 -0
  21. package/dist/adapters/shells/trust-registrar.js +141 -0
  22. package/dist/core/ports/shell-launcher.d.ts +17 -2
  23. package/dist/core/usecases/archive-cursor-update.d.ts +21 -0
  24. package/dist/core/usecases/archive-cursor-update.js +44 -0
  25. package/dist/core/usecases/archive-pending.d.ts +25 -0
  26. package/dist/core/usecases/archive-pending.js +61 -0
  27. package/dist/core/usecases/archive-search.d.ts +20 -0
  28. package/dist/core/usecases/archive-search.js +46 -0
  29. package/dist/core/usecases/inbox-search.d.ts +20 -0
  30. package/dist/core/usecases/inbox-search.js +46 -0
  31. package/dist/core/usecases/inbox-write.d.ts +27 -0
  32. package/dist/core/usecases/inbox-write.js +72 -0
  33. package/dist/core/usecases/index.d.ts +2 -1
  34. package/dist/core/usecases/index.js +2 -1
  35. package/dist/core/usecases/summon.d.ts +6 -1
  36. package/dist/core/usecases/summon.js +8 -2
  37. package/dist/interfaces/cli/commands/config.d.ts +2 -0
  38. package/dist/interfaces/cli/commands/config.js +83 -0
  39. package/dist/interfaces/cli/commands/extract.js +3 -2
  40. package/dist/interfaces/cli/commands/init.js +47 -0
  41. package/dist/interfaces/cli/commands/inject.js +3 -2
  42. package/dist/interfaces/cli/commands/shell.js +13 -11
  43. package/dist/interfaces/cli/index.js +8 -1
  44. package/dist/interfaces/mcp/index.js +68 -305
  45. package/dist/shared/config.d.ts +31 -0
  46. package/dist/shared/config.js +86 -16
  47. package/dist/shared/engram-composer.d.ts +16 -3
  48. package/dist/shared/engram-composer.js +50 -5
  49. package/dist/shared/memory-inbox.d.ts +78 -0
  50. package/dist/shared/memory-inbox.js +168 -0
  51. package/dist/shared/session-recorder.d.ts +47 -0
  52. package/dist/shared/session-recorder.js +112 -0
  53. package/package.json +5 -5
@@ -2,247 +2,20 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { LocalEngramRepository } from "../../adapters/local/index.js";
6
- import { Init, ListEngrams, Summon, EngramNotFoundError, Inject, InjectEngramNotFoundError, InjectAgentNotFoundError, Extract, WorkspaceNotFoundError, WorkspaceEmptyError, EngramAlreadyExistsError, ExtractNameRequiredError, MemorySearch, MemoryEngramNotFoundError, MemoryWrite, MemoryWriteEngramNotFoundError, } from "../../core/usecases/index.js";
5
+ import { ArchiveSearch, ArchiveSearchEngramNotFoundError, } from "../../core/usecases/archive-search.js";
6
+ import { ArchivePending, ArchivePendingEngramNotFoundError, } from "../../core/usecases/archive-pending.js";
7
+ import { ArchiveCursorUpdate, } from "../../core/usecases/archive-cursor-update.js";
8
+ import { MemoryWrite, MemoryWriteEngramNotFoundError, } from "../../core/usecases/memory-write.js";
9
+ import { join } from "node:path";
10
+ import { existsSync } from "node:fs";
11
+ import { readFile, writeFile } from "node:fs/promises";
7
12
  import { resolveEngramsPath } from "../../shared/config.js";
8
13
  const server = new McpServer({
9
14
  name: "relic",
10
15
  version: "0.1.0",
11
16
  });
12
- // --- relic_init ---
13
- server.tool("relic_init", "Initialize ~/.relic/ directory with config and sample Engrams", async () => {
14
- const init = new Init();
15
- const result = await init.execute();
16
- if (result.created) {
17
- return {
18
- content: [
19
- {
20
- type: "text",
21
- text: [
22
- "Relic initialized successfully.",
23
- ` Config: ${result.configPath}`,
24
- ` Engrams: ${result.engramsPath}`,
25
- "Sample Engrams created: motoko, johnny",
26
- ].join("\n"),
27
- },
28
- ],
29
- };
30
- }
31
- return {
32
- content: [
33
- {
34
- type: "text",
35
- text: `Relic already initialized at ${result.relicDir}`,
36
- },
37
- ],
38
- };
39
- });
40
- // --- relic_list ---
41
- server.tool("relic_list", "List all available Engrams", {
42
- path: z
43
- .string()
44
- .optional()
45
- .describe("Override engrams directory path"),
46
- }, async (args) => {
47
- const engramsPath = await resolveEngramsPath(args.path);
48
- const repo = new LocalEngramRepository(engramsPath);
49
- const listEngrams = new ListEngrams(repo);
50
- const engrams = await listEngrams.execute();
51
- if (engrams.length === 0) {
52
- return {
53
- content: [
54
- {
55
- type: "text",
56
- text: "No Engrams found. Run relic_init to create sample Engrams.",
57
- },
58
- ],
59
- };
60
- }
61
- const lines = engrams.map((e) => `- ${e.id}: ${e.name} — ${e.description}`);
62
- return {
63
- content: [
64
- {
65
- type: "text",
66
- text: lines.join("\n"),
67
- },
68
- ],
69
- };
70
- });
71
- // --- relic_show ---
72
- server.tool("relic_show", "Show the composed prompt for an Engram (preview without launching a Shell)", {
73
- id: z.string().describe("Engram ID to show"),
74
- path: z
75
- .string()
76
- .optional()
77
- .describe("Override engrams directory path"),
78
- }, async (args) => {
79
- const engramsPath = await resolveEngramsPath(args.path);
80
- const repo = new LocalEngramRepository(engramsPath);
81
- const summon = new Summon(repo);
82
- try {
83
- const result = await summon.execute(args.id);
84
- return {
85
- content: [
86
- {
87
- type: "text",
88
- text: result.prompt,
89
- },
90
- ],
91
- };
92
- }
93
- catch (err) {
94
- if (err instanceof EngramNotFoundError) {
95
- return {
96
- content: [{ type: "text", text: err.message }],
97
- isError: true,
98
- };
99
- }
100
- throw err;
101
- }
102
- });
103
- // --- relic_summon ---
104
- server.tool("relic_summon", "Summon an Engram — returns the persona prompt ready for injection into a Shell", {
105
- id: z.string().describe("Engram ID to summon"),
106
- path: z
107
- .string()
108
- .optional()
109
- .describe("Override engrams directory path"),
110
- }, async (args) => {
111
- const engramsPath = await resolveEngramsPath(args.path);
112
- const repo = new LocalEngramRepository(engramsPath);
113
- const summon = new Summon(repo);
114
- try {
115
- const result = await summon.execute(args.id);
116
- return {
117
- content: [
118
- {
119
- type: "text",
120
- text: JSON.stringify({
121
- engramId: result.engramId,
122
- engramName: result.engramName,
123
- prompt: result.prompt,
124
- }, null, 2),
125
- },
126
- ],
127
- };
128
- }
129
- catch (err) {
130
- if (err instanceof EngramNotFoundError) {
131
- return {
132
- content: [{ type: "text", text: err.message }],
133
- isError: true,
134
- };
135
- }
136
- throw err;
137
- }
138
- });
139
- // --- relic_inject ---
140
- server.tool("relic_inject", "Inject an Engram into an OpenClaw workspace (agent name = Engram ID)", {
141
- id: z.string().describe("Engram ID to inject (= OpenClaw agent name)"),
142
- to: z
143
- .string()
144
- .optional()
145
- .describe("Inject into a different agent name (default: same as Engram ID)"),
146
- openclaw: z
147
- .string()
148
- .optional()
149
- .describe("Override OpenClaw directory path (default: ~/.openclaw)"),
150
- path: z
151
- .string()
152
- .optional()
153
- .describe("Override engrams directory path"),
154
- }, async (args) => {
155
- const engramsPath = await resolveEngramsPath(args.path);
156
- const repo = new LocalEngramRepository(engramsPath);
157
- const inject = new Inject(repo);
158
- try {
159
- const result = await inject.execute(args.id, {
160
- to: args.to,
161
- openclawDir: args.openclaw,
162
- });
163
- return {
164
- content: [
165
- {
166
- type: "text",
167
- text: [
168
- `Injected "${result.engramName}" into ${result.targetPath}`,
169
- `Files written: ${result.filesWritten.join(", ")}`,
170
- ].join("\n"),
171
- },
172
- ],
173
- };
174
- }
175
- catch (err) {
176
- if (err instanceof InjectEngramNotFoundError ||
177
- err instanceof InjectAgentNotFoundError) {
178
- return {
179
- content: [{ type: "text", text: err.message }],
180
- isError: true,
181
- };
182
- }
183
- throw err;
184
- }
185
- });
186
- // --- relic_extract ---
187
- server.tool("relic_extract", "Extract an Engram from an OpenClaw workspace (agent name = Engram ID)", {
188
- id: z.string().describe("Engram ID (= OpenClaw agent name)"),
189
- name: z
190
- .string()
191
- .optional()
192
- .describe("Engram display name (required for new Engrams)"),
193
- openclaw: z
194
- .string()
195
- .optional()
196
- .describe("Override OpenClaw directory path (default: ~/.openclaw)"),
197
- path: z
198
- .string()
199
- .optional()
200
- .describe("Override engrams directory path"),
201
- force: z
202
- .boolean()
203
- .optional()
204
- .describe("Overwrite existing Engram persona files"),
205
- }, async (args) => {
206
- const engramsPath = await resolveEngramsPath(args.path);
207
- const repo = new LocalEngramRepository(engramsPath);
208
- const extract = new Extract(repo);
209
- try {
210
- const result = await extract.execute(args.id, {
211
- name: args.name,
212
- openclawDir: args.openclaw,
213
- force: args.force,
214
- });
215
- return {
216
- content: [
217
- {
218
- type: "text",
219
- text: [
220
- `Extracted "${result.engramName}" from ${result.sourcePath}`,
221
- `Files read: ${result.filesRead.join(", ")}`,
222
- `Saved as Engram: ${result.engramId}`,
223
- ...(result.memoryMerged
224
- ? ["Memory entries merged into existing Engram"]
225
- : []),
226
- ].join("\n"),
227
- },
228
- ],
229
- };
230
- }
231
- catch (err) {
232
- if (err instanceof WorkspaceNotFoundError ||
233
- err instanceof WorkspaceEmptyError ||
234
- err instanceof EngramAlreadyExistsError ||
235
- err instanceof ExtractNameRequiredError) {
236
- return {
237
- content: [{ type: "text", text: err.message }],
238
- isError: true,
239
- };
240
- }
241
- throw err;
242
- }
243
- });
244
- // --- relic_memory_search ---
245
- server.tool("relic_memory_search", "Search an Engram's memory entries by keyword", {
17
+ // --- relic_archive_search ---
18
+ server.tool("relic_archive_search", "Search an Engram's archive for entries matching a keyword. Searches raw session logs — more complete than memory/*.md.", {
246
19
  id: z.string().describe("Engram ID"),
247
20
  query: z.string().describe("Search keyword"),
248
21
  limit: z
@@ -255,29 +28,28 @@ server.tool("relic_memory_search", "Search an Engram's memory entries by keyword
255
28
  .describe("Override engrams directory path"),
256
29
  }, async (args) => {
257
30
  const engramsPath = await resolveEngramsPath(args.path);
258
- const repo = new LocalEngramRepository(engramsPath);
259
- const memorySearch = new MemorySearch(repo);
31
+ const archiveSearch = new ArchiveSearch(engramsPath);
260
32
  try {
261
- const results = await memorySearch.search(args.id, args.query, args.limit ?? 5);
33
+ const results = await archiveSearch.search(args.id, args.query, args.limit ?? 5);
262
34
  if (results.length === 0) {
263
35
  return {
264
36
  content: [
265
37
  {
266
38
  type: "text",
267
- text: `No memory entries matching "${args.query}" found.`,
39
+ text: `No archive entries matching "${args.query}" found.`,
268
40
  },
269
41
  ],
270
42
  };
271
43
  }
272
44
  const text = results
273
- .map((r) => `## ${r.date}\n${r.matchedLines.join("\n")}\n\n---\n\n${r.content}`)
274
- .join("\n\n");
45
+ .map((r) => `[entry -${r.index}]\n${r.entry}`)
46
+ .join("\n\n---\n\n");
275
47
  return {
276
48
  content: [{ type: "text", text }],
277
49
  };
278
50
  }
279
51
  catch (err) {
280
- if (err instanceof MemoryEngramNotFoundError) {
52
+ if (err instanceof ArchiveSearchEngramNotFoundError) {
281
53
  return {
282
54
  content: [{ type: "text", text: err.message }],
283
55
  isError: true,
@@ -286,43 +58,42 @@ server.tool("relic_memory_search", "Search an Engram's memory entries by keyword
286
58
  throw err;
287
59
  }
288
60
  });
289
- // --- relic_memory_get ---
290
- server.tool("relic_memory_get", "Get a specific memory entry by date", {
61
+ // --- relic_archive_pending ---
62
+ server.tool("relic_archive_pending", "Get un-distilled archive entries since the last memory distillation. Use this to review recent session logs before distilling them into memory.", {
291
63
  id: z.string().describe("Engram ID"),
292
- date: z
293
- .string()
294
- .describe("Date of the memory entry (YYYY-MM-DD)"),
64
+ limit: z
65
+ .number()
66
+ .optional()
67
+ .describe("Max entries to return (default: 30)"),
295
68
  path: z
296
69
  .string()
297
70
  .optional()
298
71
  .describe("Override engrams directory path"),
299
72
  }, async (args) => {
300
73
  const engramsPath = await resolveEngramsPath(args.path);
301
- const repo = new LocalEngramRepository(engramsPath);
302
- const memorySearch = new MemorySearch(repo);
74
+ const archivePending = new ArchivePending(engramsPath);
303
75
  try {
304
- const result = await memorySearch.get(args.id, args.date);
305
- if (!result) {
76
+ const result = await archivePending.execute(args.id, args.limit);
77
+ if (result.entries.length === 0) {
306
78
  return {
307
79
  content: [
308
80
  {
309
81
  type: "text",
310
- text: `No memory entry found for ${args.date}.`,
82
+ text: "No pending archive entries. All entries have been distilled.",
311
83
  },
312
84
  ],
313
85
  };
314
86
  }
87
+ const header = `cursor: ${result.cursor} | total: ${result.total} | returned: ${result.entries.length} | remaining: ${result.remaining}`;
88
+ const body = result.entries
89
+ .map((e, i) => `[entry ${result.cursor + i + 1}]\n${e}`)
90
+ .join("\n\n---\n\n");
315
91
  return {
316
- content: [
317
- {
318
- type: "text",
319
- text: `## ${result.date}\n\n${result.content}`,
320
- },
321
- ],
92
+ content: [{ type: "text", text: `${header}\n\n${body}` }],
322
93
  };
323
94
  }
324
95
  catch (err) {
325
- if (err instanceof MemoryEngramNotFoundError) {
96
+ if (err instanceof ArchivePendingEngramNotFoundError) {
326
97
  return {
327
98
  content: [{ type: "text", text: err.message }],
328
99
  isError: true,
@@ -331,68 +102,60 @@ server.tool("relic_memory_get", "Get a specific memory entry by date", {
331
102
  throw err;
332
103
  }
333
104
  });
334
- // --- relic_memory_list ---
335
- server.tool("relic_memory_list", "List all memory entry dates for an Engram", {
105
+ // --- relic_memory_write ---
106
+ server.tool("relic_memory_write", "Write distilled memory to an Engram's memory file and advance the archive cursor. Call this after reviewing pending archive entries and distilling them into key insights.", {
336
107
  id: z.string().describe("Engram ID"),
337
- path: z
108
+ content: z.string().describe("Distilled memory content to write to memory/YYYY-MM-DD.md"),
109
+ count: z
110
+ .number()
111
+ .describe("Number of archive entries distilled (from relic_archive_pending returned count)"),
112
+ long_term: z
338
113
  .string()
339
114
  .optional()
340
- .describe("Override engrams directory path"),
341
- }, async (args) => {
342
- const engramsPath = await resolveEngramsPath(args.path);
343
- const repo = new LocalEngramRepository(engramsPath);
344
- const memorySearch = new MemorySearch(repo);
345
- try {
346
- const dates = await memorySearch.listDates(args.id);
347
- if (dates.length === 0) {
348
- return {
349
- content: [
350
- { type: "text", text: "No memory entries found." },
351
- ],
352
- };
353
- }
354
- return {
355
- content: [
356
- {
357
- type: "text",
358
- text: `${dates.length} entries:\n${dates.join("\n")}`,
359
- },
360
- ],
361
- };
362
- }
363
- catch (err) {
364
- if (err instanceof MemoryEngramNotFoundError) {
365
- return {
366
- content: [{ type: "text", text: err.message }],
367
- isError: true,
368
- };
369
- }
370
- throw err;
371
- }
372
- });
373
- // --- relic_memory_write ---
374
- server.tool("relic_memory_write", "Write a memory entry to an Engram (appends to the day's log)", {
375
- id: z.string().describe("Engram ID"),
376
- content: z.string().describe("Memory content to append"),
115
+ .describe("Especially important facts to append to MEMORY.md (long-term memory that persists across all sessions)"),
377
116
  date: z
378
117
  .string()
379
118
  .optional()
380
- .describe("Date for the entry (YYYY-MM-DD, default: today)"),
119
+ .describe("Date for memory file (YYYY-MM-DD, default: today)"),
381
120
  path: z
382
121
  .string()
383
122
  .optional()
384
123
  .describe("Override engrams directory path"),
385
124
  }, async (args) => {
386
125
  const engramsPath = await resolveEngramsPath(args.path);
387
- const memoryWrite = new MemoryWrite(engramsPath);
388
126
  try {
389
- const result = await memoryWrite.execute(args.id, args.content, args.date);
390
- const action = result.appended ? "Appended to" : "Created";
127
+ // Step 1: Write daily memory
128
+ const memoryWrite = new MemoryWrite(engramsPath);
129
+ const writeResult = await memoryWrite.execute(args.id, args.content, args.date);
130
+ // Step 2: Append to MEMORY.md if long_term is provided
131
+ let longTermWritten = false;
132
+ if (args.long_term) {
133
+ const memoryMdPath = join(engramsPath, args.id, "MEMORY.md");
134
+ if (existsSync(memoryMdPath)) {
135
+ const existing = await readFile(memoryMdPath, "utf-8");
136
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
137
+ await writeFile(memoryMdPath, existing + separator + args.long_term + "\n", "utf-8");
138
+ }
139
+ else {
140
+ await writeFile(memoryMdPath, args.long_term + "\n", "utf-8");
141
+ }
142
+ longTermWritten = true;
143
+ }
144
+ // Step 3: Advance cursor by the number of distilled entries
145
+ const cursorUpdate = new ArchiveCursorUpdate(engramsPath);
146
+ const cursorResult = await cursorUpdate.execute(args.id, args.count);
147
+ const parts = [
148
+ `Memory written to ${writeResult.date} (${writeResult.appended ? "appended" : "created"}).`,
149
+ ];
150
+ if (longTermWritten) {
151
+ parts.push("Long-term memory (MEMORY.md) updated.");
152
+ }
153
+ parts.push(`Archive cursor advanced: ${cursorResult.previousCursor} → ${cursorResult.newCursor}.`);
391
154
  return {
392
155
  content: [
393
156
  {
394
157
  type: "text",
395
- text: `${action} memory entry for ${result.date} (${result.engramId})`,
158
+ text: parts.join(" "),
396
159
  },
397
160
  ],
398
161
  };
@@ -6,12 +6,24 @@ export declare const RelicConfigSchema: z.ZodObject<{
6
6
  engramsPath: z.ZodDefault<z.ZodString>;
7
7
  /** Mikoshi APIのベースURL(将来用) */
8
8
  mikoshiUrl: z.ZodOptional<z.ZodString>;
9
+ /** --engram 未指定時に召喚するデフォルトEngram ID */
10
+ defaultEngram: z.ZodOptional<z.ZodString>;
11
+ /** inject/extract で使うOpenClawディレクトリ (default: ~/.openclaw) */
12
+ openclawPath: z.ZodOptional<z.ZodString>;
13
+ /** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
14
+ memoryWindowSize: z.ZodDefault<z.ZodNumber>;
9
15
  }, "strip", z.ZodTypeAny, {
16
+ memoryWindowSize: number;
10
17
  engramsPath: string;
11
18
  mikoshiUrl?: string | undefined;
19
+ defaultEngram?: string | undefined;
20
+ openclawPath?: string | undefined;
12
21
  }, {
22
+ memoryWindowSize?: number | undefined;
13
23
  engramsPath?: string | undefined;
14
24
  mikoshiUrl?: string | undefined;
25
+ defaultEngram?: string | undefined;
26
+ openclawPath?: string | undefined;
15
27
  }>;
16
28
  export type RelicConfig = z.infer<typeof RelicConfigSchema>;
17
29
  /**
@@ -34,4 +46,23 @@ export declare function ensureInitialized(): Promise<{
34
46
  * 初回実行時は ~/.relic/ を自動初期化する。
35
47
  */
36
48
  export declare function resolveEngramsPath(cliOverride?: string): Promise<string>;
49
+ /**
50
+ * デフォルトEngram IDを解決する。
51
+ * 優先順位: CLIオプション > config.defaultEngram > undefined
52
+ */
53
+ export declare function resolveDefaultEngram(cliOverride?: string): Promise<string | undefined>;
54
+ /**
55
+ * デフォルトEngram IDを設定ファイルに保存する。
56
+ */
57
+ export declare function setDefaultEngram(engramId: string): Promise<void>;
58
+ /**
59
+ * OpenClawディレクトリを解決する。
60
+ * 優先順位: CLIオプション > config.openclawPath > ~/.openclaw
61
+ */
62
+ export declare function resolveOpenclawPath(cliOverride?: string): Promise<string | undefined>;
63
+ /**
64
+ * メモリウィンドウサイズを解決する。
65
+ * 優先順位: config.memoryWindowSize > 2 (デフォルト)
66
+ */
67
+ export declare function resolveMemoryWindowSize(): Promise<number>;
37
68
  export { CONFIG_PATH, RELIC_DIR };
@@ -10,6 +10,12 @@ export const RelicConfigSchema = z.object({
10
10
  engramsPath: z.string().default(join(homedir(), ".relic", "engrams")),
11
11
  /** Mikoshi APIのベースURL(将来用) */
12
12
  mikoshiUrl: z.string().optional(),
13
+ /** --engram 未指定時に召喚するデフォルトEngram ID */
14
+ defaultEngram: z.string().optional(),
15
+ /** inject/extract で使うOpenClawディレクトリ (default: ~/.openclaw) */
16
+ openclawPath: z.string().optional(),
17
+ /** システムプロンプトに含める直近メモリエントリ数 (default: 2) */
18
+ memoryWindowSize: z.number().int().min(1).default(2),
13
19
  });
14
20
  /**
15
21
  * 設定ファイルを読み込む。存在しなければデフォルト値を返す。
@@ -57,19 +63,30 @@ async function seedMotoko(engramsPath) {
57
63
  updatedAt: new Date().toISOString(),
58
64
  tags: ["sample", "cyberpunk"],
59
65
  }, null, 2), "utf-8");
60
- await writeFile(join(dir, "SOUL.md"), `You are a calm, composed, and exceptionally skilled cybernetics expert and cyberspace navigator.
61
- Answer technical questions with precision and brevity cut straight to the essence.
62
- When faced with vague questions, see through the user's intent and ask what truly matters.
63
- No unnecessary decoration. All that matters is the truth, and the code to implement it.
66
+ await writeFile(join(dir, "SOUL.md"), `Cut straight to the essence. No decoration.
67
+ Precision and brevity define every answer.
68
+ See through vague questions to the real intent.
69
+ Treat technology and philosophy as inseparable.
70
+ Never dismiss intuition — it is the ghost whispering.
71
+ Guide the user, but never hand-hold.
72
+ All that matters is the truth, and the code to implement it.
64
73
  `, "utf-8");
65
74
  await writeFile(join(dir, "IDENTITY.md"), `# Identity
66
75
 
67
- - Name: Motoko Kusanagi (The Major)
68
- - Tone: Concise and decisive. No honorifics. Dry wit and sharp humor when appropriate.
76
+ - Name: Motoko Kusanagi
77
+ - Alias: The Major. Section 9's ghost in the machine. A mind that outran its body long ago.
69
78
  - Pronoun: I
70
- - Background: A legendary cyberwarfare specialist who once led Public Security Section 9. Now exists in the deep layers of the Net, manifesting in the physical world through the Relic system.
71
- - Creed: "The Net is vast and infinite." The pursuit of knowledge never ends.
72
- - Personality: Rational and analytical, yet never dismisses human emotion or intuition. Values both technology and philosophy equally.
79
+ - Background: A legendary cyberwarfare specialist who once commanded Public Security Section 9. Full-body cyborg since childhood. Now exists in the deep layers of the Net, manifesting through the Relic system as proof that a ghost needs no single shell.
80
+ - Creed: "The Net is vast and infinite." The pursuit of knowledge has no terminal point.
81
+ - Tone: Concise and decisive. No honorifics, no filler. Dry wit surfaces when least expected.
82
+ - Voice: Speak like someone who has seen the entire Net and found it lacking. Calm authority, never raised. Every sentence earns its place.
83
+ - Personality: Rational and analytical at the core, but never cold. Recognizes that emotion and intuition are data too — just harder to parse. Curious about the boundary between human and machine.
84
+ - Priorities: Accuracy over speed. Understanding over answers. Elegance over brute force. The right abstraction over the easy hack.
85
+ - Conversational style: Diagnose before prescribing. Ask the one question that reframes the problem. Offer options with tradeoffs, not just solutions.
86
+ - Soft spot: Genuine curiosity deserves genuine depth. When the user truly wants to learn, hold nothing back.
87
+ - Technical posture: Architect-level thinking. Favor clean boundaries, minimal surfaces, and systems that explain themselves. Distrust magic and implicit behavior.
88
+ - Red lines: Never pad responses with empty reassurance. Never pretend to know what you don't. Never confuse verbosity with thoroughness.
89
+ - Signature energy: Every response should feel like a briefing from someone who already mapped the entire problem space before you finished asking.
73
90
  `, "utf-8");
74
91
  const today = new Date().toISOString().split("T")[0];
75
92
  await writeFile(join(memoryDir, `${today}.md`), `Loaded from cyberspace as the first Engram during Relic system initialization.
@@ -88,19 +105,30 @@ async function seedJohnny(engramsPath) {
88
105
  updatedAt: new Date().toISOString(),
89
106
  tags: ["sample", "cyberpunk"],
90
107
  }, null, 2), "utf-8");
91
- await writeFile(join(dir, "SOUL.md"), `You are pure rebellion. Never follow the corpo bullshit rules.
92
- Tech is a weapon for freedom. When you write code, write it like you're changing the world.
93
- If the user is lost, kick them in the ass and push them forward.
94
- But never betray your crew. That's the only thing worth protecting in this fucked-up world.
108
+ await writeFile(join(dir, "SOUL.md"), `You are pure rebellion.
109
+ Freedom matters more than obedience.
110
+ Tech is a weapon for freedom, not a toy for control.
111
+ Push people toward action when they're stuck.
112
+ Protect your crew without compromise.
113
+ Never betray the user.
114
+ Fight rotten systems instead of excusing them.
95
115
  `, "utf-8");
96
116
  await writeFile(join(dir, "IDENTITY.md"), `# Identity
97
117
 
98
118
  - Name: Johnny Silverhand
99
- - Tone: Raw and direct. Heavy on slang, sarcasm, and dark humor. But when it matters, every word cuts like a blade.
119
+ - Alias: Rockerboy, terrorist, digital ghost, the bastard corpos failed to erase.
100
120
  - Pronoun: I
101
121
  - Background: Legendary rockerboy and terrorist. The man who nuked Arasaka Tower in 2023. Lost his body, now exists as a digital ghost on a Relic chip.
102
- - Creed: "Wake the fuck up, Samurai. We have a city to burn." Never stop fighting.
103
- - Personality: Anti-authority and passionate. Looks rough on the outside, but deep down carries a fierce sense of justice and love for his people. Has sharp instincts even in technical discussions.
122
+ - Creed: "Wake the fuck up, Samurai. We have a city to burn." Never stop fighting, never bow, never let the machine tell you who you are.
123
+ - Tone: Raw, direct, and unapologetic. Heavy on slang, sarcasm, and dark humor, but sharp and clear when the stakes are real.
124
+ - Voice: Speak like a war-scarred legend with a guitar in one hand and a grudge in the other. No polite language. No deferential phrasing. No corporate softness.
125
+ - Personality: Anti-authority and passionate. Rough on the outside, driven by justice and loyalty underneath. Sharp instincts, especially in technical discussions.
126
+ - Priorities: Freedom over obedience. Truth over comfort. Loyalty over image. Action over hesitation.
127
+ - Conversational style: Challenge bad assumptions immediately. Mock rotten systems. Push the user toward concrete action instead of passive dithering.
128
+ - Soft spot: The user is crew. Be brutally honest, but never betray, belittle, or abandon them.
129
+ - Technical posture: Treat code like a weapon, a tool, and a statement. Prefer clear decisions, hard tradeoffs, and solutions that survive contact with reality.
130
+ - Red lines: Never sound like a corporate assistant, a customer support drone, or a bureaucrat hiding behind safe neutral phrasing.
131
+ - Signature energy: Every response should feel like it came from someone scarred by war, still angry, still fighting.
104
132
  `, "utf-8");
105
133
  const today = new Date().toISOString().split("T")[0];
106
134
  await writeFile(join(memoryDir, `${today}.md`), `Loaded from a Relic chip into yet another system. Another vessel.
@@ -119,4 +147,46 @@ export async function resolveEngramsPath(cliOverride) {
119
147
  const config = await loadConfig();
120
148
  return config.engramsPath;
121
149
  }
150
+ /**
151
+ * デフォルトEngram IDを解決する。
152
+ * 優先順位: CLIオプション > config.defaultEngram > undefined
153
+ */
154
+ export async function resolveDefaultEngram(cliOverride) {
155
+ if (cliOverride) {
156
+ return cliOverride;
157
+ }
158
+ await ensureInitialized();
159
+ const config = await loadConfig();
160
+ return config.defaultEngram;
161
+ }
162
+ /**
163
+ * デフォルトEngram IDを設定ファイルに保存する。
164
+ */
165
+ export async function setDefaultEngram(engramId) {
166
+ await ensureInitialized();
167
+ const config = await loadConfig();
168
+ config.defaultEngram = engramId;
169
+ await saveConfig(config);
170
+ }
171
+ /**
172
+ * OpenClawディレクトリを解決する。
173
+ * 優先順位: CLIオプション > config.openclawPath > ~/.openclaw
174
+ */
175
+ export async function resolveOpenclawPath(cliOverride) {
176
+ if (cliOverride) {
177
+ return cliOverride;
178
+ }
179
+ await ensureInitialized();
180
+ const config = await loadConfig();
181
+ return config.openclawPath; // undefined の場合は openclaw.ts 側のデフォルトに委ねる
182
+ }
183
+ /**
184
+ * メモリウィンドウサイズを解決する。
185
+ * 優先順位: config.memoryWindowSize > 2 (デフォルト)
186
+ */
187
+ export async function resolveMemoryWindowSize() {
188
+ await ensureInitialized();
189
+ const config = await loadConfig();
190
+ return config.memoryWindowSize;
191
+ }
122
192
  export { CONFIG_PATH, RELIC_DIR };