skyloom 1.13.5 → 1.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -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
+ }