skyloom 1.13.5 → 1.13.7

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 (195) 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/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -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
+ }