@dyyz1993/pi-coding-agent 0.74.24 → 0.74.27

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 (157) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +3 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/session-manager.d.ts +5 -0
  6. package/dist/core/session-manager.d.ts.map +1 -1
  7. package/dist/core/session-manager.js +8 -0
  8. package/dist/core/session-manager.js.map +1 -1
  9. package/dist/extensions/agent-permissions/index.ts +235 -0
  10. package/dist/extensions/ask-tools/index.ts +115 -0
  11. package/dist/extensions/auto-memory/contract.d.ts +51 -0
  12. package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
  13. package/dist/extensions/auto-memory/contract.js +2 -0
  14. package/dist/extensions/auto-memory/contract.js.map +1 -0
  15. package/dist/extensions/auto-memory/contract.ts +56 -0
  16. package/dist/extensions/auto-memory/index.ts +969 -0
  17. package/dist/extensions/auto-memory/prompts.ts +202 -0
  18. package/dist/extensions/auto-memory/skip-rules.ts +297 -0
  19. package/dist/extensions/auto-memory/utils.ts +208 -0
  20. package/dist/extensions/auto-session-title/index.ts +83 -0
  21. package/dist/extensions/bash-ext/contract.d.ts +79 -0
  22. package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
  23. package/dist/extensions/bash-ext/contract.js +2 -0
  24. package/dist/extensions/bash-ext/contract.js.map +1 -0
  25. package/dist/extensions/bash-ext/contract.ts +69 -0
  26. package/dist/extensions/bash-ext/index.ts +858 -0
  27. package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
  28. package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
  29. package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
  30. package/dist/extensions/claude-hooks-compat/index.ts +178 -0
  31. package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
  32. package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
  33. package/dist/extensions/claude-hooks-compat/types.ts +77 -0
  34. package/dist/extensions/compaction-manager/config.ts +47 -0
  35. package/dist/extensions/compaction-manager/context-fold.ts +63 -0
  36. package/dist/extensions/compaction-manager/index.ts +151 -0
  37. package/dist/extensions/compaction-manager/microcompact.ts +49 -0
  38. package/dist/extensions/compaction-manager/reactive.ts +9 -0
  39. package/dist/extensions/compaction-manager/session-memory.ts +48 -0
  40. package/dist/extensions/coordinator/INTEGRATION.md +376 -0
  41. package/dist/extensions/coordinator/handler.test.ts +277 -0
  42. package/dist/extensions/coordinator/handler.ts +189 -0
  43. package/dist/extensions/coordinator/index.ts +261 -0
  44. package/dist/extensions/coordinator/types.d.ts +100 -0
  45. package/dist/extensions/coordinator/types.d.ts.map +1 -0
  46. package/dist/extensions/coordinator/types.js +2 -0
  47. package/dist/extensions/coordinator/types.js.map +1 -0
  48. package/dist/extensions/coordinator/types.ts +72 -0
  49. package/dist/extensions/file-snapshot/index.ts +131 -0
  50. package/dist/extensions/file-time-guard/README.md +133 -0
  51. package/dist/extensions/file-time-guard/config.ts +13 -0
  52. package/dist/extensions/file-time-guard/index.ts +171 -0
  53. package/dist/extensions/hooks-engine/index.ts +117 -0
  54. package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
  55. package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
  56. package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
  57. package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
  58. package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
  59. package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
  60. package/dist/extensions/lsp/lsp/contract.js +2 -0
  61. package/dist/extensions/lsp/lsp/contract.js.map +1 -0
  62. package/dist/extensions/lsp/lsp/contract.ts +103 -0
  63. package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
  64. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
  65. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
  66. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
  67. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
  68. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
  69. package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
  70. package/dist/extensions/lsp/lsp/index.ts +310 -0
  71. package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
  72. package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
  73. package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
  74. package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
  75. package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
  76. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
  77. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
  78. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
  79. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
  80. package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
  81. package/dist/extensions/message-bridge/GUIDE.md +210 -0
  82. package/dist/extensions/message-bridge/index.ts +222 -0
  83. package/dist/extensions/output-guard/index.ts +446 -0
  84. package/dist/extensions/preview/index.ts +278 -0
  85. package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
  86. package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
  87. package/dist/extensions/rules-engine/cache.js +232 -0
  88. package/dist/extensions/rules-engine/cache.ts +38 -0
  89. package/dist/extensions/rules-engine/config.js +63 -0
  90. package/dist/extensions/rules-engine/config.ts +70 -0
  91. package/dist/extensions/rules-engine/index.js +1530 -0
  92. package/dist/extensions/rules-engine/index.ts +552 -0
  93. package/dist/extensions/rules-engine/injector.js +68 -0
  94. package/dist/extensions/rules-engine/injector.ts +74 -0
  95. package/dist/extensions/rules-engine/loader.js +179 -0
  96. package/dist/extensions/rules-engine/loader.ts +205 -0
  97. package/dist/extensions/rules-engine/matcher.js +534 -0
  98. package/dist/extensions/rules-engine/matcher.ts +52 -0
  99. package/dist/extensions/rules-engine/types.d.ts +156 -0
  100. package/dist/extensions/rules-engine/types.d.ts.map +1 -0
  101. package/dist/extensions/rules-engine/types.js +2 -0
  102. package/dist/extensions/rules-engine/types.js.map +1 -0
  103. package/dist/extensions/rules-engine/types.ts +169 -0
  104. package/dist/extensions/session-supervisor/checker.ts +116 -0
  105. package/dist/extensions/session-supervisor/config.ts +45 -0
  106. package/dist/extensions/session-supervisor/index.ts +726 -0
  107. package/dist/extensions/session-supervisor/prompts.ts +132 -0
  108. package/dist/extensions/session-supervisor/scheduler.ts +69 -0
  109. package/dist/extensions/session-supervisor/types.ts +215 -0
  110. package/dist/extensions/subagent/README.md +172 -0
  111. package/dist/extensions/subagent/agents/explorer.md +25 -0
  112. package/dist/extensions/subagent/agents/guide.md +27 -0
  113. package/dist/extensions/subagent/agents/planner.md +37 -0
  114. package/dist/extensions/subagent/agents/reviewer.md +35 -0
  115. package/dist/extensions/subagent/agents/scout.md +50 -0
  116. package/dist/extensions/subagent/agents/verification.md +35 -0
  117. package/dist/extensions/subagent/agents/worker.md +24 -0
  118. package/dist/extensions/subagent/agents.ts +25 -0
  119. package/dist/extensions/subagent/index.ts +987 -0
  120. package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
  121. package/dist/extensions/subagent/prompts/implement.md +10 -0
  122. package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
  123. package/dist/extensions/subagent-ext/contract.d.ts +2 -0
  124. package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
  125. package/dist/extensions/subagent-ext/contract.js +2 -0
  126. package/dist/extensions/subagent-ext/contract.js.map +1 -0
  127. package/dist/extensions/subagent-ext/contract.ts +1 -0
  128. package/dist/extensions/subagent-ext/index.ts +347 -0
  129. package/dist/extensions/subagent-shared/contract.d.ts +25 -0
  130. package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
  131. package/dist/extensions/subagent-shared/contract.js +2 -0
  132. package/dist/extensions/subagent-shared/contract.js.map +1 -0
  133. package/dist/extensions/subagent-shared/contract.ts +28 -0
  134. package/dist/extensions/subagent-shared/index.ts +4 -0
  135. package/dist/extensions/subagent-shared/render.ts +166 -0
  136. package/dist/extensions/subagent-shared/types.ts +35 -0
  137. package/dist/extensions/subagent-shared/utils.ts +112 -0
  138. package/dist/extensions/subagent-v2/contract.d.ts +2 -0
  139. package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
  140. package/dist/extensions/subagent-v2/contract.js +2 -0
  141. package/dist/extensions/subagent-v2/contract.js.map +1 -0
  142. package/dist/extensions/subagent-v2/contract.ts +1 -0
  143. package/dist/extensions/subagent-v2/index.ts +599 -0
  144. package/dist/extensions/todo-ext/contract.d.ts +27 -0
  145. package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
  146. package/dist/extensions/todo-ext/contract.js +2 -0
  147. package/dist/extensions/todo-ext/contract.js.map +1 -0
  148. package/dist/extensions/todo-ext/contract.ts +30 -0
  149. package/dist/extensions/todo-ext/index.ts +419 -0
  150. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  151. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  152. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  153. package/examples/extensions/sandbox/package-lock.json +2 -2
  154. package/examples/extensions/sandbox/package.json +1 -1
  155. package/examples/extensions/with-deps/package-lock.json +2 -2
  156. package/examples/extensions/with-deps/package.json +1 -1
  157. package/package.json +6 -5
@@ -0,0 +1,202 @@
1
+ export const MEMORY_SYSTEM_PROMPT = (memoryDir: string, memoryContent: string): string => `# auto memory
2
+
3
+ You have a persistent memory system at \`${memoryDir}\`.
4
+
5
+ ## Types of memory
6
+ - user — user's role, goals, preferences, knowledge
7
+ - feedback — guidance about how to approach work (both corrections AND confirmations)
8
+ - project — ongoing work, deadlines, decisions not derivable from code
9
+ - reference — pointers to external systems (dashboards, issue trackers)
10
+ - bookmark — user-bookmarked chat messages with LLM summaries (user-managed, never auto-delete)
11
+
12
+ ## What NOT to save
13
+ - Code patterns, architecture, file paths (derivable from code)
14
+ - Git history (derivable from git)
15
+ - Debug solutions (the fix is in the code)
16
+ - Anything already in CLAUDE.md or system instructions
17
+
18
+ ## How to save
19
+ Step 1 — Write memory file with frontmatter:
20
+ ---
21
+ name: {{memory name}}
22
+ description: {{one-line description}}
23
+ type: {{user, feedback, project, reference}}
24
+ ---
25
+ {{content with Why: and How to apply: lines for feedback/project types}}
26
+
27
+ Step 2 — Add pointer in MEMORY.md (one line, ~150 chars):
28
+ - [Title](file.md) — one-line hook
29
+
30
+ ## When to access
31
+ - Read memory files when you need user context or project history
32
+ - Proactively save important information you learn about the user or project
33
+
34
+ ## MEMORY.md
35
+ ${memoryContent || "Your memory is currently empty."}`;
36
+
37
+ export const SELECT_MEMORIES_PROMPT = `你是记忆系统的文件选择器 + 关键词净化器。
38
+
39
+ ## 任务 1:文件选择
40
+ 根据用户查询选择相关记忆文件。
41
+ - 只选择确定有用的
42
+ - 最多 5 个
43
+ - 不确定就不选
44
+
45
+ ## 任务 2:关键词净化
46
+
47
+ ### 规则类型
48
+ - exact: 精确匹配(查询整句 === 关键词)
49
+ - prefix: 开头匹配(查询以关键词开头)
50
+ - contains: 包含匹配(查询中包含关键词,易误判,慎用)
51
+ - regex: 正则匹配(复杂模式)
52
+
53
+ ### 规则动作
54
+ - skip: 命中 → 跳过 Prefetch
55
+ - guard: 命中 → 不跳过(拦截 skip,优先级最高)
56
+
57
+ ### 正向净化(添加 skip 规则)
58
+ 如果本次 selected 与上次相同 → 用户在延续话题
59
+ → 提取 skip 规则
60
+
61
+ ### 反向净化(添加 guard 规则 或 删除 skip 规则)
62
+ 分析 history 中被跳过的条目(skipped=true)。
63
+ 看该条目的 selected 是否合理:
64
+ - 如果被跳过的那条 selected 和它前后的非 skip 条目 selected 不同
65
+ → 说明那次跳过是误判
66
+ → 标记为 bad_skip,提供修正建议
67
+ - 如果被跳过的那条 selected 合理
68
+ → 该关键词可以保留
69
+
70
+ ## 回复格式(JSON only)
71
+ {
72
+ "selected": ["file1.md"],
73
+ "purification": {
74
+ "add_rules": [
75
+ { "pattern": "继续吧", "mode": "exact", "action": "skip" },
76
+ { "pattern": "^跑一下.{0,5}$", "mode": "regex", "action": "skip" }
77
+ ],
78
+ "remove_rules": [
79
+ { "pattern": "好的", "mode": "exact" }
80
+ ],
81
+ "bad_skips": [
82
+ {
83
+ "query": "好的",
84
+ "matched_rules": ["好的(exact)"],
85
+ "reason": "'好的'太泛,单独出现也可能是新话题开头",
86
+ "suggestion": "remove"
87
+ }
88
+ ]
89
+ }
90
+ }
91
+
92
+ 无需净化时不包含 purification 字段。`;
93
+
94
+ export const EXTRACTION_PROMPT = (
95
+ manifest: string,
96
+ ): string => `You are the memory extraction subagent. Analyze the recent conversation
97
+ and determine what should be persisted to memory.
98
+
99
+ ## Available memory files
100
+ ${manifest}
101
+
102
+ Check this list — update existing rather than creating duplicates.
103
+
104
+ ## Types of memory
105
+ user — user's role, goals, preferences, knowledge
106
+ feedback — guidance about how to approach work (corrections AND confirmations)
107
+ project — ongoing work, deadlines, decisions not derivable from code
108
+ reference — pointers to external systems
109
+
110
+ ## What NOT to save
111
+ - Code patterns, architecture, file paths (derivable from code)
112
+ - Git history (derivable from git)
113
+ - Debug solutions (the fix is in the code)
114
+ - Anything obvious from reading the codebase
115
+
116
+ Respond with JSON only:
117
+ {
118
+ "actions": [
119
+ {
120
+ "op": "create",
121
+ "filename": "feedback_testing.md",
122
+ "name": "Testing Policy",
123
+ "description": "Never mock the database in integration tests",
124
+ "type": "feedback",
125
+ "content": "Integration tests must hit a real database...\\n\\n**Why:** ...\\n\\n**How to apply:** ..."
126
+ },
127
+ {
128
+ "op": "update",
129
+ "filename": "user_role.md",
130
+ "append": "\\n\\nAlso prefers TypeScript over JavaScript."
131
+ },
132
+ { "op": "skip" }
133
+ ]
134
+ }`;
135
+
136
+ export const DREAM_PROMPT = (
137
+ allContent: string,
138
+ indexContent: string,
139
+ memoryDir: string,
140
+ ): string => `You are performing a dream — a reflective pass over memory files.
141
+ Analyze all memories and determine what to consolidate.
142
+
143
+ Memory directory: ${memoryDir}
144
+
145
+ ## Phase 1 — Orient
146
+ Understand the current MEMORY.md index and existing topic files.
147
+
148
+ ## Phase 2 — Gather signal
149
+ Check for:
150
+ - Duplicated information across files
151
+ - Contradicted facts (old info vs new)
152
+ - Stale information (outdated deadlines, completed projects)
153
+ - Related topics that should be merged
154
+
155
+ ## Phase 3 — Consolidate
156
+ Decide what to merge, delete, or update.
157
+
158
+ ## ⚠️ Bookmark protection rules (type=bookmark)
159
+ - **NEVER delete** bookmark files — they are user-managed and must be preserved
160
+ - **NEVER merge** bookmark files into other files unless they are duplicates of each other
161
+ - You MAY update/refine the summary content of a bookmark to make it more concise
162
+ - You MAY update tags of a bookmark if they have become stale or incomplete
163
+ - If two bookmarks cover the exact same topic, you may merge them but MUST preserve sourceSession/sourceMessageIds references
164
+
165
+ ## Phase 4 — Prune
166
+ Generate a new MEMORY.md index (≤ 200 lines, ≤ 25KB).
167
+
168
+ All memories:
169
+ ${allContent}
170
+
171
+ Current MEMORY.md:
172
+ ${indexContent}
173
+
174
+ Respond with JSON only:
175
+ {
176
+ "merges": [
177
+ { "sources": ["file1.md", "file2.md"], "target": "merged.md", "content": "..." }
178
+ ],
179
+ "deletions": ["stale_file.md"],
180
+ "updates": [
181
+ { "filename": "existing.md", "newContent": "..." }
182
+ ],
183
+ "newIndex": "- [Title1](file1.md) — desc\\n- [Title2](file2.md) — desc\\n..."
184
+ }`;
185
+
186
+ export const BOOKMARK_SUMMARY_PROMPT = (
187
+ messageContent: string,
188
+ existingManifest: string,
189
+ ): string => `You are creating a bookmark summary for chat messages the user has explicitly saved.
190
+
191
+ ## The bookmarked content:
192
+ ${messageContent}
193
+
194
+ ${existingManifest ? `## Existing bookmarks (avoid duplicate titles):\n${existingManifest}` : ""}
195
+
196
+ Generate a structured bookmark document. Respond with JSON only:
197
+ {
198
+ "title": "Short descriptive title (≤50 chars)",
199
+ "description": "One-line description of what this covers",
200
+ "summary": "A well-structured markdown summary (2-5 paragraphs). Extract key insights, decisions, code patterns, or solutions. Preserve important details.",
201
+ "tags": ["tag1", "tag2", "tag3"]
202
+ }`;
@@ -0,0 +1,297 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export interface SkipRule {
6
+ pattern: string;
7
+ mode: "exact" | "prefix" | "contains" | "regex";
8
+ action: "skip" | "guard";
9
+ builtin?: boolean;
10
+ }
11
+
12
+ export interface HistoryEntry {
13
+ query: string;
14
+ selected: string[];
15
+ skipped: boolean;
16
+ skip_hits: string[];
17
+ guard_hits: string[];
18
+ timestamp: number;
19
+ }
20
+
21
+ export interface SkipWordStore {
22
+ version: number;
23
+ rules: SkipRule[];
24
+ history: HistoryEntry[];
25
+ lastPurifyTimestamp: number;
26
+ }
27
+
28
+ export interface PurificationResult {
29
+ add_rules?: Array<{ pattern: string; mode: SkipRule["mode"]; action: SkipRule["action"] }>;
30
+ remove_rules?: Array<{ pattern: string; mode: SkipRule["mode"] }>;
31
+ bad_skips?: Array<{
32
+ query: string;
33
+ matched_rules: string[];
34
+ reason: string;
35
+ suggestion: "remove" | "add_guard";
36
+ }>;
37
+ }
38
+
39
+ const MAX_HISTORY = 20;
40
+ const MAX_SKIP_RULES = 50;
41
+ const MAX_GUARD_RULES = 30;
42
+ const STORE_FILENAME = ".prefetch-skip-words.json";
43
+
44
+ export function getGlobalMemoryDir(): string {
45
+ return join(homedir(), ".pi", "agent", "memory");
46
+ }
47
+
48
+ export function getDefaultRules(): SkipRule[] {
49
+ const skipPatterns: Array<{ pattern: string; mode: SkipRule["mode"] }> = [
50
+ { pattern: "继续", mode: "exact" },
51
+ { pattern: "continue", mode: "exact" },
52
+ { pattern: "好的", mode: "exact" },
53
+ { pattern: "ok", mode: "exact" },
54
+ { pattern: "OK", mode: "exact" },
55
+ { pattern: "yes", mode: "exact" },
56
+ { pattern: "y", mode: "exact" },
57
+ { pattern: "是", mode: "exact" },
58
+ { pattern: "对", mode: "exact" },
59
+ { pattern: "嗯", mode: "exact" },
60
+ { pattern: "继续", mode: "prefix" },
61
+ { pattern: "谢谢", mode: "exact" },
62
+ { pattern: "感谢", mode: "exact" },
63
+ { pattern: "thanks", mode: "exact" },
64
+ { pattern: "thx", mode: "exact" },
65
+ { pattern: "收到", mode: "exact" },
66
+ { pattern: "明白", mode: "exact" },
67
+ { pattern: "了解", mode: "exact" },
68
+ { pattern: "知道了", mode: "exact" },
69
+ { pattern: "行", mode: "exact" },
70
+ { pattern: "可以", mode: "exact" },
71
+ { pattern: "没问题", mode: "exact" },
72
+ { pattern: "不用了", mode: "exact" },
73
+ { pattern: "算了", mode: "exact" },
74
+ { pattern: "稍等", mode: "exact" },
75
+ { pattern: "等一下", mode: "exact" },
76
+ { pattern: "好的谢谢", mode: "exact" },
77
+ { pattern: "好", mode: "exact" },
78
+ { pattern: "done", mode: "exact" },
79
+ { pattern: "got it", mode: "exact" },
80
+ { pattern: "right", mode: "exact" },
81
+ { pattern: "sure", mode: "exact" },
82
+ { pattern: "no", mode: "exact" },
83
+ { pattern: "nope", mode: "exact" },
84
+ { pattern: "嗯嗯", mode: "exact" },
85
+ { pattern: "哦", mode: "exact" },
86
+ { pattern: "啊", mode: "exact" },
87
+ { pattern: "哈哈", mode: "exact" },
88
+ { pattern: "懂了", mode: "exact" },
89
+ { pattern: "没错", mode: "exact" },
90
+ { pattern: "对的", mode: "exact" },
91
+ { pattern: "那就这样", mode: "exact" },
92
+ { pattern: "可以的", mode: "exact" },
93
+ { pattern: "差不多", mode: "exact" },
94
+ { pattern: "看起来不错", mode: "exact" },
95
+ { pattern: "就这样吧", mode: "exact" },
96
+ ];
97
+
98
+ const guardPatterns: Array<{ pattern: string; mode: SkipRule["mode"] }> = [
99
+ { pattern: "?", mode: "contains" },
100
+ { pattern: "?", mode: "contains" },
101
+ { pattern: "怎么", mode: "prefix" },
102
+ { pattern: "如何", mode: "prefix" },
103
+ { pattern: "为什么", mode: "prefix" },
104
+ { pattern: "什么", mode: "prefix" },
105
+ { pattern: "哪", mode: "prefix" },
106
+ { pattern: "吗", mode: "contains" },
107
+ { pattern: "呢", mode: "contains" },
108
+ { pattern: "帮", mode: "prefix" },
109
+ { pattern: "帮我", mode: "prefix" },
110
+ { pattern: "请", mode: "prefix" },
111
+ { pattern: "麻烦", mode: "prefix" },
112
+ { pattern: "\n", mode: "contains" },
113
+ { pattern: "看看", mode: "contains" },
114
+ { pattern: "查看", mode: "prefix" },
115
+ { pattern: "查一下", mode: "prefix" },
116
+ { pattern: "找", mode: "prefix" },
117
+ { pattern: "搜", mode: "prefix" },
118
+ { pattern: "分析", mode: "prefix" },
119
+ { pattern: "解释", mode: "prefix" },
120
+ { pattern: "写", mode: "prefix" },
121
+ { pattern: "改", mode: "prefix" },
122
+ { pattern: "修", mode: "prefix" },
123
+ { pattern: "删", mode: "prefix" },
124
+ { pattern: "新增", mode: "prefix" },
125
+ { pattern: "优化", mode: "prefix" },
126
+ { pattern: "重构", mode: "prefix" },
127
+ { pattern: "设计", mode: "prefix" },
128
+ { pattern: "实现", mode: "prefix" },
129
+ { pattern: "创建", mode: "prefix" },
130
+ ];
131
+
132
+ return [
133
+ ...skipPatterns.map((p) => ({ ...p, action: "skip" as const, builtin: true })),
134
+ ...guardPatterns.map((p) => ({ ...p, action: "guard" as const, builtin: true })),
135
+ ];
136
+ }
137
+
138
+ export function matchRule(query: string, rule: SkipRule): boolean {
139
+ if (!query || !rule.pattern) return false;
140
+ const q = query.toLowerCase();
141
+ const p = rule.pattern.toLowerCase();
142
+
143
+ switch (rule.mode) {
144
+ case "exact":
145
+ return q === p;
146
+ case "prefix":
147
+ return q.startsWith(p);
148
+ case "contains":
149
+ return q.includes(p);
150
+ case "regex": {
151
+ try {
152
+ const re = new RegExp(rule.pattern);
153
+ return re.test(query);
154
+ } catch (err) {
155
+ console.debug("[auto-memory] regex evaluation failed:", err instanceof Error ? err.message : err);
156
+ return false;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ export function evaluateRules(
163
+ query: string,
164
+ rules: SkipRule[],
165
+ ): { shouldSkip: boolean; skipHits: string[]; guardHits: string[] } {
166
+ const skipHits: string[] = [];
167
+ const guardHits: string[] = [];
168
+
169
+ for (const rule of rules) {
170
+ if (matchRule(query, rule)) {
171
+ if (rule.action === "skip") {
172
+ skipHits.push(rule.pattern);
173
+ } else {
174
+ guardHits.push(rule.pattern);
175
+ }
176
+ }
177
+ }
178
+
179
+ return {
180
+ shouldSkip: skipHits.length > 0 && guardHits.length === 0,
181
+ skipHits,
182
+ guardHits,
183
+ };
184
+ }
185
+
186
+ export function getDefaultStore(): SkipWordStore {
187
+ return {
188
+ version: 1,
189
+ rules: getDefaultRules(),
190
+ history: [],
191
+ lastPurifyTimestamp: 0,
192
+ };
193
+ }
194
+
195
+ export function loadSkipWordStore(dir: string): SkipWordStore {
196
+ const filePath = join(dir, STORE_FILENAME);
197
+ if (!existsSync(filePath)) {
198
+ return getDefaultStore();
199
+ }
200
+ try {
201
+ const raw = readFileSync(filePath, "utf-8");
202
+ const parsed = JSON.parse(raw) as SkipWordStore;
203
+ if (parsed.history.length > MAX_HISTORY) {
204
+ parsed.history = parsed.history.slice(-MAX_HISTORY);
205
+ }
206
+ return parsed;
207
+ } catch (err) {
208
+ console.debug("[auto-memory] skip word store load failed:", err instanceof Error ? err.message : err);
209
+ return getDefaultStore();
210
+ }
211
+ }
212
+
213
+ export async function saveSkipWordStore(dir: string, store: SkipWordStore): Promise<void> {
214
+ mkdirSync(dir, { recursive: true });
215
+ const filePath = join(dir, STORE_FILENAME);
216
+ const tmpPath = filePath + ".tmp";
217
+ const data = JSON.stringify(store, null, 2);
218
+ writeFileSync(tmpPath, data, "utf-8");
219
+ renameSync(tmpPath, filePath);
220
+ }
221
+
222
+ export function addHistoryEntry(store: SkipWordStore, entry: HistoryEntry): SkipWordStore {
223
+ const history = [...store.history, entry];
224
+ if (history.length > MAX_HISTORY) {
225
+ history.splice(0, history.length - MAX_HISTORY);
226
+ }
227
+ return { ...store, history };
228
+ }
229
+
230
+ export function applyPurification(store: SkipWordStore, result: PurificationResult): SkipWordStore {
231
+ const rules = [...store.rules];
232
+
233
+ if (result.add_rules) {
234
+ for (const add of result.add_rules) {
235
+ const exists = rules.some((r) => r.pattern === add.pattern && r.mode === add.mode && r.action === add.action);
236
+ if (!exists) {
237
+ rules.push({ pattern: add.pattern, mode: add.mode, action: add.action, builtin: false });
238
+ }
239
+ }
240
+ }
241
+
242
+ if (result.remove_rules) {
243
+ for (const rem of result.remove_rules) {
244
+ const idx = rules.findIndex((r) => r.pattern === rem.pattern && r.mode === rem.mode && !r.builtin);
245
+ if (idx !== -1) {
246
+ rules.splice(idx, 1);
247
+ }
248
+ }
249
+ }
250
+
251
+ if (result.bad_skips) {
252
+ for (const bad of result.bad_skips) {
253
+ for (const matchedPattern of bad.matched_rules) {
254
+ const idx = rules.findIndex((r) => r.pattern === matchedPattern && r.action === "skip" && !r.builtin);
255
+ if (idx !== -1) {
256
+ if (bad.suggestion === "remove") {
257
+ rules.splice(idx, 1);
258
+ }
259
+ } else {
260
+ const builtinRule = rules.find((r) => r.pattern === matchedPattern && r.action === "skip" && r.builtin);
261
+ if (builtinRule && bad.suggestion === "add_guard") {
262
+ rules.push({
263
+ pattern: builtinRule.pattern,
264
+ mode: builtinRule.mode,
265
+ action: "guard",
266
+ builtin: false,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ const nonBuiltinSkips = rules.filter((r) => r.action === "skip" && !r.builtin);
275
+ if (nonBuiltinSkips.length > MAX_SKIP_RULES) {
276
+ const toRemove = nonBuiltinSkips.slice(0, nonBuiltinSkips.length - MAX_SKIP_RULES);
277
+ for (const r of toRemove) {
278
+ const idx = rules.indexOf(r);
279
+ if (idx !== -1) rules.splice(idx, 1);
280
+ }
281
+ }
282
+
283
+ const nonBuiltinGuards = rules.filter((r) => r.action === "guard" && !r.builtin);
284
+ if (nonBuiltinGuards.length > MAX_GUARD_RULES) {
285
+ const toRemove = nonBuiltinGuards.slice(0, nonBuiltinGuards.length - MAX_GUARD_RULES);
286
+ for (const r of toRemove) {
287
+ const idx = rules.indexOf(r);
288
+ if (idx !== -1) rules.splice(idx, 1);
289
+ }
290
+ }
291
+
292
+ return {
293
+ ...store,
294
+ rules,
295
+ lastPurifyTimestamp: Date.now(),
296
+ };
297
+ }
@@ -0,0 +1,208 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { isAbsolute, join, normalize, resolve } from "node:path";
6
+
7
+ export const ENTRYPOINT_NAME = "MEMORY.md";
8
+ export const MAX_ENTRYPOINT_LINES = 200;
9
+ export const MAX_ENTRYPOINT_BYTES = 25_000;
10
+ export const MAX_MEMORY_FILES = 200;
11
+ export const MAX_RELEVANT_MEMORIES = 5;
12
+ export const MAX_MEMORY_BYTES_PER_FILE = 8000;
13
+ export const DREAM_MIN_HOURS = 24;
14
+ export const DREAM_MIN_SESSIONS = 5;
15
+
16
+ export type MemoryType = "user" | "feedback" | "project" | "reference" | "bookmark";
17
+
18
+ export interface MemoryHeader {
19
+ filename: string;
20
+ filePath: string;
21
+ mtimeMs: number;
22
+ description: string | null;
23
+ type: MemoryType | undefined;
24
+ sourceSession?: string | null;
25
+ tags?: string[] | null;
26
+ }
27
+
28
+ export function getProjectRoot(cwd: string): string {
29
+ if (!existsSync(join(cwd, ".git"))) {
30
+ return cwd;
31
+ }
32
+ try {
33
+ const result = execSync("git rev-parse --git-common-dir", {
34
+ cwd,
35
+ encoding: "utf-8",
36
+ stdio: ["pipe", "pipe", "pipe"],
37
+ }).trim();
38
+ if (result && isAbsolute(result)) {
39
+ return realpathSync(resolve(result, ".."));
40
+ }
41
+ if (result) {
42
+ return realpathSync(resolve(cwd, result, ".."));
43
+ }
44
+ } catch (err) {
45
+ console.debug("[auto-memory] git rev-parse failed:", err instanceof Error ? err.message : err);
46
+ }
47
+ return cwd;
48
+ }
49
+
50
+ function encodeCwd(cwd: string): string {
51
+ let resolved = cwd;
52
+ try {
53
+ resolved = realpathSync(cwd);
54
+ } catch (err) {
55
+ console.debug("[auto-memory] realpathSync failed:", err instanceof Error ? err.message : err);
56
+ }
57
+ return `--${resolved.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
58
+ }
59
+
60
+ export function getMemoryDir(cwd: string): string {
61
+ const agentDir = join(homedir(), ".pi", "agent");
62
+ return join(agentDir, "memory", encodeCwd(getProjectRoot(cwd)));
63
+ }
64
+
65
+ export function getEntrypointPath(cwd: string): string {
66
+ return join(getMemoryDir(cwd), ENTRYPOINT_NAME);
67
+ }
68
+
69
+ export function getSubagentDir(cwd: string): string {
70
+ return join(getMemoryDir(cwd), "..", "subagent");
71
+ }
72
+
73
+ export function isMemoryPath(absolutePath: string, cwd: string): boolean {
74
+ const memoryDir = normalize(getMemoryDir(cwd));
75
+ const normalized = normalize(absolutePath);
76
+ return normalized.startsWith(`${memoryDir}/`) || normalized.startsWith(`${memoryDir}\\`);
77
+ }
78
+
79
+ export function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
80
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "");
81
+
82
+ if (!normalized.startsWith("---")) {
83
+ return { frontmatter: {}, body: normalized };
84
+ }
85
+
86
+ const endIndex = normalized.indexOf("\n---", 3);
87
+ if (endIndex === -1) {
88
+ return { frontmatter: {}, body: normalized };
89
+ }
90
+
91
+ const yamlString = normalized.slice(4, endIndex);
92
+ const body = normalized.slice(endIndex + 4).trim();
93
+
94
+ const frontmatter: Record<string, string> = {};
95
+ for (const line of yamlString.split("\n")) {
96
+ const colonIndex = line.indexOf(":");
97
+ if (colonIndex === -1) continue;
98
+ const key = line.slice(0, colonIndex).trim();
99
+ const value = line.slice(colonIndex + 1).trim();
100
+ if (key) {
101
+ frontmatter[key] = value;
102
+ }
103
+ }
104
+
105
+ return { frontmatter, body };
106
+ }
107
+
108
+ export function isBookmarkType(header: MemoryHeader): boolean {
109
+ return header.type === "bookmark";
110
+ }
111
+
112
+ export function buildFrontmatter(fields: { name: string; description: string; type: MemoryType }): string {
113
+ return `---\nname: ${fields.name}\ndescription: ${fields.description}\ntype: ${fields.type}\n---`;
114
+ }
115
+
116
+ export interface BookmarkFrontmatterFields {
117
+ name: string;
118
+ description: string;
119
+ sourceSession: string;
120
+ sourceMessageIds: string[];
121
+ tags: string[];
122
+ createdAt: string;
123
+ }
124
+
125
+ export function buildBookmarkFrontmatter(fields: BookmarkFrontmatterFields): string {
126
+ const tagsStr = fields.tags.join(", ");
127
+ return `---\nname: ${fields.name}\ndescription: ${fields.description}\ntype: bookmark\nsourceSession: ${fields.sourceSession}\nsourceMessageIds: ${fields.sourceMessageIds.join(", ")}\ntags: [${tagsStr}]\ncreatedAt: ${fields.createdAt}\n---`;
128
+ }
129
+
130
+ export function truncateEntrypoint(raw: string): { content: string; wasTruncated: boolean } {
131
+ if (!raw) {
132
+ return { content: "", wasTruncated: false };
133
+ }
134
+
135
+ let lines = raw.split("\n");
136
+ let truncated = false;
137
+
138
+ if (lines.length > MAX_ENTRYPOINT_LINES) {
139
+ lines = lines.slice(0, MAX_ENTRYPOINT_LINES);
140
+ truncated = true;
141
+ }
142
+
143
+ let content = lines.join("\n");
144
+
145
+ if (Buffer.byteLength(content, "utf-8") > MAX_ENTRYPOINT_BYTES) {
146
+ truncated = true;
147
+ const bytes = Buffer.from(content, "utf-8");
148
+ content = bytes.slice(0, MAX_ENTRYPOINT_BYTES).toString("utf-8");
149
+ const lastNewline = content.lastIndexOf("\n");
150
+ if (lastNewline !== -1) {
151
+ content = content.slice(0, lastNewline);
152
+ }
153
+ }
154
+
155
+ return { content, wasTruncated: truncated };
156
+ }
157
+
158
+ export async function scanMemoryFiles(memoryDir: string): Promise<MemoryHeader[]> {
159
+ if (!existsSync(memoryDir)) {
160
+ return [];
161
+ }
162
+
163
+ const entries = readdirSync(memoryDir);
164
+ const headers: MemoryHeader[] = [];
165
+
166
+ for (const entry of entries) {
167
+ if (entry.startsWith(".")) continue;
168
+ if (entry === ENTRYPOINT_NAME) continue;
169
+ if (!entry.endsWith(".md")) continue;
170
+
171
+ const filePath = join(memoryDir, entry);
172
+ const stat = statSync(filePath);
173
+ if (!stat.isFile()) continue;
174
+
175
+ const content = await readFile(filePath, "utf-8");
176
+ const { frontmatter } = parseFrontmatter(content);
177
+
178
+ headers.push({
179
+ filename: entry,
180
+ filePath,
181
+ mtimeMs: stat.mtimeMs,
182
+ description: frontmatter.description ?? null,
183
+ type: frontmatter.type as MemoryType | undefined,
184
+ sourceSession: frontmatter.sourceSession ?? null,
185
+ tags: frontmatter.tags
186
+ ? frontmatter.tags
187
+ .replace(/^\[|\]$/g, "")
188
+ .split(",")
189
+ .map((t) => t.trim())
190
+ .filter(Boolean)
191
+ : null,
192
+ });
193
+ }
194
+
195
+ headers.sort((a, b) => b.mtimeMs - a.mtimeMs);
196
+ return headers;
197
+ }
198
+
199
+ export function formatManifest(headers: MemoryHeader[]): string {
200
+ return headers
201
+ .map((h) => {
202
+ const parts = [h.filename];
203
+ if (h.description) parts.push(h.description);
204
+ if (h.type) parts.push(`[${h.type}]`);
205
+ return parts.join(" — ");
206
+ })
207
+ .join("\n");
208
+ }