icopilot 2.2.0
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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseDocument, stringify } from 'yaml';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { theme } from '../ui/theme.js';
|
|
6
|
+
const DEFAULT_FILTER_FLAGS = 'gi';
|
|
7
|
+
export const FILTERS_CONFIG_FILE = path.join('.icopilot', 'filters.yaml');
|
|
8
|
+
const BUILTIN_FILTER_RULES = [
|
|
9
|
+
{
|
|
10
|
+
name: 'email',
|
|
11
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
12
|
+
type: 'pii',
|
|
13
|
+
action: 'redact',
|
|
14
|
+
replacement: '[REDACTED:EMAIL]',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'phone',
|
|
18
|
+
pattern: /\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?){2}\d{4}\b/g,
|
|
19
|
+
type: 'pii',
|
|
20
|
+
action: 'redact',
|
|
21
|
+
replacement: '[REDACTED:PHONE]',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'ssn',
|
|
25
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
26
|
+
type: 'pii',
|
|
27
|
+
action: 'redact',
|
|
28
|
+
replacement: '[REDACTED:SSN]',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'credit-card',
|
|
32
|
+
pattern: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
33
|
+
type: 'pii',
|
|
34
|
+
action: 'block',
|
|
35
|
+
replacement: '[BLOCKED:CARD]',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'api-key',
|
|
39
|
+
pattern: /\b(?:sk-[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{35})\b/g,
|
|
40
|
+
type: 'secret',
|
|
41
|
+
action: 'block',
|
|
42
|
+
replacement: '[BLOCKED:API_KEY]',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const BUILTIN_RULE_NAMES = new Set(BUILTIN_FILTER_RULES.map((rule) => rule.name.toLowerCase()));
|
|
46
|
+
export class ContentFilter {
|
|
47
|
+
rules = new Map();
|
|
48
|
+
constructor(rules = builtinFilterRules()) {
|
|
49
|
+
for (const rule of rules) {
|
|
50
|
+
this.addRule(rule);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
addRule(rule) {
|
|
54
|
+
const normalized = normalizeRule(rule);
|
|
55
|
+
this.rules.set(normalized.name.toLowerCase(), normalized);
|
|
56
|
+
}
|
|
57
|
+
removeRule(name) {
|
|
58
|
+
return this.rules.delete(name.trim().toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
filter(text) {
|
|
61
|
+
const matches = this.scan(text);
|
|
62
|
+
const replacements = selectReplacementMatches(matches);
|
|
63
|
+
let cursor = 0;
|
|
64
|
+
let filtered = '';
|
|
65
|
+
for (const match of replacements) {
|
|
66
|
+
filtered += text.slice(cursor, match.index);
|
|
67
|
+
filtered += match.replacement ?? defaultReplacement(match.action, match.type);
|
|
68
|
+
cursor = match.end;
|
|
69
|
+
}
|
|
70
|
+
filtered += text.slice(cursor);
|
|
71
|
+
const blocks = matches.filter((match) => match.action === 'block').length;
|
|
72
|
+
const redactions = matches.filter((match) => match.action === 'redact').length;
|
|
73
|
+
const warnings = matches.filter((match) => match.action === 'warn').length;
|
|
74
|
+
return {
|
|
75
|
+
original: text,
|
|
76
|
+
filtered,
|
|
77
|
+
matches,
|
|
78
|
+
blocked: blocks > 0,
|
|
79
|
+
changed: filtered !== text,
|
|
80
|
+
redactions,
|
|
81
|
+
warnings,
|
|
82
|
+
blocks,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
scan(text) {
|
|
86
|
+
const matches = [];
|
|
87
|
+
for (const rule of this.rules.values()) {
|
|
88
|
+
const matcher = toGlobalRegex(rule.pattern);
|
|
89
|
+
let result;
|
|
90
|
+
while ((result = matcher.exec(text)) !== null) {
|
|
91
|
+
const value = result[0] ?? '';
|
|
92
|
+
if (!value) {
|
|
93
|
+
matcher.lastIndex += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!shouldKeepMatch(rule, value)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
matches.push({
|
|
100
|
+
name: rule.name,
|
|
101
|
+
match: value,
|
|
102
|
+
index: result.index,
|
|
103
|
+
end: result.index + value.length,
|
|
104
|
+
type: rule.type,
|
|
105
|
+
action: rule.action,
|
|
106
|
+
replacement: rule.action === 'warn'
|
|
107
|
+
? undefined
|
|
108
|
+
: (rule.replacement ?? defaultReplacement(rule.action, rule.type)),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return matches.sort(compareMatches);
|
|
113
|
+
}
|
|
114
|
+
getRules() {
|
|
115
|
+
return Array.from(this.rules.values())
|
|
116
|
+
.map(cloneRule)
|
|
117
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
118
|
+
}
|
|
119
|
+
isClean(text) {
|
|
120
|
+
return this.scan(text).length === 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function builtinFilterRules() {
|
|
124
|
+
return BUILTIN_FILTER_RULES.map(cloneRule);
|
|
125
|
+
}
|
|
126
|
+
export function defaultFiltersConfigPath(cwd = config.cwd) {
|
|
127
|
+
return path.join(cwd, FILTERS_CONFIG_FILE);
|
|
128
|
+
}
|
|
129
|
+
export function loadProjectContentFilter(cwd = config.cwd) {
|
|
130
|
+
const filter = new ContentFilter(builtinFilterRules());
|
|
131
|
+
try {
|
|
132
|
+
const configFile = readFiltersConfig(defaultFiltersConfigPath(cwd));
|
|
133
|
+
for (const name of configFile.disabled ?? []) {
|
|
134
|
+
filter.removeRule(name);
|
|
135
|
+
}
|
|
136
|
+
for (const rule of configFile.rules ?? []) {
|
|
137
|
+
filter.addRule(hydrateRule(rule));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return filter;
|
|
142
|
+
}
|
|
143
|
+
return filter;
|
|
144
|
+
}
|
|
145
|
+
export function saveProjectFilterRule(cwd, rule) {
|
|
146
|
+
const configPath = defaultFiltersConfigPath(cwd);
|
|
147
|
+
const configFile = readFiltersConfig(configPath);
|
|
148
|
+
const normalized = normalizeRule(rule);
|
|
149
|
+
const ruleName = normalized.name.toLowerCase();
|
|
150
|
+
const nextConfig = {
|
|
151
|
+
disabled: (configFile.disabled ?? []).filter((name) => name.toLowerCase() !== ruleName),
|
|
152
|
+
rules: [
|
|
153
|
+
...(configFile.rules ?? []).filter((entry) => entry.name.toLowerCase() !== ruleName),
|
|
154
|
+
dehydrateRule(normalized),
|
|
155
|
+
].sort((left, right) => left.name.localeCompare(right.name)),
|
|
156
|
+
};
|
|
157
|
+
writeFiltersConfig(configPath, nextConfig);
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
export function removeProjectFilterRule(cwd, name) {
|
|
161
|
+
const target = name.trim().toLowerCase();
|
|
162
|
+
const configPath = defaultFiltersConfigPath(cwd);
|
|
163
|
+
const configFile = readFiltersConfig(configPath);
|
|
164
|
+
const customRules = configFile.rules ?? [];
|
|
165
|
+
const hadCustomRule = customRules.some((rule) => rule.name.toLowerCase() === target);
|
|
166
|
+
const nextConfig = {
|
|
167
|
+
disabled: [...new Set(configFile.disabled ?? [])],
|
|
168
|
+
rules: customRules.filter((rule) => rule.name.toLowerCase() !== target),
|
|
169
|
+
};
|
|
170
|
+
if (hadCustomRule) {
|
|
171
|
+
writeFiltersConfig(configPath, nextConfig);
|
|
172
|
+
return { removed: true, source: 'custom' };
|
|
173
|
+
}
|
|
174
|
+
if (BUILTIN_RULE_NAMES.has(target)) {
|
|
175
|
+
nextConfig.disabled = [...new Set([...(nextConfig.disabled ?? []), target])].sort((left, right) => left.localeCompare(right));
|
|
176
|
+
writeFiltersConfig(configPath, nextConfig);
|
|
177
|
+
return { removed: true, source: 'builtin' };
|
|
178
|
+
}
|
|
179
|
+
return { removed: false, source: null };
|
|
180
|
+
}
|
|
181
|
+
export function parseFilterAction(value) {
|
|
182
|
+
const normalized = value.trim().toLowerCase();
|
|
183
|
+
return normalized === 'redact' || normalized === 'warn' || normalized === 'block'
|
|
184
|
+
? normalized
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
export function parseFilterPattern(value) {
|
|
188
|
+
const trimmed = value.trim();
|
|
189
|
+
const inline = trimmed.match(/^\/(.+)\/([a-z]*)$/i);
|
|
190
|
+
if (inline) {
|
|
191
|
+
return new RegExp(inline[1] ?? '', inline[2] ?? '');
|
|
192
|
+
}
|
|
193
|
+
return new RegExp(trimmed, DEFAULT_FILTER_FLAGS);
|
|
194
|
+
}
|
|
195
|
+
export function formatFilterRules(filter, cwd = config.cwd) {
|
|
196
|
+
const rules = filter.getRules();
|
|
197
|
+
const lines = [`${theme.brand('Content filters')} ${theme.dim(defaultFiltersConfigPath(cwd))}`];
|
|
198
|
+
if (rules.length === 0) {
|
|
199
|
+
lines.push(` ${theme.dim('No filter rules configured.')}`);
|
|
200
|
+
return `${lines.join('\n')}\n`;
|
|
201
|
+
}
|
|
202
|
+
for (const rule of rules) {
|
|
203
|
+
const source = BUILTIN_RULE_NAMES.has(rule.name.toLowerCase()) ? 'builtin' : 'custom';
|
|
204
|
+
lines.push(` ${theme.hl(rule.name)} ${theme.dim(`${rule.type}/${rule.action}/${source} /${rule.pattern.source}/${rule.pattern.flags}`)}`);
|
|
205
|
+
}
|
|
206
|
+
return `${lines.join('\n')}\n`;
|
|
207
|
+
}
|
|
208
|
+
export function formatFilterTestResult(result) {
|
|
209
|
+
const lines = [`${theme.brand('Content filter test')}`, ` status: ${statusLabel(result)}`];
|
|
210
|
+
if (result.matches.length === 0) {
|
|
211
|
+
lines.push(` ${theme.ok('No matches found.')}`);
|
|
212
|
+
return `${lines.join('\n')}\n`;
|
|
213
|
+
}
|
|
214
|
+
lines.push(` summary: ${theme.dim(summarizeFilterResult(result))}`, '', theme.brand('Matches'));
|
|
215
|
+
for (const match of result.matches) {
|
|
216
|
+
lines.push(` ${theme.hl(match.name)} ${theme.dim(`${match.type}/${match.action}`)} ` +
|
|
217
|
+
`${theme.dim(`@${match.index}-${match.end}`)}`);
|
|
218
|
+
}
|
|
219
|
+
lines.push('', theme.brand('Filtered output'), result.filtered);
|
|
220
|
+
return `${lines.join('\n')}\n`;
|
|
221
|
+
}
|
|
222
|
+
export function summarizeFilterResult(result) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
if (result.redactions > 0)
|
|
225
|
+
parts.push(`${result.redactions} redaction${result.redactions === 1 ? '' : 's'}`);
|
|
226
|
+
if (result.blocks > 0)
|
|
227
|
+
parts.push(`${result.blocks} block${result.blocks === 1 ? '' : 's'}`);
|
|
228
|
+
if (result.warnings > 0)
|
|
229
|
+
parts.push(`${result.warnings} warning${result.warnings === 1 ? '' : 's'}`);
|
|
230
|
+
return parts.join(', ') || 'no matches';
|
|
231
|
+
}
|
|
232
|
+
function compareMatches(left, right) {
|
|
233
|
+
if (left.index !== right.index)
|
|
234
|
+
return left.index - right.index;
|
|
235
|
+
const actionDelta = actionPriority(right.action) - actionPriority(left.action);
|
|
236
|
+
if (actionDelta !== 0)
|
|
237
|
+
return actionDelta;
|
|
238
|
+
return right.end - right.index - (left.end - left.index);
|
|
239
|
+
}
|
|
240
|
+
function selectReplacementMatches(matches) {
|
|
241
|
+
const selected = [];
|
|
242
|
+
let cursor = -1;
|
|
243
|
+
for (const match of [...matches].sort(compareMatches)) {
|
|
244
|
+
if (match.action === 'warn')
|
|
245
|
+
continue;
|
|
246
|
+
if (match.index < cursor)
|
|
247
|
+
continue;
|
|
248
|
+
selected.push(match);
|
|
249
|
+
cursor = match.end;
|
|
250
|
+
}
|
|
251
|
+
return selected;
|
|
252
|
+
}
|
|
253
|
+
function actionPriority(action) {
|
|
254
|
+
switch (action) {
|
|
255
|
+
case 'block':
|
|
256
|
+
return 3;
|
|
257
|
+
case 'redact':
|
|
258
|
+
return 2;
|
|
259
|
+
case 'warn':
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function shouldKeepMatch(rule, value) {
|
|
264
|
+
if (rule.name.toLowerCase() === 'credit-card') {
|
|
265
|
+
const digits = value.replace(/\D/g, '');
|
|
266
|
+
return digits.length >= 13 && digits.length <= 19 && passesLuhn(digits);
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
function passesLuhn(value) {
|
|
271
|
+
let sum = 0;
|
|
272
|
+
let doubleDigit = false;
|
|
273
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
274
|
+
let digit = Number(value[index]);
|
|
275
|
+
if (Number.isNaN(digit))
|
|
276
|
+
return false;
|
|
277
|
+
if (doubleDigit) {
|
|
278
|
+
digit *= 2;
|
|
279
|
+
if (digit > 9)
|
|
280
|
+
digit -= 9;
|
|
281
|
+
}
|
|
282
|
+
sum += digit;
|
|
283
|
+
doubleDigit = !doubleDigit;
|
|
284
|
+
}
|
|
285
|
+
return sum > 0 && sum % 10 === 0;
|
|
286
|
+
}
|
|
287
|
+
function normalizeRule(rule) {
|
|
288
|
+
const name = rule.name.trim();
|
|
289
|
+
if (!name) {
|
|
290
|
+
throw new Error('Filter rule name must be a non-empty string.');
|
|
291
|
+
}
|
|
292
|
+
if (!(rule.pattern instanceof RegExp)) {
|
|
293
|
+
throw new Error(`Filter rule "${name}" must use a valid regular expression.`);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
name,
|
|
297
|
+
pattern: new RegExp(rule.pattern.source, rule.pattern.flags),
|
|
298
|
+
type: rule.type,
|
|
299
|
+
action: rule.action,
|
|
300
|
+
replacement: rule.replacement?.trim() || undefined,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function cloneRule(rule) {
|
|
304
|
+
return {
|
|
305
|
+
...rule,
|
|
306
|
+
pattern: new RegExp(rule.pattern.source, rule.pattern.flags),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function toGlobalRegex(pattern) {
|
|
310
|
+
const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
|
|
311
|
+
return new RegExp(pattern.source, flags);
|
|
312
|
+
}
|
|
313
|
+
function defaultReplacement(action, type) {
|
|
314
|
+
return `[${action === 'block' ? 'BLOCKED' : 'REDACTED'}:${type.toUpperCase()}]`;
|
|
315
|
+
}
|
|
316
|
+
function readFiltersConfig(filePath) {
|
|
317
|
+
if (!fs.existsSync(filePath)) {
|
|
318
|
+
return { disabled: [], rules: [] };
|
|
319
|
+
}
|
|
320
|
+
const document = parseDocument(fs.readFileSync(filePath, 'utf8'));
|
|
321
|
+
if (document.errors.length > 0) {
|
|
322
|
+
throw document.errors[0] ?? new Error('Invalid content filter YAML.');
|
|
323
|
+
}
|
|
324
|
+
return parseFiltersConfig(document.toJSON());
|
|
325
|
+
}
|
|
326
|
+
function parseFiltersConfig(value) {
|
|
327
|
+
if (!value) {
|
|
328
|
+
return { disabled: [], rules: [] };
|
|
329
|
+
}
|
|
330
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
331
|
+
throw new Error('filters config must contain a YAML object');
|
|
332
|
+
}
|
|
333
|
+
const record = value;
|
|
334
|
+
const disabled = record.disabled === undefined
|
|
335
|
+
? []
|
|
336
|
+
: parseStringArray(record.disabled, 'disabled').map((entry) => entry.toLowerCase());
|
|
337
|
+
const rules = record.rules === undefined
|
|
338
|
+
? []
|
|
339
|
+
: parseRuleList(record.rules).sort((left, right) => left.name.localeCompare(right.name));
|
|
340
|
+
return {
|
|
341
|
+
disabled: [...new Set(disabled)],
|
|
342
|
+
rules,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function parseStringArray(value, field) {
|
|
346
|
+
if (!Array.isArray(value)) {
|
|
347
|
+
throw new Error(`filters config field "${field}" must be an array`);
|
|
348
|
+
}
|
|
349
|
+
return value.flatMap((entry) => {
|
|
350
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
351
|
+
throw new Error(`filters config field "${field}" must contain non-empty strings`);
|
|
352
|
+
}
|
|
353
|
+
return entry.trim();
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function parseRuleList(value) {
|
|
357
|
+
if (!Array.isArray(value)) {
|
|
358
|
+
throw new Error('filters config field "rules" must be an array');
|
|
359
|
+
}
|
|
360
|
+
return value.map(validatePersistedRule);
|
|
361
|
+
}
|
|
362
|
+
function validatePersistedRule(value) {
|
|
363
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
364
|
+
throw new Error('filter rules must be YAML objects');
|
|
365
|
+
}
|
|
366
|
+
const record = value;
|
|
367
|
+
const name = requireString(record.name, 'name');
|
|
368
|
+
const pattern = requireString(record.pattern, 'pattern');
|
|
369
|
+
const type = parseRuleType(record.type);
|
|
370
|
+
const action = parseRuleAction(record.action);
|
|
371
|
+
const flags = optionalString(record.flags, 'flags');
|
|
372
|
+
const replacement = optionalString(record.replacement, 'replacement');
|
|
373
|
+
try {
|
|
374
|
+
new RegExp(pattern, flags ?? '');
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
378
|
+
throw new Error(`filter rule "${name}" has an invalid pattern: ${message}`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
name,
|
|
382
|
+
pattern,
|
|
383
|
+
flags,
|
|
384
|
+
type,
|
|
385
|
+
action,
|
|
386
|
+
replacement,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function hydrateRule(rule) {
|
|
390
|
+
return normalizeRule({
|
|
391
|
+
name: rule.name,
|
|
392
|
+
pattern: new RegExp(rule.pattern, rule.flags ?? ''),
|
|
393
|
+
type: rule.type ?? 'custom',
|
|
394
|
+
action: rule.action ?? 'warn',
|
|
395
|
+
replacement: rule.replacement,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function dehydrateRule(rule) {
|
|
399
|
+
return {
|
|
400
|
+
name: rule.name,
|
|
401
|
+
pattern: rule.pattern.source,
|
|
402
|
+
flags: rule.pattern.flags || undefined,
|
|
403
|
+
type: rule.type,
|
|
404
|
+
action: rule.action,
|
|
405
|
+
replacement: rule.replacement,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function writeFiltersConfig(filePath, configFile) {
|
|
409
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
410
|
+
fs.writeFileSync(filePath, stringify({
|
|
411
|
+
disabled: [...new Set(configFile.disabled ?? [])].sort((left, right) => left.localeCompare(right)),
|
|
412
|
+
rules: [...(configFile.rules ?? [])].sort((left, right) => left.name.localeCompare(right.name)),
|
|
413
|
+
}), 'utf8');
|
|
414
|
+
}
|
|
415
|
+
function requireString(value, field) {
|
|
416
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
417
|
+
throw new Error(`filters config field "${field}" must be a non-empty string`);
|
|
418
|
+
}
|
|
419
|
+
return value.trim();
|
|
420
|
+
}
|
|
421
|
+
function optionalString(value, field) {
|
|
422
|
+
if (value === undefined || value === null)
|
|
423
|
+
return undefined;
|
|
424
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
425
|
+
throw new Error(`filters config field "${field}" must be a non-empty string`);
|
|
426
|
+
}
|
|
427
|
+
return value.trim();
|
|
428
|
+
}
|
|
429
|
+
function parseRuleType(value) {
|
|
430
|
+
if (value === undefined)
|
|
431
|
+
return undefined;
|
|
432
|
+
if (value === 'pii' || value === 'secret' || value === 'custom')
|
|
433
|
+
return value;
|
|
434
|
+
throw new Error('filters config field "type" must be pii, secret, or custom');
|
|
435
|
+
}
|
|
436
|
+
function parseRuleAction(value) {
|
|
437
|
+
if (value === undefined)
|
|
438
|
+
return undefined;
|
|
439
|
+
if (value === 'redact' || value === 'warn' || value === 'block')
|
|
440
|
+
return value;
|
|
441
|
+
throw new Error('filters config field "action" must be redact, warn, or block');
|
|
442
|
+
}
|
|
443
|
+
function statusLabel(result) {
|
|
444
|
+
if (result.blocked)
|
|
445
|
+
return theme.err('blocked');
|
|
446
|
+
if (result.changed)
|
|
447
|
+
return theme.warn('filtered');
|
|
448
|
+
return theme.ok('clean');
|
|
449
|
+
}
|