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
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 文件级检查点 — snapshot files before agents mutate them; /rewind restores.
3
+ *
4
+ * Every chat turn / task opens a checkpoint "turn". Before write_file /
5
+ * edit_file / delete_file executes, the target's current content (or its
6
+ * absence) is snapshotted — first touch per path per turn wins, so a rewind
7
+ * restores the state from *before* the turn began. Lets users hand agents
8
+ * risky changes and undo them in one command, without involving git.
9
+ *
10
+ * Deliberately session-scoped and in-memory (like Claude Code checkpoints):
11
+ * not a git replacement, and `run_bash` side effects cannot be rewound.
12
+ */
13
+
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { getLogger } from './logger';
17
+
18
+ const log = getLogger('checkpoint');
19
+
20
+ interface FileSnapshot {
21
+ /** Absolute path. */
22
+ path: string;
23
+ /** Content before the turn, or null if the file did not exist. */
24
+ content: string | null;
25
+ }
26
+
27
+ export interface CheckpointTurn {
28
+ id: number;
29
+ label: string;
30
+ at: Date;
31
+ snapshots: Map<string, FileSnapshot>;
32
+ }
33
+
34
+ const MAX_TURNS = 50;
35
+ const MAX_FILE_BYTES = 2 * 1024 * 1024; // skip snapshotting monsters
36
+ const MUTATING_TOOL_RE = /^(write_file|edit_file|delete_file)$/;
37
+
38
+ class FileCheckpointStore {
39
+ private turns: CheckpointTurn[] = [];
40
+ private current: CheckpointTurn | null = null;
41
+ private seq = 0;
42
+
43
+ /** Open a new turn; subsequent snapshots attach to it. */
44
+ beginTurn(label: string): void {
45
+ // An empty previous turn is replaced, not stacked.
46
+ if (this.current && this.current.snapshots.size === 0) {
47
+ this.turns.pop();
48
+ }
49
+ this.current = { id: ++this.seq, label: label.slice(0, 60), at: new Date(), snapshots: new Map() };
50
+ this.turns.push(this.current);
51
+ if (this.turns.length > MAX_TURNS) this.turns.shift();
52
+ }
53
+
54
+ /** Snapshot a path before mutation (first touch per turn wins). */
55
+ snapshot(rawPath: string): void {
56
+ if (!this.current) this.beginTurn('(implicit)');
57
+ const abs = path.resolve(rawPath);
58
+ if (this.current!.snapshots.has(abs)) return;
59
+ let content: string | null = null;
60
+ try {
61
+ if (fs.existsSync(abs)) {
62
+ const stat = fs.statSync(abs);
63
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
64
+ content = fs.readFileSync(abs, 'utf-8');
65
+ }
66
+ } catch (e) {
67
+ log.warn('snapshot_failed', { path: abs, error: String(e) });
68
+ return;
69
+ }
70
+ this.current!.snapshots.set(abs, { path: abs, content });
71
+ }
72
+
73
+ /** Should this tool call be snapshotted? Returns the path to snapshot. */
74
+ pathToSnapshot(toolName: string, args: Record<string, any>): string | null {
75
+ if (!MUTATING_TOOL_RE.test(toolName)) return null;
76
+ const p = args?.path;
77
+ return typeof p === 'string' && p.trim() ? p : null;
78
+ }
79
+
80
+ /** Turns that actually captured changes, newest first. */
81
+ list(): Array<{ id: number; label: string; at: Date; files: string[] }> {
82
+ return this.turns
83
+ .filter(t => t.snapshots.size > 0)
84
+ .map(t => ({ id: t.id, label: t.label, at: t.at, files: [...t.snapshots.keys()] }))
85
+ .reverse();
86
+ }
87
+
88
+ /**
89
+ * Restore the last `count` non-empty turns (newest backwards). When the
90
+ * same file appears in several turns, the oldest snapshot wins — that is
91
+ * the state from before the earliest rewound turn.
92
+ */
93
+ rewind(count: number = 1): { restored: string[]; deleted: string[]; turns: number } {
94
+ const nonEmpty = this.turns.filter(t => t.snapshots.size > 0);
95
+ const target = nonEmpty.slice(-count);
96
+ if (target.length === 0) return { restored: [], deleted: [], turns: 0 };
97
+
98
+ // oldest-first iteration: later assignments overwrite, so the oldest
99
+ // snapshot per path ends up in the map
100
+ const finalState = new Map<string, FileSnapshot>();
101
+ for (let i = target.length - 1; i >= 0; i--) {
102
+ for (const snap of target[i].snapshots.values()) finalState.set(snap.path, snap);
103
+ }
104
+
105
+ const restored: string[] = [];
106
+ const deleted: string[] = [];
107
+ for (const snap of finalState.values()) {
108
+ try {
109
+ if (snap.content === null) {
110
+ if (fs.existsSync(snap.path)) { fs.unlinkSync(snap.path); deleted.push(snap.path); }
111
+ } else {
112
+ fs.mkdirSync(path.dirname(snap.path), { recursive: true });
113
+ fs.writeFileSync(snap.path, snap.content, 'utf-8');
114
+ restored.push(snap.path);
115
+ }
116
+ } catch (e) {
117
+ log.warn('rewind_failed', { path: snap.path, error: String(e) });
118
+ }
119
+ }
120
+
121
+ // Rewound turns are consumed.
122
+ const ids = new Set(target.map(t => t.id));
123
+ this.turns = this.turns.filter(t => !ids.has(t.id));
124
+ if (this.current && ids.has(this.current.id)) this.current = null;
125
+ return { restored, deleted, turns: target.length };
126
+ }
127
+
128
+ /** Test/reset hook. */
129
+ clear(): void { this.turns = []; this.current = null; }
130
+ }
131
+
132
+ let _store: FileCheckpointStore | null = null;
133
+ export function getFileCheckpoints(): FileCheckpointStore {
134
+ if (!_store) _store = new FileCheckpointStore();
135
+ return _store;
136
+ }
@@ -1,103 +1,103 @@
1
- /**
2
- * 输出过滤模块 — sensitive information sanitization.
3
- *
4
- * Before agent responses reach the user (or are persisted),
5
- * scan for and redact sensitive patterns like API keys,
6
- * tokens, passwords, PII, and internal paths.
7
- */
8
-
9
- /* ═══════════════════════════════════════
10
- Detection patterns — compiled once at module load
11
- ═══════════════════════════════════════ */
12
- const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
13
- // API keys & tokens
14
- [/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
15
- [/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
16
- [/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
17
- [/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
18
-
19
- // AWS credentials
20
- [/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
21
- [/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
22
-
23
- // Passwords
24
- [/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
25
- [/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
26
-
27
- // Connection strings
28
- [/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
29
- [/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
30
-
31
- // Private keys
32
- [/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
33
-
34
- // IP addresses (local only)
35
- [/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
36
- [/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
37
- [/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
38
-
39
- // File paths
40
- [/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
41
- ];
42
-
43
- /* Email masking (function-based, handled separately) */
44
- const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
45
-
46
-
47
- /* ═══════════════════════════════════════
48
- Filter function
49
- ═══════════════════════════════════════ */
50
- export interface FilterResult {
51
- clean: string;
52
- redacted: boolean;
53
- count: number;
54
- details: string[];
55
- }
56
-
57
- export function filterOutput(text: string): FilterResult {
58
- if (!text) return { clean: "", redacted: false, count: 0, details: [] };
59
-
60
- let clean = text;
61
- let count = 0;
62
- const details: string[] = [];
63
-
64
- // Email masking (function-based replacement)
65
- let emailCount = 0;
66
- clean = clean.replace(EMAIL_RE, (full, user, domain) => {
67
- emailCount++;
68
- return (user as string).slice(0, 2) + "***@" + (domain as string);
69
- });
70
- if (emailCount > 0) {
71
- count += emailCount;
72
- details.push(`Masked ${emailCount}x email addresses`);
73
- }
74
-
75
- for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
76
- const matches = clean.match(pattern);
77
- if (matches) {
78
- count += matches.length;
79
- if (typeof replacement === "string") {
80
- details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
81
- } else {
82
- details.push(`Masked ${matches.length}x email addresses`);
83
- }
84
- clean = clean.replace(pattern, replacement as string);
85
- }
86
- }
87
-
88
- return { clean, redacted: count > 0, count, details };
89
- }
90
-
91
- /* ═══════════════════════════════════════
92
- Quick check — is filtering needed?
93
- ═══════════════════════════════════════ */
94
- export function needsFiltering(text: string): boolean {
95
- if (!text) return false;
96
- // Quick scan with the most common patterns
97
- if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
98
- if (/api_key.*[:=]/.test(text)) return true;
99
- if (/password.*[:=]/.test(text)) return true;
100
- if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
101
- if (EMAIL_RE.test(text)) return true;
102
- return false;
103
- }
1
+ /**
2
+ * 输出过滤模块 — sensitive information sanitization.
3
+ *
4
+ * Before agent responses reach the user (or are persisted),
5
+ * scan for and redact sensitive patterns like API keys,
6
+ * tokens, passwords, PII, and internal paths.
7
+ */
8
+
9
+ /* ═══════════════════════════════════════
10
+ Detection patterns — compiled once at module load
11
+ ═══════════════════════════════════════ */
12
+ const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
13
+ // API keys & tokens
14
+ [/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
15
+ [/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
16
+ [/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
17
+ [/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
18
+
19
+ // AWS credentials
20
+ [/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
21
+ [/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
22
+
23
+ // Passwords
24
+ [/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
25
+ [/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
26
+
27
+ // Connection strings
28
+ [/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
29
+ [/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
30
+
31
+ // Private keys
32
+ [/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
33
+
34
+ // IP addresses (local only)
35
+ [/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
36
+ [/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
37
+ [/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
38
+
39
+ // File paths
40
+ [/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
41
+ ];
42
+
43
+ /* Email masking (function-based, handled separately) */
44
+ const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
45
+
46
+
47
+ /* ═══════════════════════════════════════
48
+ Filter function
49
+ ═══════════════════════════════════════ */
50
+ export interface FilterResult {
51
+ clean: string;
52
+ redacted: boolean;
53
+ count: number;
54
+ details: string[];
55
+ }
56
+
57
+ export function filterOutput(text: string): FilterResult {
58
+ if (!text) return { clean: "", redacted: false, count: 0, details: [] };
59
+
60
+ let clean = text;
61
+ let count = 0;
62
+ const details: string[] = [];
63
+
64
+ // Email masking (function-based replacement)
65
+ let emailCount = 0;
66
+ clean = clean.replace(EMAIL_RE, (full, user, domain) => {
67
+ emailCount++;
68
+ return (user as string).slice(0, 2) + "***@" + (domain as string);
69
+ });
70
+ if (emailCount > 0) {
71
+ count += emailCount;
72
+ details.push(`Masked ${emailCount}x email addresses`);
73
+ }
74
+
75
+ for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
76
+ const matches = clean.match(pattern);
77
+ if (matches) {
78
+ count += matches.length;
79
+ if (typeof replacement === "string") {
80
+ details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
81
+ } else {
82
+ details.push(`Masked ${matches.length}x email addresses`);
83
+ }
84
+ clean = clean.replace(pattern, replacement as string);
85
+ }
86
+ }
87
+
88
+ return { clean, redacted: count > 0, count, details };
89
+ }
90
+
91
+ /* ═══════════════════════════════════════
92
+ Quick check — is filtering needed?
93
+ ═══════════════════════════════════════ */
94
+ export function needsFiltering(text: string): boolean {
95
+ if (!text) return false;
96
+ // Quick scan with the most common patterns
97
+ if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
98
+ if (/api_key.*[:=]/.test(text)) return true;
99
+ if (/password.*[:=]/.test(text)) return true;
100
+ if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
101
+ if (EMAIL_RE.test(text)) return true;
102
+ return false;
103
+ }
package/src/core/graph.ts CHANGED
@@ -1,156 +1,156 @@
1
- /**
2
- * 简易知识图谱 — entity-relation storage in SQLite.
3
- *
4
- * Lightweight triple store: (subject, predicate, object) with metadata.
5
- * Used for: project info, tool preferences, dependency relationships.
6
- *
7
- * Schema:
8
- * CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
9
- *
10
- * Queries:
11
- * - Find all relations for an entity
12
- * - Find all entities matching a predicate
13
- * - Transitive closure (2-hop max for performance)
14
- */
15
-
16
- import * as fs from "fs";
17
- import * as path from "path";
18
- import { USER_CONFIG_DIR } from "./config";
19
- import { getLogger } from "./logger";
20
-
21
- const log = getLogger("graph");
22
-
23
- /* ═══════════════════════════════════════
24
- Triple store — in-memory + optional persistence
25
- ═══════════════════════════════════════ */
26
- interface Triple {
27
- subj: string;
28
- pred: string;
29
- obj: string;
30
- agent: string;
31
- ts: string;
32
- meta?: Record<string, string>;
33
- }
34
-
35
- export class KnowledgeGraph {
36
- private triples: Triple[] = [];
37
- private indexPath: string;
38
-
39
- constructor(name: string = "default") {
40
- this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
41
- this.load();
42
- }
43
-
44
- /** Add a fact: (subject, predicate, object). */
45
- add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
46
- // Deduplicate
47
- const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
48
- if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
49
-
50
- this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
51
- if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
52
- this.save();
53
- }
54
-
55
- /** Find all facts about an entity. */
56
- about(entity: string, limit: number = 20): Triple[] {
57
- return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
58
- }
59
-
60
- /** Find all subjects matching a predicate. */
61
- byPredicate(pred: string): Triple[] {
62
- return this.triples.filter(t => t.pred === pred);
63
- }
64
-
65
- /** Find all objects for a subject-predicate pair. */
66
- find(subj: string, pred: string): Triple[] {
67
- return this.triples.filter(t => t.subj === subj && t.pred === pred);
68
- }
69
-
70
- /** Transitive expansion: 2-hop from a starting entity. */
71
- expand(entity: string, maxDepth: number = 2): Triple[] {
72
- const seen = new Set<Triple>();
73
- const queue = [entity];
74
- for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
75
- const current = queue.shift()!;
76
- const facts = this.about(current, 10);
77
- for (const f of facts) {
78
- if (seen.has(f)) continue;
79
- seen.add(f);
80
- if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
81
- if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
82
- }
83
- }
84
- return Array.from(seen);
85
- }
86
-
87
- /** Remove a fact. */
88
- remove(subj: string, pred: string, obj: string): void {
89
- this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
90
- this.save();
91
- }
92
-
93
- /** Search for entities or predicates containing a keyword. */
94
- search(keyword: string, limit: number = 15): Triple[] {
95
- const k = keyword.toLowerCase();
96
- return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
97
- }
98
-
99
- /** Format facts as readable text. */
100
- format(entity?: string): string {
101
- const facts = entity ? this.about(entity) : this.triples.slice(-30);
102
- if (facts.length === 0) return "(no facts)";
103
- const bySubj = new Map<string, string[]>();
104
- for (const f of facts) {
105
- if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
106
- bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
107
- }
108
- const lines: string[] = [];
109
- for (const [subj, preds] of bySubj) {
110
- lines.push(`**${subj}**: ${preds.join(", ")}`);
111
- }
112
- return lines.join("\n");
113
- }
114
-
115
- get size(): number { return this.triples.length; }
116
-
117
- private save(): void {
118
- try {
119
- const dir = path.dirname(this.indexPath);
120
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
121
- fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
122
- } catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
123
- }
124
-
125
- private load(): void {
126
- try {
127
- if (fs.existsSync(this.indexPath)) {
128
- this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
129
- }
130
- } catch { this.triples = []; }
131
- }
132
- }
133
-
134
- /* ── Auto-extract facts from conversation ── */
135
- const RELATION_PATTERNS: Array<[RegExp, string]> = [
136
- [/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
137
- [/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
138
- [/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
139
- [/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
140
- [/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
141
- ];
142
-
143
- export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
144
- const facts: Array<[string, string, string]> = [];
145
- for (const [pattern, pred] of RELATION_PATTERNS) {
146
- let match;
147
- while ((match = pattern.exec(text)) !== null) {
148
- const subj = match[1].trim().toLowerCase();
149
- const obj = match[2].trim();
150
- if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
151
- facts.push([subj, pred, obj]);
152
- }
153
- }
154
- }
155
- return facts;
156
- }
1
+ /**
2
+ * 简易知识图谱 — entity-relation storage in SQLite.
3
+ *
4
+ * Lightweight triple store: (subject, predicate, object) with metadata.
5
+ * Used for: project info, tool preferences, dependency relationships.
6
+ *
7
+ * Schema:
8
+ * CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
9
+ *
10
+ * Queries:
11
+ * - Find all relations for an entity
12
+ * - Find all entities matching a predicate
13
+ * - Transitive closure (2-hop max for performance)
14
+ */
15
+
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+ import { USER_CONFIG_DIR } from "./config";
19
+ import { getLogger } from "./logger";
20
+
21
+ const log = getLogger("graph");
22
+
23
+ /* ═══════════════════════════════════════
24
+ Triple store — in-memory + optional persistence
25
+ ═══════════════════════════════════════ */
26
+ interface Triple {
27
+ subj: string;
28
+ pred: string;
29
+ obj: string;
30
+ agent: string;
31
+ ts: string;
32
+ meta?: Record<string, string>;
33
+ }
34
+
35
+ export class KnowledgeGraph {
36
+ private triples: Triple[] = [];
37
+ private indexPath: string;
38
+
39
+ constructor(name: string = "default") {
40
+ this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
41
+ this.load();
42
+ }
43
+
44
+ /** Add a fact: (subject, predicate, object). */
45
+ add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
46
+ // Deduplicate
47
+ const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
48
+ if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
49
+
50
+ this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
51
+ if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
52
+ this.save();
53
+ }
54
+
55
+ /** Find all facts about an entity. */
56
+ about(entity: string, limit: number = 20): Triple[] {
57
+ return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
58
+ }
59
+
60
+ /** Find all subjects matching a predicate. */
61
+ byPredicate(pred: string): Triple[] {
62
+ return this.triples.filter(t => t.pred === pred);
63
+ }
64
+
65
+ /** Find all objects for a subject-predicate pair. */
66
+ find(subj: string, pred: string): Triple[] {
67
+ return this.triples.filter(t => t.subj === subj && t.pred === pred);
68
+ }
69
+
70
+ /** Transitive expansion: 2-hop from a starting entity. */
71
+ expand(entity: string, maxDepth: number = 2): Triple[] {
72
+ const seen = new Set<Triple>();
73
+ const queue = [entity];
74
+ for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
75
+ const current = queue.shift()!;
76
+ const facts = this.about(current, 10);
77
+ for (const f of facts) {
78
+ if (seen.has(f)) continue;
79
+ seen.add(f);
80
+ if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
81
+ if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
82
+ }
83
+ }
84
+ return Array.from(seen);
85
+ }
86
+
87
+ /** Remove a fact. */
88
+ remove(subj: string, pred: string, obj: string): void {
89
+ this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
90
+ this.save();
91
+ }
92
+
93
+ /** Search for entities or predicates containing a keyword. */
94
+ search(keyword: string, limit: number = 15): Triple[] {
95
+ const k = keyword.toLowerCase();
96
+ return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
97
+ }
98
+
99
+ /** Format facts as readable text. */
100
+ format(entity?: string): string {
101
+ const facts = entity ? this.about(entity) : this.triples.slice(-30);
102
+ if (facts.length === 0) return "(no facts)";
103
+ const bySubj = new Map<string, string[]>();
104
+ for (const f of facts) {
105
+ if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
106
+ bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
107
+ }
108
+ const lines: string[] = [];
109
+ for (const [subj, preds] of bySubj) {
110
+ lines.push(`**${subj}**: ${preds.join(", ")}`);
111
+ }
112
+ return lines.join("\n");
113
+ }
114
+
115
+ get size(): number { return this.triples.length; }
116
+
117
+ private save(): void {
118
+ try {
119
+ const dir = path.dirname(this.indexPath);
120
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
121
+ fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
122
+ } catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
123
+ }
124
+
125
+ private load(): void {
126
+ try {
127
+ if (fs.existsSync(this.indexPath)) {
128
+ this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
129
+ }
130
+ } catch { this.triples = []; }
131
+ }
132
+ }
133
+
134
+ /* ── Auto-extract facts from conversation ── */
135
+ const RELATION_PATTERNS: Array<[RegExp, string]> = [
136
+ [/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
137
+ [/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
138
+ [/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
139
+ [/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
140
+ [/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
141
+ ];
142
+
143
+ export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
144
+ const facts: Array<[string, string, string]> = [];
145
+ for (const [pattern, pred] of RELATION_PATTERNS) {
146
+ let match;
147
+ while ((match = pattern.exec(text)) !== null) {
148
+ const subj = match[1].trim().toLowerCase();
149
+ const obj = match[2].trim();
150
+ if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
151
+ facts.push([subj, pred, obj]);
152
+ }
153
+ }
154
+ }
155
+ return facts;
156
+ }