skyloom 1.13.6 → 1.13.8

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 (193) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent.d.ts +20 -0
  49. package/dist/core/agent.d.ts.map +1 -1
  50. package/dist/core/agent.js +199 -16
  51. package/dist/core/agent.js.map +1 -1
  52. package/dist/core/factory.d.ts.map +1 -1
  53. package/dist/core/factory.js +34 -2
  54. package/dist/core/factory.js.map +1 -1
  55. package/dist/core/file_checkpoint.d.ts +57 -0
  56. package/dist/core/file_checkpoint.d.ts.map +1 -0
  57. package/dist/core/file_checkpoint.js +162 -0
  58. package/dist/core/file_checkpoint.js.map +1 -0
  59. package/dist/core/hooks.d.ts +43 -0
  60. package/dist/core/hooks.d.ts.map +1 -0
  61. package/dist/core/hooks.js +110 -0
  62. package/dist/core/hooks.js.map +1 -0
  63. package/dist/core/llm.d.ts.map +1 -1
  64. package/dist/core/llm.js +15 -9
  65. package/dist/core/llm.js.map +1 -1
  66. package/dist/core/longdoc.js +5 -5
  67. package/dist/core/mcp.d.ts +16 -0
  68. package/dist/core/mcp.d.ts.map +1 -1
  69. package/dist/core/mcp.js +55 -0
  70. package/dist/core/mcp.js.map +1 -1
  71. package/dist/core/model_config.d.ts +40 -0
  72. package/dist/core/model_config.d.ts.map +1 -0
  73. package/dist/core/model_config.js +191 -0
  74. package/dist/core/model_config.js.map +1 -0
  75. package/dist/core/skill.d.ts +7 -0
  76. package/dist/core/skill.d.ts.map +1 -1
  77. package/dist/core/skill.js +47 -0
  78. package/dist/core/skill.js.map +1 -1
  79. package/dist/core/skymd.d.ts +39 -0
  80. package/dist/core/skymd.d.ts.map +1 -0
  81. package/dist/core/skymd.js +177 -0
  82. package/dist/core/skymd.js.map +1 -0
  83. package/dist/core/tool.d.ts +12 -0
  84. package/dist/core/tool.d.ts.map +1 -1
  85. package/dist/core/tool.js +30 -0
  86. package/dist/core/tool.js.map +1 -1
  87. package/dist/core/verify.d.ts +27 -0
  88. package/dist/core/verify.d.ts.map +1 -0
  89. package/dist/core/verify.js +62 -0
  90. package/dist/core/verify.js.map +1 -0
  91. package/dist/skills/loader.d.ts +22 -2
  92. package/dist/skills/loader.d.ts.map +1 -1
  93. package/dist/skills/loader.js +45 -15
  94. package/dist/skills/loader.js.map +1 -1
  95. package/dist/tools/builtin.d.ts.map +1 -1
  96. package/dist/tools/builtin.js +13 -3
  97. package/dist/tools/builtin.js.map +1 -1
  98. package/dist/tools/model_tool.d.ts +11 -0
  99. package/dist/tools/model_tool.d.ts.map +1 -0
  100. package/dist/tools/model_tool.js +71 -0
  101. package/dist/tools/model_tool.js.map +1 -0
  102. package/dist/tools/todo.d.ts +30 -0
  103. package/dist/tools/todo.d.ts.map +1 -0
  104. package/dist/tools/todo.js +78 -0
  105. package/dist/tools/todo.js.map +1 -0
  106. package/docs/AESTHETIC_DESIGN.md +152 -144
  107. package/docs/OPTIMIZATION_PLAN.md +178 -178
  108. package/package.json +68 -68
  109. package/scripts/install.js +48 -48
  110. package/scripts/link.js +10 -10
  111. package/setup.bat +79 -79
  112. package/skill-test-ty2fOA/test.md +10 -10
  113. package/src/agents/dew.ts +70 -70
  114. package/src/agents/fair.ts +102 -102
  115. package/src/agents/fog.ts +48 -48
  116. package/src/agents/frost.ts +50 -50
  117. package/src/agents/rain.ts +50 -50
  118. package/src/agents/snow.ts +239 -239
  119. package/src/cli/commands_md.ts +112 -0
  120. package/src/cli/input_macros.ts +83 -0
  121. package/src/cli/loom.ts +982 -0
  122. package/src/cli/loom_chat.ts +598 -0
  123. package/src/cli/main.ts +255 -9
  124. package/src/cli/mode.ts +58 -58
  125. package/src/cli/tui.ts +228 -222
  126. package/src/core/agent/guard.ts +134 -134
  127. package/src/core/agent/task.ts +100 -100
  128. package/src/core/agent.ts +195 -16
  129. package/src/core/arbitrate.ts +162 -162
  130. package/src/core/catalog.ts +178 -178
  131. package/src/core/checkpoint.ts +94 -94
  132. package/src/core/estimate.ts +104 -104
  133. package/src/core/evolve.ts +191 -191
  134. package/src/core/factory.ts +31 -2
  135. package/src/core/file_checkpoint.ts +136 -0
  136. package/src/core/filter.ts +103 -103
  137. package/src/core/graph.ts +156 -156
  138. package/src/core/hooks.ts +126 -0
  139. package/src/core/icons.ts +53 -53
  140. package/src/core/index.ts +37 -37
  141. package/src/core/learn.ts +146 -146
  142. package/src/core/llm.ts +15 -9
  143. package/src/core/longdoc.ts +155 -155
  144. package/src/core/mcp.ts +48 -0
  145. package/src/core/mcp_server.ts +176 -176
  146. package/src/core/model_config.ts +157 -0
  147. package/src/core/profile.ts +255 -255
  148. package/src/core/router.ts +124 -124
  149. package/src/core/sandbox.ts +142 -142
  150. package/src/core/security.ts +243 -243
  151. package/src/core/skill.ts +42 -0
  152. package/src/core/skymd.ts +143 -0
  153. package/src/core/theme.ts +65 -65
  154. package/src/core/tool.ts +30 -0
  155. package/src/core/tool_router.ts +193 -193
  156. package/src/core/vector.ts +152 -152
  157. package/src/core/verify.ts +71 -0
  158. package/src/core/workspace.ts +150 -150
  159. package/src/plugins/loader.ts +66 -66
  160. package/src/skills/loader.ts +45 -16
  161. package/src/sql.js.d.ts +29 -29
  162. package/src/tools/builtin.ts +13 -3
  163. package/src/tools/computer.ts +269 -269
  164. package/src/tools/delegate.ts +49 -49
  165. package/src/tools/model_tool.ts +74 -0
  166. package/src/tools/todo.ts +76 -0
  167. package/src/web/tts.ts +93 -93
  168. package/tests/agent.test.ts +159 -159
  169. package/tests/agent_helpers.test.ts +48 -48
  170. package/tests/bus.test.ts +121 -121
  171. package/tests/catalog.test.ts +86 -86
  172. package/tests/checkpoint_commands.test.ts +124 -0
  173. package/tests/claude_compat.test.ts +110 -0
  174. package/tests/config.test.ts +41 -41
  175. package/tests/guard.test.ts +75 -75
  176. package/tests/icons.test.ts +45 -45
  177. package/tests/loom.test.ts +248 -0
  178. package/tests/memory.test.ts +170 -170
  179. package/tests/model_config.test.ts +109 -0
  180. package/tests/router.test.ts +86 -86
  181. package/tests/schemas.test.ts +51 -51
  182. package/tests/semantic.test.ts +83 -83
  183. package/tests/setup.ts +10 -10
  184. package/tests/skill.test.ts +172 -172
  185. package/tests/skymd.test.ts +146 -0
  186. package/tests/task.test.ts +60 -60
  187. package/tests/todo_toolstats.test.ts +94 -0
  188. package/tests/tool.test.ts +108 -108
  189. package/tests/tool_router.test.ts +71 -71
  190. package/tests/tui.test.ts +67 -67
  191. package/vitest.config.ts +17 -17
  192. package/=12 +0 -0
  193. package/=8 +0 -0
@@ -1,255 +1,255 @@
1
- /**
2
- * Local user profile (用户画像) + per-agent custom personas.
3
- *
4
- * Everything here lives under ~/.skyloom/ and never leaves the machine:
5
- *
6
- * - profile.json — a free-form dict of facts about the user.
7
- * - memories.json — running narrative of moods, life events, things worth following up on.
8
- * - personas/<agent>.md — optional custom role for a specific agent.
9
- */
10
-
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
14
-
15
- const VALID_AGENTS = new Set<string>(AGENT_NAMES);
16
-
17
- // Keep only the most recent N memories in the prompt + on disk.
18
- const MEMORY_CAP = 40;
19
- // When over cap, fold this many of the oldest notes into ONE digest entry.
20
- const FOLD_BATCH = 8;
21
-
22
- // Pluggable summarizer for memory folding.
23
- let _summarizer: ((notes: string[]) => string) | null = null;
24
-
25
- export function setMemorySummarizer(fn: (notes: string[]) => string): void {
26
- _summarizer = fn;
27
- }
28
-
29
- function profilePath(): string {
30
- return path.join(USER_CONFIG_DIR, 'profile.json');
31
- }
32
-
33
- function memoriesPath(): string {
34
- return path.join(USER_CONFIG_DIR, 'memories.json');
35
- }
36
-
37
- function personaPath(agent: string): string {
38
- return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
39
- }
40
-
41
- // ── User profile ──
42
-
43
- export function loadProfile(): Record<string, string> {
44
- const p = profilePath();
45
- if (!fs.existsSync(p)) return {};
46
- try {
47
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
48
- return typeof data === 'object' && data !== null ? data : {};
49
- } catch {
50
- return {};
51
- }
52
- }
53
-
54
- export function saveProfile(data: Record<string, string>): void {
55
- const p = profilePath();
56
- const dir = path.dirname(p);
57
- if (!fs.existsSync(dir)) {
58
- fs.mkdirSync(dir, { recursive: true });
59
- }
60
- const tmp = p + '.tmp';
61
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
62
- fs.renameSync(tmp, p);
63
- }
64
-
65
- export function setProfileField(key: string, value: string): void {
66
- key = (key || '').trim();
67
- if (!key) return;
68
- const data = loadProfile();
69
- data[key] = value;
70
- saveProfile(data);
71
- }
72
-
73
- export function clearProfileField(key?: string | null): void {
74
- if (key == null) {
75
- try {
76
- const p = profilePath();
77
- if (fs.existsSync(p)) fs.unlinkSync(p);
78
- } catch { /* ignore */ }
79
- return;
80
- }
81
- const data = loadProfile();
82
- delete data[key];
83
- saveProfile(data);
84
- }
85
-
86
- export function formatProfileForPrompt(lang: string = 'zh'): string {
87
- const data = loadProfile();
88
- const entries = Object.entries(data);
89
- if (entries.length === 0) return '';
90
-
91
- const lines = entries.map(([k, v]) => {
92
- return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
93
- });
94
- const body = lines.join('\n');
95
-
96
- if (lang === 'en') {
97
- return '\n\n## About the user (remember this and use it naturally)\n' + body;
98
- }
99
- return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
100
- }
101
-
102
- // ── Emotional / narrative memory ──
103
-
104
- export function loadMemories(): Record<string, any>[] {
105
- const p = memoriesPath();
106
- if (!fs.existsSync(p)) return [];
107
- try {
108
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
109
- return Array.isArray(data) ? data : [];
110
- } catch {
111
- return [];
112
- }
113
- }
114
-
115
- function norm(text: string): string {
116
- return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
117
- }
118
-
119
- function writeMemories(items: Record<string, any>[]): void {
120
- const p = memoriesPath();
121
- const dir = path.dirname(p);
122
- if (!fs.existsSync(dir)) {
123
- fs.mkdirSync(dir, { recursive: true });
124
- }
125
- const tmp = p + '.tmp';
126
- fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
127
- fs.renameSync(tmp, p);
128
- }
129
-
130
- function digest(notes: string[]): string {
131
- const seen: string[] = [];
132
- for (let n of notes) {
133
- n = n.trim();
134
- if (n.startsWith('早些时候:')) {
135
- n = n.slice('早些时候:'.length);
136
- }
137
- for (const part of n.split(';')) {
138
- const p = part.trim();
139
- if (p && !seen.includes(p)) {
140
- seen.push(p);
141
- }
142
- }
143
- }
144
- let joined = seen.join(';');
145
- if (joined.length > 180) {
146
- joined = joined.slice(0, 179) + '…';
147
- }
148
- return '早些时候:' + joined;
149
- }
150
-
151
- function summarizeNotes(notes: string[]): string {
152
- if (_summarizer) {
153
- try {
154
- const out = _summarizer(notes);
155
- if (out && out.trim()) return out.trim();
156
- } catch { /* ignore */ }
157
- }
158
- return digest(notes);
159
- }
160
-
161
- function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
162
- let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
163
- foldN = Math.min(foldN, items.length - 1);
164
- if (foldN <= 0) return items.slice(-MEMORY_CAP);
165
-
166
- const old = items.slice(0, foldN);
167
- const rest = items.slice(foldN);
168
- const digestEntry: Record<string, any> = {
169
- ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
170
- note: summarizeNotes(old.map(m => String(m.note || ''))),
171
- summary: true,
172
- };
173
- return [digestEntry, ...rest];
174
- }
175
-
176
- export function appendMemory(note: string): boolean {
177
- note = (note || '').trim();
178
- if (!note) return false;
179
-
180
- const items = loadMemories();
181
- const key = norm(note);
182
-
183
- if (key) {
184
- for (const m of items) {
185
- if (norm(String(m.note || '')) === key) {
186
- m.ts = new Date().toISOString().slice(0, 10); // refresh recency
187
- if (note.length > String(m.note || '').length) {
188
- m.note = note; // keep the richer phrasing
189
- }
190
- writeMemories(items);
191
- return true;
192
- }
193
- }
194
- }
195
-
196
- items.push({ ts: new Date().toISOString().slice(0, 10), note });
197
- if (items.length > MEMORY_CAP) {
198
- writeMemories(foldOldest(items));
199
- } else {
200
- writeMemories(items);
201
- }
202
- return true;
203
- }
204
-
205
- export function clearMemories(): void {
206
- try {
207
- const p = memoriesPath();
208
- if (fs.existsSync(p)) fs.unlinkSync(p);
209
- } catch { /* ignore */ }
210
- }
211
-
212
- export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
213
- const items = loadMemories();
214
- if (items.length === 0) return '';
215
-
216
- const recent = items.slice(-limit);
217
- const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
218
- const body = lines.join('\n');
219
-
220
- if (lang === 'en') {
221
- return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
222
- }
223
- return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
224
- }
225
-
226
- // ── Per-agent custom persona ──
227
-
228
- export function loadPersona(agent: string): string | null {
229
- const p = personaPath(agent);
230
- if (!fs.existsSync(p)) return null;
231
- try {
232
- const text = fs.readFileSync(p, 'utf-8').trim();
233
- return text || null;
234
- } catch {
235
- return null;
236
- }
237
- }
238
-
239
- export function savePersona(agent: string, text: string): boolean {
240
- if (!VALID_AGENTS.has(agent)) return false;
241
- const p = personaPath(agent);
242
- const dir = path.dirname(p);
243
- if (!fs.existsSync(dir)) {
244
- fs.mkdirSync(dir, { recursive: true });
245
- }
246
- fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
247
- return true;
248
- }
249
-
250
- export function clearPersona(agent: string): void {
251
- try {
252
- const p = personaPath(agent);
253
- if (fs.existsSync(p)) fs.unlinkSync(p);
254
- } catch { /* ignore */ }
255
- }
1
+ /**
2
+ * Local user profile (用户画像) + per-agent custom personas.
3
+ *
4
+ * Everything here lives under ~/.skyloom/ and never leaves the machine:
5
+ *
6
+ * - profile.json — a free-form dict of facts about the user.
7
+ * - memories.json — running narrative of moods, life events, things worth following up on.
8
+ * - personas/<agent>.md — optional custom role for a specific agent.
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
14
+
15
+ const VALID_AGENTS = new Set<string>(AGENT_NAMES);
16
+
17
+ // Keep only the most recent N memories in the prompt + on disk.
18
+ const MEMORY_CAP = 40;
19
+ // When over cap, fold this many of the oldest notes into ONE digest entry.
20
+ const FOLD_BATCH = 8;
21
+
22
+ // Pluggable summarizer for memory folding.
23
+ let _summarizer: ((notes: string[]) => string) | null = null;
24
+
25
+ export function setMemorySummarizer(fn: (notes: string[]) => string): void {
26
+ _summarizer = fn;
27
+ }
28
+
29
+ function profilePath(): string {
30
+ return path.join(USER_CONFIG_DIR, 'profile.json');
31
+ }
32
+
33
+ function memoriesPath(): string {
34
+ return path.join(USER_CONFIG_DIR, 'memories.json');
35
+ }
36
+
37
+ function personaPath(agent: string): string {
38
+ return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
39
+ }
40
+
41
+ // ── User profile ──
42
+
43
+ export function loadProfile(): Record<string, string> {
44
+ const p = profilePath();
45
+ if (!fs.existsSync(p)) return {};
46
+ try {
47
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
48
+ return typeof data === 'object' && data !== null ? data : {};
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ export function saveProfile(data: Record<string, string>): void {
55
+ const p = profilePath();
56
+ const dir = path.dirname(p);
57
+ if (!fs.existsSync(dir)) {
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ }
60
+ const tmp = p + '.tmp';
61
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
62
+ fs.renameSync(tmp, p);
63
+ }
64
+
65
+ export function setProfileField(key: string, value: string): void {
66
+ key = (key || '').trim();
67
+ if (!key) return;
68
+ const data = loadProfile();
69
+ data[key] = value;
70
+ saveProfile(data);
71
+ }
72
+
73
+ export function clearProfileField(key?: string | null): void {
74
+ if (key == null) {
75
+ try {
76
+ const p = profilePath();
77
+ if (fs.existsSync(p)) fs.unlinkSync(p);
78
+ } catch { /* ignore */ }
79
+ return;
80
+ }
81
+ const data = loadProfile();
82
+ delete data[key];
83
+ saveProfile(data);
84
+ }
85
+
86
+ export function formatProfileForPrompt(lang: string = 'zh'): string {
87
+ const data = loadProfile();
88
+ const entries = Object.entries(data);
89
+ if (entries.length === 0) return '';
90
+
91
+ const lines = entries.map(([k, v]) => {
92
+ return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
93
+ });
94
+ const body = lines.join('\n');
95
+
96
+ if (lang === 'en') {
97
+ return '\n\n## About the user (remember this and use it naturally)\n' + body;
98
+ }
99
+ return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
100
+ }
101
+
102
+ // ── Emotional / narrative memory ──
103
+
104
+ export function loadMemories(): Record<string, any>[] {
105
+ const p = memoriesPath();
106
+ if (!fs.existsSync(p)) return [];
107
+ try {
108
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
109
+ return Array.isArray(data) ? data : [];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ function norm(text: string): string {
116
+ return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
117
+ }
118
+
119
+ function writeMemories(items: Record<string, any>[]): void {
120
+ const p = memoriesPath();
121
+ const dir = path.dirname(p);
122
+ if (!fs.existsSync(dir)) {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ }
125
+ const tmp = p + '.tmp';
126
+ fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
127
+ fs.renameSync(tmp, p);
128
+ }
129
+
130
+ function digest(notes: string[]): string {
131
+ const seen: string[] = [];
132
+ for (let n of notes) {
133
+ n = n.trim();
134
+ if (n.startsWith('早些时候:')) {
135
+ n = n.slice('早些时候:'.length);
136
+ }
137
+ for (const part of n.split(';')) {
138
+ const p = part.trim();
139
+ if (p && !seen.includes(p)) {
140
+ seen.push(p);
141
+ }
142
+ }
143
+ }
144
+ let joined = seen.join(';');
145
+ if (joined.length > 180) {
146
+ joined = joined.slice(0, 179) + '…';
147
+ }
148
+ return '早些时候:' + joined;
149
+ }
150
+
151
+ function summarizeNotes(notes: string[]): string {
152
+ if (_summarizer) {
153
+ try {
154
+ const out = _summarizer(notes);
155
+ if (out && out.trim()) return out.trim();
156
+ } catch { /* ignore */ }
157
+ }
158
+ return digest(notes);
159
+ }
160
+
161
+ function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
162
+ let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
163
+ foldN = Math.min(foldN, items.length - 1);
164
+ if (foldN <= 0) return items.slice(-MEMORY_CAP);
165
+
166
+ const old = items.slice(0, foldN);
167
+ const rest = items.slice(foldN);
168
+ const digestEntry: Record<string, any> = {
169
+ ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
170
+ note: summarizeNotes(old.map(m => String(m.note || ''))),
171
+ summary: true,
172
+ };
173
+ return [digestEntry, ...rest];
174
+ }
175
+
176
+ export function appendMemory(note: string): boolean {
177
+ note = (note || '').trim();
178
+ if (!note) return false;
179
+
180
+ const items = loadMemories();
181
+ const key = norm(note);
182
+
183
+ if (key) {
184
+ for (const m of items) {
185
+ if (norm(String(m.note || '')) === key) {
186
+ m.ts = new Date().toISOString().slice(0, 10); // refresh recency
187
+ if (note.length > String(m.note || '').length) {
188
+ m.note = note; // keep the richer phrasing
189
+ }
190
+ writeMemories(items);
191
+ return true;
192
+ }
193
+ }
194
+ }
195
+
196
+ items.push({ ts: new Date().toISOString().slice(0, 10), note });
197
+ if (items.length > MEMORY_CAP) {
198
+ writeMemories(foldOldest(items));
199
+ } else {
200
+ writeMemories(items);
201
+ }
202
+ return true;
203
+ }
204
+
205
+ export function clearMemories(): void {
206
+ try {
207
+ const p = memoriesPath();
208
+ if (fs.existsSync(p)) fs.unlinkSync(p);
209
+ } catch { /* ignore */ }
210
+ }
211
+
212
+ export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
213
+ const items = loadMemories();
214
+ if (items.length === 0) return '';
215
+
216
+ const recent = items.slice(-limit);
217
+ const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
218
+ const body = lines.join('\n');
219
+
220
+ if (lang === 'en') {
221
+ return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
222
+ }
223
+ return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
224
+ }
225
+
226
+ // ── Per-agent custom persona ──
227
+
228
+ export function loadPersona(agent: string): string | null {
229
+ const p = personaPath(agent);
230
+ if (!fs.existsSync(p)) return null;
231
+ try {
232
+ const text = fs.readFileSync(p, 'utf-8').trim();
233
+ return text || null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ export function savePersona(agent: string, text: string): boolean {
240
+ if (!VALID_AGENTS.has(agent)) return false;
241
+ const p = personaPath(agent);
242
+ const dir = path.dirname(p);
243
+ if (!fs.existsSync(dir)) {
244
+ fs.mkdirSync(dir, { recursive: true });
245
+ }
246
+ fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
247
+ return true;
248
+ }
249
+
250
+ export function clearPersona(agent: string): void {
251
+ try {
252
+ const p = personaPath(agent);
253
+ if (fs.existsSync(p)) fs.unlinkSync(p);
254
+ } catch { /* ignore */ }
255
+ }