thatgfsj-code 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +279 -0
  2. package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +564 -0
  3. package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +194 -0
  4. package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +24 -0
  5. package/LICENSE +21 -0
  6. package/README.md +100 -0
  7. package/ROADMAP.md +107 -0
  8. package/dist/agent/core.d.ts +56 -0
  9. package/dist/agent/core.d.ts.map +1 -0
  10. package/dist/agent/core.js +142 -0
  11. package/dist/agent/core.js.map +1 -0
  12. package/dist/agent/index.d.ts +8 -0
  13. package/dist/agent/index.d.ts.map +1 -0
  14. package/dist/agent/index.js +8 -0
  15. package/dist/agent/index.js.map +1 -0
  16. package/dist/agent/intent.d.ts +36 -0
  17. package/dist/agent/intent.d.ts.map +1 -0
  18. package/dist/agent/intent.js +146 -0
  19. package/dist/agent/intent.js.map +1 -0
  20. package/dist/agent/streaming.d.ts +50 -0
  21. package/dist/agent/streaming.d.ts.map +1 -0
  22. package/dist/agent/streaming.js +110 -0
  23. package/dist/agent/streaming.js.map +1 -0
  24. package/dist/core/ai-engine.d.ts +59 -0
  25. package/dist/core/ai-engine.d.ts.map +1 -0
  26. package/dist/core/ai-engine.js +378 -0
  27. package/dist/core/ai-engine.js.map +1 -0
  28. package/dist/core/cli.d.ts +21 -0
  29. package/dist/core/cli.d.ts.map +1 -0
  30. package/dist/core/cli.js +143 -0
  31. package/dist/core/cli.js.map +1 -0
  32. package/dist/core/config.d.ts +38 -0
  33. package/dist/core/config.d.ts.map +1 -0
  34. package/dist/core/config.js +129 -0
  35. package/dist/core/config.js.map +1 -0
  36. package/dist/core/context-compactor.d.ts +54 -0
  37. package/dist/core/context-compactor.d.ts.map +1 -0
  38. package/dist/core/context-compactor.js +197 -0
  39. package/dist/core/context-compactor.js.map +1 -0
  40. package/dist/core/hooks.d.ts +75 -0
  41. package/dist/core/hooks.d.ts.map +1 -0
  42. package/dist/core/hooks.js +146 -0
  43. package/dist/core/hooks.js.map +1 -0
  44. package/dist/core/permissions.d.ts +52 -0
  45. package/dist/core/permissions.d.ts.map +1 -0
  46. package/dist/core/permissions.js +237 -0
  47. package/dist/core/permissions.js.map +1 -0
  48. package/dist/core/session.d.ts +54 -0
  49. package/dist/core/session.d.ts.map +1 -0
  50. package/dist/core/session.js +147 -0
  51. package/dist/core/session.js.map +1 -0
  52. package/dist/core/skills.d.ts +60 -0
  53. package/dist/core/skills.d.ts.map +1 -0
  54. package/dist/core/skills.js +175 -0
  55. package/dist/core/skills.js.map +1 -0
  56. package/dist/core/state.d.ts +60 -0
  57. package/dist/core/state.d.ts.map +1 -0
  58. package/dist/core/state.js +106 -0
  59. package/dist/core/state.js.map +1 -0
  60. package/dist/core/subagent.d.ts +68 -0
  61. package/dist/core/subagent.d.ts.map +1 -0
  62. package/dist/core/subagent.js +142 -0
  63. package/dist/core/subagent.js.map +1 -0
  64. package/dist/core/system-prompt.d.ts +40 -0
  65. package/dist/core/system-prompt.d.ts.map +1 -0
  66. package/dist/core/system-prompt.js +138 -0
  67. package/dist/core/system-prompt.js.map +1 -0
  68. package/dist/core/tool-registry.d.ts +53 -0
  69. package/dist/core/tool-registry.d.ts.map +1 -0
  70. package/dist/core/tool-registry.js +138 -0
  71. package/dist/core/tool-registry.js.map +1 -0
  72. package/dist/core/types.d.ts +206 -0
  73. package/dist/core/types.d.ts.map +1 -0
  74. package/dist/core/types.js +127 -0
  75. package/dist/core/types.js.map +1 -0
  76. package/dist/index.d.ts +7 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +481 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/mcp/client.d.ts +83 -0
  81. package/dist/mcp/client.d.ts.map +1 -0
  82. package/dist/mcp/client.js +267 -0
  83. package/dist/mcp/client.js.map +1 -0
  84. package/dist/repl/index.d.ts +8 -0
  85. package/dist/repl/index.d.ts.map +1 -0
  86. package/dist/repl/index.js +8 -0
  87. package/dist/repl/index.js.map +1 -0
  88. package/dist/repl/input.d.ts +61 -0
  89. package/dist/repl/input.d.ts.map +1 -0
  90. package/dist/repl/input.js +127 -0
  91. package/dist/repl/input.js.map +1 -0
  92. package/dist/repl/loop.d.ts +53 -0
  93. package/dist/repl/loop.d.ts.map +1 -0
  94. package/dist/repl/loop.js +241 -0
  95. package/dist/repl/loop.js.map +1 -0
  96. package/dist/repl/output.d.ts +105 -0
  97. package/dist/repl/output.d.ts.map +1 -0
  98. package/dist/repl/output.js +197 -0
  99. package/dist/repl/output.js.map +1 -0
  100. package/dist/repl/welcome.d.ts +68 -0
  101. package/dist/repl/welcome.d.ts.map +1 -0
  102. package/dist/repl/welcome.js +249 -0
  103. package/dist/repl/welcome.js.map +1 -0
  104. package/dist/tools/file.d.ts +46 -0
  105. package/dist/tools/file.d.ts.map +1 -0
  106. package/dist/tools/file.js +100 -0
  107. package/dist/tools/file.js.map +1 -0
  108. package/dist/tools/git.d.ts +52 -0
  109. package/dist/tools/git.d.ts.map +1 -0
  110. package/dist/tools/git.js +112 -0
  111. package/dist/tools/git.js.map +1 -0
  112. package/dist/tools/index.d.ts +22 -0
  113. package/dist/tools/index.d.ts.map +1 -0
  114. package/dist/tools/index.js +43 -0
  115. package/dist/tools/index.js.map +1 -0
  116. package/dist/tools/search.d.ts +36 -0
  117. package/dist/tools/search.d.ts.map +1 -0
  118. package/dist/tools/search.js +223 -0
  119. package/dist/tools/search.js.map +1 -0
  120. package/dist/tools/shell.d.ts +57 -0
  121. package/dist/tools/shell.d.ts.map +1 -0
  122. package/dist/tools/shell.js +153 -0
  123. package/dist/tools/shell.js.map +1 -0
  124. package/dist/utils/diff-preview.d.ts +42 -0
  125. package/dist/utils/diff-preview.d.ts.map +1 -0
  126. package/dist/utils/diff-preview.js +174 -0
  127. package/dist/utils/diff-preview.js.map +1 -0
  128. package/dist/utils/index.d.ts +8 -0
  129. package/dist/utils/index.d.ts.map +1 -0
  130. package/dist/utils/index.js +8 -0
  131. package/dist/utils/index.js.map +1 -0
  132. package/dist/utils/memory.d.ts +81 -0
  133. package/dist/utils/memory.d.ts.map +1 -0
  134. package/dist/utils/memory.js +186 -0
  135. package/dist/utils/memory.js.map +1 -0
  136. package/dist/utils/project-context.d.ts +62 -0
  137. package/dist/utils/project-context.d.ts.map +1 -0
  138. package/dist/utils/project-context.js +197 -0
  139. package/dist/utils/project-context.js.map +1 -0
  140. package/docs/API_KEY_GUIDE.md +236 -0
  141. package/docs/FAQ.md +182 -0
  142. package/install.bat +63 -0
  143. package/install.ps1 +238 -0
  144. package/install.sh +113 -0
  145. package/package.json +36 -0
  146. package/src/agent/core.ts +179 -0
  147. package/src/agent/index.ts +8 -0
  148. package/src/agent/intent.ts +181 -0
  149. package/src/agent/streaming.ts +132 -0
  150. package/src/core/ai-engine.ts +437 -0
  151. package/src/core/cli.ts +171 -0
  152. package/src/core/config.ts +147 -0
  153. package/src/core/context-compactor.ts +245 -0
  154. package/src/core/hooks.ts +196 -0
  155. package/src/core/permissions.ts +308 -0
  156. package/src/core/session.ts +165 -0
  157. package/src/core/skills.ts +208 -0
  158. package/src/core/state.ts +120 -0
  159. package/src/core/subagent.ts +195 -0
  160. package/src/core/system-prompt.ts +163 -0
  161. package/src/core/tool-registry.ts +157 -0
  162. package/src/core/types.ts +280 -0
  163. package/src/index.ts +544 -0
  164. package/src/mcp/client.ts +330 -0
  165. package/src/repl/index.ts +8 -0
  166. package/src/repl/input.ts +139 -0
  167. package/src/repl/loop.ts +280 -0
  168. package/src/repl/output.ts +222 -0
  169. package/src/repl/welcome.ts +296 -0
  170. package/src/tools/file.ts +117 -0
  171. package/src/tools/git.ts +132 -0
  172. package/src/tools/index.ts +48 -0
  173. package/src/tools/search.ts +263 -0
  174. package/src/tools/shell.ts +181 -0
  175. package/src/utils/diff-preview.ts +202 -0
  176. package/src/utils/index.ts +8 -0
  177. package/src/utils/memory.ts +223 -0
  178. package/src/utils/project-context.ts +207 -0
  179. package/tsconfig.json +19 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * S03: Permissions System
3
+ * 6-stage permission pipeline for tool execution
4
+ * Core principle: permissions are first-class citizens, not an afterthought
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import { ToolContext, ToolMetadata } from './types.js';
9
+
10
+ // ==================== Permission Types ====================
11
+
12
+ export type PermissionMode = 'accept' | 'deny' | 'ask';
13
+ export type PermissionLevel = 'none' | 'low' | 'medium' | 'high' | 'critical';
14
+
15
+ export interface PermissionResult {
16
+ allowed: boolean;
17
+ reason: string;
18
+ level: PermissionLevel;
19
+ requiresConfirmation: boolean;
20
+ stage: 1 | 2 | 3 | 4 | 5 | 6;
21
+ }
22
+
23
+ /**
24
+ * Permission rule — pattern-based allow/deny rules
25
+ */
26
+ export interface PermissionRule {
27
+ pattern: RegExp;
28
+ action: 'allow' | 'deny';
29
+ reason: string;
30
+ }
31
+
32
+ /**
33
+ * S03: Permission Checker — 6-stage pipeline
34
+ */
35
+ export class PermissionChecker {
36
+ private mode: PermissionMode = 'ask';
37
+ private rules: PermissionRule[] = [];
38
+ private workspaceRoot: string;
39
+
40
+ // Stage 1: Blocked patterns
41
+ private static BLOCKED_PATTERNS = [
42
+ /^rm\s+-rf\s+\//i,
43
+ /^rm\s+-rf\s+\$HOME/i,
44
+ /^rm\s+-rf\s+%USERPROFILE%/i,
45
+ /^del\s+\/f\s+\/s\s+\/q/i,
46
+ /^format\s+[a-z]:/i,
47
+ /^mkfs/i,
48
+ /^dd\s+if=/i,
49
+ /^shred/i,
50
+ />\s*\/dev\/sda/i,
51
+ /^curl\s+.*\||^wget\s+.*\|/i,
52
+ /^eval\s+/i,
53
+ /^exec\s+/i,
54
+ /base64\s+-d\s+.*\|/i,
55
+ /\$\(.*\)/i,
56
+ /`.*`/i,
57
+ ];
58
+
59
+ // Stage 2: Commands needing confirmation
60
+ private static CONFIRM_REQUIRED = [
61
+ 'rm -rf',
62
+ 'rmdir',
63
+ 'del /s /q',
64
+ 'format',
65
+ 'mkfs',
66
+ 'dd',
67
+ 'shutdown',
68
+ 'init 0',
69
+ 'init 6',
70
+ 'poweroff',
71
+ 'reboot',
72
+ 'pkill',
73
+ 'killall',
74
+ 'mv .* /tmp',
75
+ 'chmod 777',
76
+ 'chmod -R 777',
77
+ 'git push --force',
78
+ 'git push -f',
79
+ ];
80
+
81
+ // Stage 4: AI borderline commands (use LLM to classify)
82
+ private static BORDERLINE_PATTERNS = [
83
+ /curl\s+https?:\/\//i,
84
+ /wget\s+/i,
85
+ /npm\s+install\s+-g/i,
86
+ /pip\s+install\s+/i,
87
+ /yarn\s+add\s+/i,
88
+ /sudo\s+/i,
89
+ /chown\s+/i,
90
+ /chmod\s+[67]/i,
91
+ /^git\s+reset/i,
92
+ /^git\s+clean/i,
93
+ ];
94
+
95
+ constructor(workspaceRoot: string = process.cwd()) {
96
+ this.workspaceRoot = workspaceRoot;
97
+ }
98
+
99
+ /**
100
+ * S03: 6-Stage Permission Pipeline
101
+ * Every tool execution passes through all 6 stages
102
+ */
103
+ async check(
104
+ toolName: string,
105
+ params: Record<string, any>,
106
+ ctx: ToolContext
107
+ ): Promise<PermissionResult> {
108
+
109
+ // Stage 1: Pattern block — reject known dangerous commands immediately
110
+ const stage1 = this.stage1PatternBlock(toolName, params);
111
+ if (!stage1.allowed) return stage1;
112
+
113
+ // Stage 2: Permission rules — check user-defined allow/deny rules
114
+ const stage2 = this.stage2Rules(toolName, params);
115
+ if (!stage2.allowed) return stage2;
116
+
117
+ // Stage 3: Path sandboxing — prevent workspace escape
118
+ const stage3 = this.stage3PathSandbox(toolName, params);
119
+ if (!stage3.allowed) return stage3;
120
+
121
+ // Stage 4: AI classification for borderline cases
122
+ const stage4 = await this.stage4AIClassify(toolName, params, ctx);
123
+ if (!stage4.allowed) return stage4;
124
+
125
+ // Stage 5: User confirmation for medium/high risk
126
+ const stage5 = this.stage5Confirmation(toolName, params, ctx);
127
+ if (stage5.requiresConfirmation) {
128
+ return stage5;
129
+ }
130
+
131
+ // Stage 6: Pass through
132
+ return {
133
+ allowed: true,
134
+ reason: 'Allowed',
135
+ level: stage5.level,
136
+ requiresConfirmation: false,
137
+ stage: 6
138
+ };
139
+ }
140
+
141
+ // Stage 1: Pattern block
142
+ private stage1PatternBlock(toolName: string, params: Record<string, any>): PermissionResult {
143
+ if (toolName !== 'shell') return { allowed: true, reason: 'Stage 1: N/A', level: 'none', requiresConfirmation: false, stage: 1 };
144
+
145
+ const command = params.command || '';
146
+ for (const pattern of PermissionChecker.BLOCKED_PATTERNS) {
147
+ if (pattern.test(command.trim())) {
148
+ return {
149
+ allowed: false,
150
+ reason: `Blocked: command matches dangerous pattern ${pattern}`,
151
+ level: 'critical',
152
+ requiresConfirmation: false,
153
+ stage: 1
154
+ };
155
+ }
156
+ }
157
+ return { allowed: true, reason: 'Stage 1: Passed pattern check', level: 'none', requiresConfirmation: false, stage: 1 };
158
+ }
159
+
160
+ // Stage 2: User-defined rules
161
+ private stage2Rules(toolName: string, params: Record<string, any>): PermissionResult {
162
+ const command = params.command || '';
163
+
164
+ for (const rule of this.rules) {
165
+ if (rule.pattern.test(command)) {
166
+ return {
167
+ allowed: rule.action === 'allow',
168
+ reason: `Rule matched: ${rule.reason}`,
169
+ level: rule.action === 'deny' ? 'high' : 'none',
170
+ requiresConfirmation: false,
171
+ stage: 2
172
+ };
173
+ }
174
+ }
175
+ return { allowed: true, reason: 'Stage 2: No rules matched', level: 'none', requiresConfirmation: false, stage: 2 };
176
+ }
177
+
178
+ // Stage 3: Path sandboxing
179
+ private stage3PathSandbox(toolName: string, params: Record<string, any>): PermissionResult {
180
+ if (toolName !== 'shell' && toolName !== 'file') return { allowed: true, reason: 'Stage 3: N/A', level: 'none', requiresConfirmation: false, stage: 3 };
181
+
182
+ const path = params.path || params.command || '';
183
+
184
+ // Check for path traversal attempts
185
+ if (/^\.\.\//.test(path) || /\%00/.test(path) || /\0/.test(path)) {
186
+ return {
187
+ allowed: false,
188
+ reason: 'Blocked: path traversal attempt detected',
189
+ level: 'high',
190
+ requiresConfirmation: false,
191
+ stage: 3
192
+ };
193
+ }
194
+
195
+ return { allowed: true, reason: 'Stage 3: Path sandbox passed', level: 'none', requiresConfirmation: false, stage: 3 };
196
+ }
197
+
198
+ // Stage 4: AI classification for borderline commands
199
+ private async stage4AIClassify(
200
+ toolName: string,
201
+ params: Record<string, any>,
202
+ ctx: ToolContext
203
+ ): Promise<PermissionResult> {
204
+ if (toolName !== 'shell') return { allowed: true, reason: 'Stage 4: N/A', level: 'none', requiresConfirmation: false, stage: 4 };
205
+
206
+ const command = params.command || '';
207
+
208
+ // Check if borderline
209
+ const isBorderline = PermissionChecker.BORDERLINE_PATTERNS.some(p => p.test(command));
210
+ if (!isBorderline) {
211
+ return { allowed: true, reason: 'Stage 4: Not borderline', level: 'none', requiresConfirmation: false, stage: 4 };
212
+ }
213
+
214
+ // For CLI, mark as medium risk (actual AI classification skipped for perf)
215
+ return {
216
+ allowed: true,
217
+ reason: 'Stage 4: Borderline — escalated to confirmation',
218
+ level: 'medium',
219
+ requiresConfirmation: false,
220
+ stage: 4
221
+ };
222
+ }
223
+
224
+ // Stage 5: User confirmation
225
+ private stage5Confirmation(
226
+ toolName: string,
227
+ params: Record<string, any>,
228
+ ctx: ToolContext
229
+ ): PermissionResult {
230
+ if (this.mode === 'accept') {
231
+ return { allowed: true, reason: 'Mode: accept all', level: 'none', requiresConfirmation: false, stage: 5 };
232
+ }
233
+
234
+ if (this.mode === 'deny') {
235
+ return { allowed: false, reason: 'Mode: deny all', level: 'high', requiresConfirmation: false, stage: 5 };
236
+ }
237
+
238
+ // mode === 'ask'
239
+ const command = params.command || '';
240
+
241
+ // Critical: auto-deny
242
+ if (/rm\s+-rf/.test(command) && !command.includes('node_modules')) {
243
+ return {
244
+ allowed: false,
245
+ reason: 'Auto-deny: rm -rf without node_modules is too risky',
246
+ level: 'critical',
247
+ requiresConfirmation: false,
248
+ stage: 5
249
+ };
250
+ }
251
+
252
+ // Check confirm required list
253
+ const needsConfirm = PermissionChecker.CONFIRM_REQUIRED.some(cmd =>
254
+ command.toLowerCase().includes(cmd.toLowerCase())
255
+ );
256
+
257
+ if (needsConfirm) {
258
+ return {
259
+ allowed: true,
260
+ reason: 'Requires user confirmation',
261
+ level: 'medium',
262
+ requiresConfirmation: true,
263
+ stage: 5
264
+ };
265
+ }
266
+
267
+ return { allowed: true, reason: 'Stage 5: No confirmation needed', level: 'low', requiresConfirmation: false, stage: 5 };
268
+ }
269
+
270
+ // ==================== Config Methods ====================
271
+
272
+ setMode(mode: PermissionMode) {
273
+ this.mode = mode;
274
+ }
275
+
276
+ getMode(): PermissionMode {
277
+ return this.mode;
278
+ }
279
+
280
+ addRule(pattern: RegExp, action: 'allow' | 'deny', reason: string) {
281
+ this.rules.push({ pattern, action, reason });
282
+ }
283
+
284
+ clearRules() {
285
+ this.rules = [];
286
+ }
287
+
288
+ setWorkspaceRoot(path: string) {
289
+ this.workspaceRoot = path;
290
+ }
291
+ }
292
+
293
+ // ==================== Permission Mode CLI ====================
294
+
295
+ export function printPermissionResult(result: PermissionResult, command?: string) {
296
+ if (!result.allowed) {
297
+ console.log(chalk.red(`\n🚫 BLOCKED [Stage ${result.stage}]: ${result.reason}`));
298
+ return;
299
+ }
300
+
301
+ if (result.requiresConfirmation && command) {
302
+ console.log(chalk.yellow(`\n⚠️ Requires confirmation [Stage ${result.stage}]: ${result.reason}`));
303
+ console.log(chalk.gray(` Command: ${command}`));
304
+ return;
305
+ }
306
+
307
+ console.log(chalk.green(`\n✅ Allowed [Stage ${result.stage}]: ${result.reason}`));
308
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Session Manager - Manages conversation history
3
+ *
4
+ * F4 (anti-[已中断]):
5
+ * - addMessage 拦截含"已中断" / 链形 think 块等污染字符串
6
+ * (之前 AI 在失败重试时会把这些字面写进 history 造成病毒循环)
7
+ * - sanitize() 在 lastMessages 输出到 LLM 前再做一次去噪
8
+ */
9
+
10
+ import { ChatMessage, Session } from './types.js';
11
+
12
+ /**
13
+ * Patterns that indicate the message is polluted by a previous broken
14
+ * tool/stream retry loop. These should never be re-sent to the LLM.
15
+ */
16
+ const POLLUTION_PATTERNS: RegExp[] = [
17
+ /\[已中断/, // literally "[已中断" - sentinel from bad run
18
+ /\u5df2\u4e2d\u65ad/, // 已中断 (escaped form)
19
+ /^\s*\[已中断\s*$/m, // line that is exactly [已中断
20
+ /^<think>[\s\S]*?<\/think>\s*$/m, // entire message that is just a think block
21
+ /\u{1F6AB}/u, // ⛔ emoji sometimes used as interrupt marker
22
+ ];
23
+
24
+ function looksPolluted(content: string): boolean {
25
+ if (!content) return false;
26
+ // Strip obvious thinking fences and check the *remaining* core content.
27
+ const stripped = content
28
+ .replace(/<think>[\s\S]*?<\/think>/g, '')
29
+ .replace(/<\/?think>/g, '')
30
+ .trim();
31
+
32
+ for (const p of POLLUTION_PATTERNS) {
33
+ if (p.test(content)) return true;
34
+ }
35
+
36
+ // A message whose entire stripped form is shorter than 6 chars AND
37
+ // contains an interruption-related token is also suspect.
38
+ if (stripped.length < 6 && /(中断|interrupted|cancelled|已取消)/i.test(stripped)) {
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ export class SessionManager {
45
+ private messages: ChatMessage[] = [];
46
+ private sessionId: string;
47
+ private createdAt: Date;
48
+ private droppedCount = 0;
49
+
50
+ constructor() {
51
+ this.sessionId = this.generateId();
52
+ this.createdAt = new Date();
53
+ }
54
+
55
+ /**
56
+ * Add a message to the session.
57
+ * Returns true if accepted, false if dropped because it looked polluted.
58
+ */
59
+ addMessage(role: ChatMessage['role'], content: string): boolean {
60
+ if (role !== 'system' && looksPolluted(content)) {
61
+ this.droppedCount++;
62
+ // Keep a 1-line internal note so the user can see *something* happened,
63
+ // but the original polluted text does NOT go to the LLM context.
64
+ this.messages.push({
65
+ role: 'user',
66
+ content: '[system: dropped a polluted prior message containing "已中断" markers. Continue with the original task.]',
67
+ name: 'system'
68
+ });
69
+ return false;
70
+ }
71
+
72
+ this.messages.push({
73
+ role,
74
+ content,
75
+ name: role === 'user' ? 'user' : undefined
76
+ });
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * Add a message WITHOUT any sanitization (for trusted callers, e.g. system prompt)
82
+ */
83
+ addMessageRaw(role: ChatMessage['role'], content: string): void {
84
+ this.messages.push({ role, content });
85
+ }
86
+
87
+ /**
88
+ * How many polluted messages have we filtered out? Useful for debugging
89
+ * the "AI keeps saying interrupted" loop.
90
+ */
91
+ getDroppedCount(): number {
92
+ return this.droppedCount;
93
+ }
94
+
95
+ /**
96
+ * Return messages ready to send to the LLM. We additionally dedupe
97
+ * adjacent identical assistant messages (a recovery hack against the
98
+ * "AI repeats the same paragraph each turn" loop).
99
+ */
100
+ getMessages(): ChatMessage[] {
101
+ const out: ChatMessage[] = [];
102
+ for (let i = 0; i < this.messages.length; i++) {
103
+ const m = this.messages[i];
104
+ const prev = out[out.length - 1];
105
+ if (
106
+ prev &&
107
+ m.role === 'assistant' &&
108
+ prev.role === 'assistant' &&
109
+ m.content.length > 0 &&
110
+ m.content === prev.content
111
+ ) {
112
+ // collapse identical neighbour
113
+ continue;
114
+ }
115
+ out.push(m);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /**
121
+ * Get message count (raw, includes internal notes)
122
+ */
123
+ getMessageCount(): number {
124
+ return this.messages.length;
125
+ }
126
+
127
+ /**
128
+ * Clear session history
129
+ */
130
+ clear(): void {
131
+ this.messages = [];
132
+ this.droppedCount = 0;
133
+ }
134
+
135
+ /**
136
+ * Get session info
137
+ */
138
+ getSession(): Session {
139
+ return {
140
+ id: this.sessionId,
141
+ messages: this.messages,
142
+ createdAt: this.createdAt,
143
+ lastActiveAt: new Date()
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Truncate messages to fit token limit
149
+ */
150
+ truncate(maxMessages: number = 20): void {
151
+ if (this.messages.length > maxMessages) {
152
+ const systemMsg = this.messages.find(m => m.role === 'system');
153
+ const otherMsgs = this.messages.filter(m => m.role !== 'system');
154
+
155
+ this.messages = [
156
+ ...(systemMsg ? [systemMsg] : []),
157
+ ...otherMsgs.slice(-(maxMessages - 1))
158
+ ];
159
+ }
160
+ }
161
+
162
+ private generateId(): string {
163
+ return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
164
+ }
165
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * S10: Skills System
3
+ * Load skills from CLAUDE.md files and expose them as available commands
4
+ *
5
+ * Skills are domain-specific knowledge packages that extend agent capabilities.
6
+ */
7
+
8
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+
11
+ export interface Skill {
12
+ name: string;
13
+ description: string;
14
+ prompt: string;
15
+ source: string; // File path or 'builtin'
16
+ tags: string[];
17
+ }
18
+
19
+ export interface SkillsCatalog {
20
+ skills: Skill[];
21
+ loadedAt: Date;
22
+ sources: string[];
23
+ }
24
+
25
+ /**
26
+ * S10: Skills Loader — discovers and loads skills from filesystem
27
+ */
28
+ export class SkillsLoader {
29
+ private skills: Map<string, Skill> = new Map();
30
+ private sources: Set<string> = new Set();
31
+
32
+ /**
33
+ * S10: Load skills from a directory
34
+ * Looks for .md files with # SkillName header
35
+ */
36
+ loadFromDir(dirPath: string): number {
37
+ if (!existsSync(dirPath)) return 0;
38
+
39
+ let count = 0;
40
+ const files = readdirSync(dirPath);
41
+
42
+ for (const file of files) {
43
+ const fullPath = join(dirPath, file);
44
+ const stat = statSync(fullPath);
45
+
46
+ if (stat.isDirectory()) {
47
+ count += this.loadFromDir(fullPath);
48
+ continue;
49
+ }
50
+
51
+ if (file.endsWith('.md') || file.endsWith('.skill')) {
52
+ const loaded = this.loadFromFile(fullPath);
53
+ if (loaded) count++;
54
+ }
55
+ }
56
+
57
+ return count;
58
+ }
59
+
60
+ /**
61
+ * S10: Load a single skill file
62
+ */
63
+ loadFromFile(filePath: string): boolean {
64
+ try {
65
+ const content = readFileSync(filePath, 'utf-8');
66
+ const skill = this.parseSkillFile(content, filePath);
67
+ if (skill) {
68
+ this.skills.set(skill.name, skill);
69
+ this.sources.add(filePath);
70
+ return true;
71
+ }
72
+ } catch (error) {
73
+ // Ignore errors
74
+ }
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * S10: Parse a skill file — extract name, description, prompt from markdown
80
+ */
81
+ private parseSkillFile(content: string, source: string): Skill | null {
82
+ const lines = content.split('\n');
83
+
84
+ // Find skill name from first heading
85
+ let name = '';
86
+ let description = '';
87
+ let promptLines: string[] = [];
88
+ let inPrompt = false;
89
+ let tags: string[] = [];
90
+
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i];
93
+
94
+ // Match # SkillName or ## SkillName
95
+ if (/^#{1,2}\s+(\S+)/.test(line.trim())) {
96
+ if (!name) {
97
+ name = line.replace(/^#{1,2}\s+/, '').trim();
98
+ } else {
99
+ // Second heading — end of prompt
100
+ break;
101
+ }
102
+ continue;
103
+ }
104
+
105
+ // Description line (after name, before first code block or empty line)
106
+ if (name && !description && line.trim() && !line.startsWith('```')) {
107
+ description = line.trim();
108
+ }
109
+
110
+ // Code block — start of prompt template
111
+ if (line.startsWith('```')) {
112
+ if (!inPrompt) {
113
+ inPrompt = true;
114
+ continue;
115
+ } else {
116
+ // End of prompt
117
+ break;
118
+ }
119
+ }
120
+
121
+ if (inPrompt) {
122
+ promptLines.push(line);
123
+ }
124
+ }
125
+
126
+ if (!name) return null;
127
+
128
+ return {
129
+ name,
130
+ description: description || name,
131
+ prompt: promptLines.join('\n').trim(),
132
+ source,
133
+ tags: this.extractTags(content)
134
+ };
135
+ }
136
+
137
+ private extractTags(content: string): string[] {
138
+ const tagMatch = content.match(/tags?:\s*(.+)/i);
139
+ if (tagMatch) {
140
+ return tagMatch[1].split(/[,\s]+/).filter(Boolean);
141
+ }
142
+ return [];
143
+ }
144
+
145
+ /**
146
+ * S10: Get all skills
147
+ */
148
+ getAll(): Skill[] {
149
+ return [...this.skills.values()];
150
+ }
151
+
152
+ /**
153
+ * S10: Get skill by name
154
+ */
155
+ get(name: string): Skill | undefined {
156
+ return this.skills.get(name);
157
+ }
158
+
159
+ /**
160
+ * S10: Search skills by tag or name
161
+ */
162
+ search(query: string): Skill[] {
163
+ const q = query.toLowerCase();
164
+ return this.getAll().filter(s =>
165
+ s.name.toLowerCase().includes(q) ||
166
+ s.description.toLowerCase().includes(q) ||
167
+ s.tags.some(t => t.toLowerCase().includes(q))
168
+ );
169
+ }
170
+
171
+ /**
172
+ * S10: Get catalog
173
+ */
174
+ getCatalog(): SkillsCatalog {
175
+ return {
176
+ skills: this.getAll(),
177
+ loadedAt: new Date(),
178
+ sources: [...this.sources]
179
+ };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * S10: Built-in skills
185
+ */
186
+ export const BUILT_IN_SKILLS: Skill[] = [
187
+ {
188
+ name: 'explain-code',
189
+ description: 'Explain what a piece of code does in plain English',
190
+ prompt: 'Explain this code:\n\n```\n{{CODE}}\n```\n\nBe concise and clear.',
191
+ source: 'builtin',
192
+ tags: ['analysis', 'documentation']
193
+ },
194
+ {
195
+ name: 'review-code',
196
+ description: 'Review code for bugs, performance issues, and best practices',
197
+ prompt: 'Review this code:\n\n```\n{{CODE}}\n```\n\nFocus on: bugs, security, performance, readability.',
198
+ source: 'builtin',
199
+ tags: ['review', 'quality']
200
+ },
201
+ {
202
+ name: 'write-test',
203
+ description: 'Generate unit tests for the given code',
204
+ prompt: 'Write unit tests for:\n\n```\n{{CODE}}\n```\n\nUse a testing framework appropriate for the language.',
205
+ source: 'builtin',
206
+ tags: ['testing', 'quality']
207
+ }
208
+ ];