claude-memory-layer 1.0.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/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata Extractor
|
|
3
|
+
* Extracts tool-specific metadata from tool inputs and outputs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ToolMetadata } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get file type from path
|
|
10
|
+
*/
|
|
11
|
+
function getFileType(filePath: string): string | undefined {
|
|
12
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
13
|
+
if (!ext) return undefined;
|
|
14
|
+
|
|
15
|
+
const typeMap: Record<string, string> = {
|
|
16
|
+
ts: 'typescript',
|
|
17
|
+
tsx: 'typescript',
|
|
18
|
+
js: 'javascript',
|
|
19
|
+
jsx: 'javascript',
|
|
20
|
+
py: 'python',
|
|
21
|
+
rb: 'ruby',
|
|
22
|
+
go: 'go',
|
|
23
|
+
rs: 'rust',
|
|
24
|
+
java: 'java',
|
|
25
|
+
kt: 'kotlin',
|
|
26
|
+
swift: 'swift',
|
|
27
|
+
c: 'c',
|
|
28
|
+
cpp: 'cpp',
|
|
29
|
+
h: 'header',
|
|
30
|
+
hpp: 'header',
|
|
31
|
+
cs: 'csharp',
|
|
32
|
+
php: 'php',
|
|
33
|
+
html: 'html',
|
|
34
|
+
css: 'css',
|
|
35
|
+
scss: 'scss',
|
|
36
|
+
json: 'json',
|
|
37
|
+
yaml: 'yaml',
|
|
38
|
+
yml: 'yaml',
|
|
39
|
+
xml: 'xml',
|
|
40
|
+
md: 'markdown',
|
|
41
|
+
sql: 'sql',
|
|
42
|
+
sh: 'shell',
|
|
43
|
+
bash: 'shell',
|
|
44
|
+
zsh: 'shell'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return typeMap[ext];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Count lines in content
|
|
52
|
+
*/
|
|
53
|
+
function countLines(content: string): number {
|
|
54
|
+
return content.split('\n').length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract bash command (without arguments that might contain secrets)
|
|
59
|
+
*/
|
|
60
|
+
function extractCommand(fullCommand: string): string {
|
|
61
|
+
// Get first word (command name)
|
|
62
|
+
const parts = fullCommand.trim().split(/\s+/);
|
|
63
|
+
const command = parts[0];
|
|
64
|
+
|
|
65
|
+
// For common commands, include safe arguments
|
|
66
|
+
const safeCommands = ['git', 'npm', 'yarn', 'pnpm', 'node', 'python', 'go', 'cargo', 'make'];
|
|
67
|
+
if (safeCommands.includes(command) && parts.length > 1) {
|
|
68
|
+
// Include subcommand for these
|
|
69
|
+
return `${command} ${parts[1]}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return command;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract metadata from tool usage
|
|
77
|
+
*/
|
|
78
|
+
export function extractMetadata(
|
|
79
|
+
toolName: string,
|
|
80
|
+
input: Record<string, unknown>,
|
|
81
|
+
output: string,
|
|
82
|
+
success: boolean
|
|
83
|
+
): ToolMetadata {
|
|
84
|
+
switch (toolName) {
|
|
85
|
+
case 'Read': {
|
|
86
|
+
const filePath = input.file_path as string | undefined;
|
|
87
|
+
return {
|
|
88
|
+
filePath,
|
|
89
|
+
fileType: filePath ? getFileType(filePath) : undefined,
|
|
90
|
+
lineCount: success ? countLines(output) : undefined
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'Write': {
|
|
95
|
+
const filePath = input.file_path as string | undefined;
|
|
96
|
+
const content = input.content as string | undefined;
|
|
97
|
+
return {
|
|
98
|
+
filePath,
|
|
99
|
+
fileType: filePath ? getFileType(filePath) : undefined,
|
|
100
|
+
lineCount: content ? countLines(content) : undefined
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'Edit': {
|
|
105
|
+
const filePath = input.file_path as string | undefined;
|
|
106
|
+
return {
|
|
107
|
+
filePath,
|
|
108
|
+
fileType: filePath ? getFileType(filePath) : undefined
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'Bash': {
|
|
113
|
+
const fullCommand = input.command as string | undefined;
|
|
114
|
+
return {
|
|
115
|
+
command: fullCommand ? extractCommand(fullCommand) : undefined
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'Grep': {
|
|
120
|
+
const pattern = input.pattern as string | undefined;
|
|
121
|
+
// Count matches from output
|
|
122
|
+
const matchCount = success
|
|
123
|
+
? (output.match(/\n/g) || []).length + (output.trim() ? 1 : 0)
|
|
124
|
+
: undefined;
|
|
125
|
+
return {
|
|
126
|
+
pattern,
|
|
127
|
+
matchCount
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'Glob': {
|
|
132
|
+
const pattern = input.pattern as string | undefined;
|
|
133
|
+
const matchCount = success
|
|
134
|
+
? (output.match(/\n/g) || []).length + (output.trim() ? 1 : 0)
|
|
135
|
+
: undefined;
|
|
136
|
+
return {
|
|
137
|
+
pattern,
|
|
138
|
+
matchCount
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'WebFetch': {
|
|
143
|
+
const url = input.url as string | undefined;
|
|
144
|
+
// Try to extract status code from output
|
|
145
|
+
const statusMatch = output.match(/status:\s*(\d{3})/i);
|
|
146
|
+
return {
|
|
147
|
+
url,
|
|
148
|
+
statusCode: statusMatch ? parseInt(statusMatch[1], 10) : undefined
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'WebSearch': {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'NotebookEdit': {
|
|
157
|
+
const notebookPath = input.notebook_path as string | undefined;
|
|
158
|
+
return {
|
|
159
|
+
filePath: notebookPath,
|
|
160
|
+
fileType: 'jupyter'
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
default:
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create embedding content for tool observation
|
|
171
|
+
*/
|
|
172
|
+
export function createToolObservationEmbedding(
|
|
173
|
+
toolName: string,
|
|
174
|
+
metadata: ToolMetadata,
|
|
175
|
+
success: boolean
|
|
176
|
+
): string {
|
|
177
|
+
const parts: string[] = [];
|
|
178
|
+
|
|
179
|
+
parts.push(`Tool: ${toolName}`);
|
|
180
|
+
|
|
181
|
+
if (metadata.filePath) {
|
|
182
|
+
parts.push(`File: ${metadata.filePath}`);
|
|
183
|
+
}
|
|
184
|
+
if (metadata.command) {
|
|
185
|
+
parts.push(`Command: ${metadata.command}`);
|
|
186
|
+
}
|
|
187
|
+
if (metadata.pattern) {
|
|
188
|
+
parts.push(`Pattern: ${metadata.pattern}`);
|
|
189
|
+
}
|
|
190
|
+
if (metadata.url) {
|
|
191
|
+
// Only include domain for privacy
|
|
192
|
+
try {
|
|
193
|
+
const url = new URL(metadata.url);
|
|
194
|
+
parts.push(`URL: ${url.hostname}`);
|
|
195
|
+
} catch {
|
|
196
|
+
// Invalid URL, skip
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
parts.push(`Result: ${success ? 'Success' : 'Failed'}`);
|
|
201
|
+
|
|
202
|
+
return parts.join('\n');
|
|
203
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Filter
|
|
3
|
+
* Combines pattern-based filtering with private tag parsing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parsePrivateTagsSafe, hasUnmatchedOpenTag } from './tag-parser.js';
|
|
7
|
+
import type { Config } from '../types.js';
|
|
8
|
+
|
|
9
|
+
export interface FilterResult {
|
|
10
|
+
content: string;
|
|
11
|
+
metadata: {
|
|
12
|
+
hasPrivateTags: boolean;
|
|
13
|
+
privateTagCount: number;
|
|
14
|
+
patternMatchCount: number;
|
|
15
|
+
originalLength: number;
|
|
16
|
+
filteredLength: number;
|
|
17
|
+
hasUnmatchedTags: boolean;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Sensitive data patterns
|
|
22
|
+
const SENSITIVE_PATTERNS = [
|
|
23
|
+
/password\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
24
|
+
/api[_-]?key\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
25
|
+
/secret\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
26
|
+
/token\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
27
|
+
/bearer\s+[a-zA-Z0-9\-_.]+/gi,
|
|
28
|
+
/AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
|
|
29
|
+
/AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
30
|
+
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
31
|
+
/ghp_[a-zA-Z0-9]{36}/g, // GitHub Personal Access Token
|
|
32
|
+
/sk-[a-zA-Z0-9]{48}/g, // OpenAI API Key
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Apply privacy filter to content
|
|
37
|
+
*/
|
|
38
|
+
export function applyPrivacyFilter(
|
|
39
|
+
content: string,
|
|
40
|
+
config: Config['privacy']
|
|
41
|
+
): FilterResult {
|
|
42
|
+
let filtered = content;
|
|
43
|
+
let privateTagCount = 0;
|
|
44
|
+
let patternMatchCount = 0;
|
|
45
|
+
const hasUnmatchedTags = hasUnmatchedOpenTag(content);
|
|
46
|
+
|
|
47
|
+
// 1. Private tag filtering
|
|
48
|
+
if (config.privateTags?.enabled !== false) {
|
|
49
|
+
const tagResult = parsePrivateTagsSafe(filtered, {
|
|
50
|
+
formats: config.privateTags?.supportedFormats || ['xml'],
|
|
51
|
+
marker: config.privateTags?.marker || '[PRIVATE]'
|
|
52
|
+
});
|
|
53
|
+
filtered = tagResult.filtered;
|
|
54
|
+
privateTagCount = tagResult.stats.count;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Built-in sensitive pattern filtering
|
|
58
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
59
|
+
// Reset lastIndex for global regex
|
|
60
|
+
pattern.lastIndex = 0;
|
|
61
|
+
const matches = filtered.match(pattern);
|
|
62
|
+
if (matches) {
|
|
63
|
+
patternMatchCount += matches.length;
|
|
64
|
+
filtered = filtered.replace(pattern, '[REDACTED]');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. Custom pattern filtering from config
|
|
69
|
+
for (const patternStr of config.excludePatterns || []) {
|
|
70
|
+
try {
|
|
71
|
+
const regex = new RegExp(
|
|
72
|
+
`(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
|
|
73
|
+
'gi'
|
|
74
|
+
);
|
|
75
|
+
const matches = filtered.match(regex);
|
|
76
|
+
if (matches) {
|
|
77
|
+
patternMatchCount += matches.length;
|
|
78
|
+
filtered = filtered.replace(regex, '[REDACTED]');
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Invalid regex pattern, skip
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Clean up consecutive markers
|
|
86
|
+
filtered = filtered.replace(/(\[PRIVATE\]\s*)+/g, '[PRIVATE]\n');
|
|
87
|
+
filtered = filtered.replace(/(\[REDACTED\]\s*)+/g, '[REDACTED] ');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: filtered,
|
|
91
|
+
metadata: {
|
|
92
|
+
hasPrivateTags: privateTagCount > 0,
|
|
93
|
+
privateTagCount,
|
|
94
|
+
patternMatchCount,
|
|
95
|
+
originalLength: content.length,
|
|
96
|
+
filteredLength: filtered.length,
|
|
97
|
+
hasUnmatchedTags
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mask sensitive data in tool input (recursively)
|
|
104
|
+
*/
|
|
105
|
+
export function maskSensitiveInput(
|
|
106
|
+
input: Record<string, unknown>
|
|
107
|
+
): Record<string, unknown> {
|
|
108
|
+
const result: Record<string, unknown> = {};
|
|
109
|
+
|
|
110
|
+
for (const [key, value] of Object.entries(input)) {
|
|
111
|
+
// Check if key suggests sensitive data
|
|
112
|
+
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth', 'credential'];
|
|
113
|
+
const isSensitiveKey = sensitiveKeys.some(k =>
|
|
114
|
+
key.toLowerCase().includes(k)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (isSensitiveKey && typeof value === 'string') {
|
|
118
|
+
result[key] = '[REDACTED]';
|
|
119
|
+
} else if (typeof value === 'string') {
|
|
120
|
+
// Apply pattern filtering to string values
|
|
121
|
+
let filtered = value;
|
|
122
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
123
|
+
pattern.lastIndex = 0;
|
|
124
|
+
filtered = filtered.replace(pattern, '[REDACTED]');
|
|
125
|
+
}
|
|
126
|
+
result[key] = filtered;
|
|
127
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
128
|
+
result[key] = maskSensitiveInput(value as Record<string, unknown>);
|
|
129
|
+
} else if (Array.isArray(value)) {
|
|
130
|
+
result[key] = value.map(item =>
|
|
131
|
+
typeof item === 'object' && item !== null
|
|
132
|
+
? maskSensitiveInput(item as Record<string, unknown>)
|
|
133
|
+
: item
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
result[key] = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Truncate output with head + tail strategy
|
|
145
|
+
*/
|
|
146
|
+
export function truncateOutput(
|
|
147
|
+
output: string,
|
|
148
|
+
options: { maxLength?: number; maxLines?: number }
|
|
149
|
+
): string {
|
|
150
|
+
const { maxLength = 10000, maxLines = 100 } = options;
|
|
151
|
+
|
|
152
|
+
// Split into lines
|
|
153
|
+
const lines = output.split('\n');
|
|
154
|
+
|
|
155
|
+
// Apply line limit first
|
|
156
|
+
if (lines.length > maxLines) {
|
|
157
|
+
const headLines = Math.ceil(maxLines / 2);
|
|
158
|
+
const tailLines = Math.floor(maxLines / 2);
|
|
159
|
+
const head = lines.slice(0, headLines);
|
|
160
|
+
const tail = lines.slice(-tailLines);
|
|
161
|
+
const truncatedLines = [
|
|
162
|
+
...head,
|
|
163
|
+
`\n... [${lines.length - maxLines} lines truncated] ...\n`,
|
|
164
|
+
...tail
|
|
165
|
+
];
|
|
166
|
+
output = truncatedLines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply character limit
|
|
170
|
+
if (output.length > maxLength) {
|
|
171
|
+
const headChars = Math.ceil(maxLength / 2);
|
|
172
|
+
const tailChars = Math.floor(maxLength / 2);
|
|
173
|
+
output = output.slice(0, headChars) +
|
|
174
|
+
`\n... [${output.length - maxLength} characters truncated] ...\n` +
|
|
175
|
+
output.slice(-tailChars);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return output;
|
|
179
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Module
|
|
3
|
+
* Exports privacy-related utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
parsePrivateTags,
|
|
8
|
+
parsePrivateTagsSafe,
|
|
9
|
+
hasUnmatchedOpenTag,
|
|
10
|
+
type PrivateSection,
|
|
11
|
+
type ParseResult,
|
|
12
|
+
type ParseOptions
|
|
13
|
+
} from './tag-parser.js';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
applyPrivacyFilter,
|
|
17
|
+
maskSensitiveInput,
|
|
18
|
+
truncateOutput,
|
|
19
|
+
type FilterResult
|
|
20
|
+
} from './filter.js';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Tag Parser
|
|
3
|
+
* Parses and removes <private> tags from content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PrivateSection {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
content: string;
|
|
10
|
+
format: 'xml' | 'bracket' | 'comment';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParseResult {
|
|
14
|
+
filtered: string;
|
|
15
|
+
sections: PrivateSection[];
|
|
16
|
+
stats: {
|
|
17
|
+
count: number;
|
|
18
|
+
totalLength: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ParseOptions {
|
|
23
|
+
formats: Array<'xml' | 'bracket' | 'comment'>;
|
|
24
|
+
marker: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Tag patterns for different formats
|
|
28
|
+
const TAG_PATTERNS: Record<string, RegExp> = {
|
|
29
|
+
xml: /<private>([\s\S]*?)<\/private>/gi,
|
|
30
|
+
bracket: /\[private\]([\s\S]*?)\[\/private\]/gi,
|
|
31
|
+
comment: /<!--\s*private\s*-->([\s\S]*?)<!--\s*\/private\s*-->/gi
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse and remove private tags from text
|
|
36
|
+
*/
|
|
37
|
+
export function parsePrivateTags(
|
|
38
|
+
text: string,
|
|
39
|
+
options: ParseOptions
|
|
40
|
+
): ParseResult {
|
|
41
|
+
const sections: PrivateSection[] = [];
|
|
42
|
+
let filtered = text;
|
|
43
|
+
|
|
44
|
+
// Find all private sections for each format
|
|
45
|
+
for (const format of options.formats) {
|
|
46
|
+
const pattern = TAG_PATTERNS[format];
|
|
47
|
+
if (!pattern) continue;
|
|
48
|
+
|
|
49
|
+
// Reset lastIndex for global regex
|
|
50
|
+
pattern.lastIndex = 0;
|
|
51
|
+
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
54
|
+
sections.push({
|
|
55
|
+
start: match.index,
|
|
56
|
+
end: match.index + match[0].length,
|
|
57
|
+
content: match[1],
|
|
58
|
+
format: format as PrivateSection['format']
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove all tags and replace with marker
|
|
64
|
+
for (const format of options.formats) {
|
|
65
|
+
const pattern = TAG_PATTERNS[format];
|
|
66
|
+
if (!pattern) continue;
|
|
67
|
+
|
|
68
|
+
// Need to create new regex for replacement (global flag issues)
|
|
69
|
+
const replacePattern = new RegExp(pattern.source, 'gi');
|
|
70
|
+
|
|
71
|
+
filtered = filtered.replace(replacePattern, (_match, content: string) => {
|
|
72
|
+
// Empty tags are completely removed
|
|
73
|
+
if (!content.trim()) return '';
|
|
74
|
+
return options.marker;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
filtered,
|
|
80
|
+
sections,
|
|
81
|
+
stats: {
|
|
82
|
+
count: sections.length,
|
|
83
|
+
totalLength: sections.reduce((sum, s) => sum + s.content.length, 0)
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse private tags safely, protecting code blocks
|
|
90
|
+
*/
|
|
91
|
+
export function parsePrivateTagsSafe(
|
|
92
|
+
text: string,
|
|
93
|
+
options: ParseOptions
|
|
94
|
+
): ParseResult {
|
|
95
|
+
// 1. Extract and protect code blocks
|
|
96
|
+
const codeBlocks: string[] = [];
|
|
97
|
+
const textWithPlaceholders = text.replace(
|
|
98
|
+
/```[\s\S]*?```/g,
|
|
99
|
+
(match) => {
|
|
100
|
+
codeBlocks.push(match);
|
|
101
|
+
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Also protect inline code
|
|
106
|
+
const inlineCode: string[] = [];
|
|
107
|
+
const textWithAllPlaceholders = textWithPlaceholders.replace(
|
|
108
|
+
/`[^`]+`/g,
|
|
109
|
+
(match) => {
|
|
110
|
+
inlineCode.push(match);
|
|
111
|
+
return `__INLINE_CODE_${inlineCode.length - 1}__`;
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// 2. Parse private tags
|
|
116
|
+
const result = parsePrivateTags(textWithAllPlaceholders, options);
|
|
117
|
+
|
|
118
|
+
// 3. Restore inline code
|
|
119
|
+
result.filtered = result.filtered.replace(
|
|
120
|
+
/__INLINE_CODE_(\d+)__/g,
|
|
121
|
+
(_, idx) => inlineCode[Number(idx)]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 4. Restore code blocks
|
|
125
|
+
result.filtered = result.filtered.replace(
|
|
126
|
+
/__CODE_BLOCK_(\d+)__/g,
|
|
127
|
+
(_, idx) => codeBlocks[Number(idx)]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if text has unclosed private tags
|
|
135
|
+
*/
|
|
136
|
+
export function hasUnmatchedOpenTag(text: string): boolean {
|
|
137
|
+
// Check for opening tags without closing
|
|
138
|
+
const openXml = (text.match(/<private>/gi) || []).length;
|
|
139
|
+
const closeXml = (text.match(/<\/private>/gi) || []).length;
|
|
140
|
+
|
|
141
|
+
const openBracket = (text.match(/\[private\]/gi) || []).length;
|
|
142
|
+
const closeBracket = (text.match(/\[\/private\]/gi) || []).length;
|
|
143
|
+
|
|
144
|
+
return openXml !== closeXml || openBracket !== closeBracket;
|
|
145
|
+
}
|