alchemylab 1.0.2 → 1.0.4
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/dist/939.js +12 -11
- package/dist/cli.js +9 -8
- package/dist/http-server.js +1 -1
- package/dist/index.js +4 -3
- package/dist/mcp-direct.js +4 -3
- package/dist/plugins/anthropic-auth.js +399 -0
- package/dist/plugins/auto-journal.js +83 -0
- package/dist/plugins/hooks/condition-evaluator.js +109 -0
- package/dist/plugins/hooks/dedup-utils.js +66 -0
- package/dist/plugins/hooks/hook-utils.js +85 -0
- package/dist/plugins/hooks/lib/condition-evaluator.js +114 -0
- package/dist/plugins/hooks/lib/dedup-utils.js +130 -0
- package/dist/plugins/hooks/lib/hook-utils.js +97 -0
- package/dist/plugins/hooks/plugin.js +278 -0
- package/dist/plugins/inject-session.js +67 -0
- package/dist/plugins/notify-parent.js +95 -0
- package/dist/plugins/system-logger.js +95 -0
- package/package.json +12 -6
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hook Condition Evaluator
|
|
4
|
+
*
|
|
5
|
+
* Evaluates hook conditions against event variables to determine
|
|
6
|
+
* if a hook should be triggered.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.evaluateSingleCondition = evaluateSingleCondition;
|
|
10
|
+
exports.evaluatePathsCondition = evaluatePathsCondition;
|
|
11
|
+
exports.evaluateCondition = evaluateCondition;
|
|
12
|
+
/**
|
|
13
|
+
* Simple glob-to-regex converter for pattern matching
|
|
14
|
+
* Handles common glob patterns without external dependencies
|
|
15
|
+
*/
|
|
16
|
+
function globToRegex(pattern) {
|
|
17
|
+
// Escape regex special chars except * and ?
|
|
18
|
+
let regex = pattern
|
|
19
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
20
|
+
// Replace ** with a placeholder
|
|
21
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
22
|
+
// Replace * with [^/]* (anything except path separator)
|
|
23
|
+
.replace(/\*/g, '[^/]*')
|
|
24
|
+
// Replace ? with single char
|
|
25
|
+
.replace(/\?/g, '.')
|
|
26
|
+
// Replace globstar placeholder with .* (anything including /)
|
|
27
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
28
|
+
return new RegExp(`^${regex}$`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if a string matches a glob pattern
|
|
32
|
+
*/
|
|
33
|
+
function isMatch(str, pattern) {
|
|
34
|
+
try {
|
|
35
|
+
return globToRegex(pattern).test(str);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Evaluate a single condition against the field value
|
|
43
|
+
*/
|
|
44
|
+
function evaluateSingleCondition(fieldValue, operator, value) {
|
|
45
|
+
switch (operator) {
|
|
46
|
+
case 'matches':
|
|
47
|
+
return isMatch(fieldValue, value);
|
|
48
|
+
case 'not_matches':
|
|
49
|
+
return !isMatch(fieldValue, value);
|
|
50
|
+
case 'contains':
|
|
51
|
+
return fieldValue.includes(value);
|
|
52
|
+
case 'not_contains':
|
|
53
|
+
return !fieldValue.includes(value);
|
|
54
|
+
case 'equals':
|
|
55
|
+
return fieldValue === value;
|
|
56
|
+
case 'not_equals':
|
|
57
|
+
return fieldValue !== value;
|
|
58
|
+
default:
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Evaluate a condition against a paths array (comma-separated)
|
|
64
|
+
* For 'matches'/'contains' - returns true if ANY path matches
|
|
65
|
+
* For 'not_matches'/'not_contains' - returns true if ALL paths don't match
|
|
66
|
+
*/
|
|
67
|
+
function evaluatePathsCondition(paths, operator, value) {
|
|
68
|
+
switch (operator) {
|
|
69
|
+
case 'matches':
|
|
70
|
+
return paths.some(p => isMatch(p, value));
|
|
71
|
+
case 'not_matches':
|
|
72
|
+
return paths.every(p => !isMatch(p, value));
|
|
73
|
+
case 'contains':
|
|
74
|
+
return paths.some(p => p.includes(value));
|
|
75
|
+
case 'not_contains':
|
|
76
|
+
return paths.every(p => !p.includes(value));
|
|
77
|
+
case 'equals':
|
|
78
|
+
return paths.some(p => p === value);
|
|
79
|
+
case 'not_equals':
|
|
80
|
+
return paths.every(p => p !== value);
|
|
81
|
+
default:
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Evaluate hook conditions against current variables
|
|
87
|
+
* Returns true if hook should be triggered, false to skip
|
|
88
|
+
*/
|
|
89
|
+
function evaluateCondition(condition, variables) {
|
|
90
|
+
// No conditions or empty conditions = always match
|
|
91
|
+
if (!condition || !condition.conditions || condition.conditions.length === 0) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const results = condition.conditions.map((c) => {
|
|
95
|
+
const fieldValue = variables[c.field] || '';
|
|
96
|
+
// For 'paths' field, check if any path matches
|
|
97
|
+
if (c.field === 'paths' && fieldValue.includes(', ')) {
|
|
98
|
+
const paths = fieldValue.split(', ');
|
|
99
|
+
return evaluatePathsCondition(paths, c.operator, c.value);
|
|
100
|
+
}
|
|
101
|
+
// Single value evaluation
|
|
102
|
+
return evaluateSingleCondition(fieldValue, c.operator, c.value);
|
|
103
|
+
});
|
|
104
|
+
// Combine results based on logic (default: AND)
|
|
105
|
+
return condition.logic === 'OR'
|
|
106
|
+
? results.some((r) => r)
|
|
107
|
+
: results.every((r) => r);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=condition-evaluator.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Deduplication Utilities
|
|
4
|
+
*
|
|
5
|
+
* Functions to prevent duplicate hook triggers when OpenCode fires
|
|
6
|
+
* the same event multiple times in quick succession.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createToolCallHash = createToolCallHash;
|
|
10
|
+
exports.isDuplicateToolCall = isDuplicateToolCall;
|
|
11
|
+
exports.clearRecentCalls = clearRecentCalls;
|
|
12
|
+
exports.getRecentCallsCount = getRecentCallsCount;
|
|
13
|
+
// Track recent tool calls with timestamps
|
|
14
|
+
const recentCalls = new Map();
|
|
15
|
+
// Default window for considering calls as duplicates (2 seconds)
|
|
16
|
+
const DEFAULT_DEDUP_WINDOW = 2000;
|
|
17
|
+
/**
|
|
18
|
+
* Create a hash for a tool call based on tool name and output/input
|
|
19
|
+
* Used to identify potentially duplicate calls
|
|
20
|
+
*
|
|
21
|
+
* @param toolName - The name of the tool being called
|
|
22
|
+
* @param text - The output or input text to include in hash (truncated to 100 chars)
|
|
23
|
+
* @returns A string hash identifying this specific call
|
|
24
|
+
*/
|
|
25
|
+
function createToolCallHash(toolName, text) {
|
|
26
|
+
// Simple hash using tool name + first 100 chars of text
|
|
27
|
+
return `${toolName}:${text?.substring(0, 100) || ''}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a tool call is a duplicate of a recent call
|
|
31
|
+
* Also cleans up old entries from the tracking map
|
|
32
|
+
*
|
|
33
|
+
* @param hash - The hash created by createToolCallHash
|
|
34
|
+
* @param now - Current timestamp (allows injection for testing)
|
|
35
|
+
* @param dedupWindow - Time window in ms to consider calls as duplicates (default: 2000ms)
|
|
36
|
+
* @returns true if this appears to be a duplicate call, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP_WINDOW) {
|
|
39
|
+
const lastTime = recentCalls.get(hash);
|
|
40
|
+
if (lastTime && (now - lastTime) < dedupWindow) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Clean up old entries (older than 2x the dedup window)
|
|
44
|
+
const cleanupThreshold = dedupWindow * 2;
|
|
45
|
+
for (const [key, time] of recentCalls.entries()) {
|
|
46
|
+
if (now - time > cleanupThreshold) {
|
|
47
|
+
recentCalls.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Record this call
|
|
51
|
+
recentCalls.set(hash, now);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Clear all tracked calls (useful for testing)
|
|
56
|
+
*/
|
|
57
|
+
function clearRecentCalls() {
|
|
58
|
+
recentCalls.clear();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the number of tracked calls (useful for testing)
|
|
62
|
+
*/
|
|
63
|
+
function getRecentCallsCount() {
|
|
64
|
+
return recentCalls.size;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=dedup-utils.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hook Utility Functions
|
|
4
|
+
*
|
|
5
|
+
* Shared utilities for hook processing including template interpolation
|
|
6
|
+
* and ID extraction from tool outputs.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.interpolatePrompt = interpolatePrompt;
|
|
10
|
+
exports.extractRequestId = extractRequestId;
|
|
11
|
+
exports.extractTaskId = extractTaskId;
|
|
12
|
+
exports.extractFilePath = extractFilePath;
|
|
13
|
+
exports.sanitizeTemplateValue = sanitizeTemplateValue;
|
|
14
|
+
/**
|
|
15
|
+
* Interpolate template variables in prompt
|
|
16
|
+
* Replaces {{variableName}} patterns with corresponding values from variables object
|
|
17
|
+
*
|
|
18
|
+
* @param template - The template string with {{variable}} placeholders
|
|
19
|
+
* @param variables - Key-value pairs to substitute into the template
|
|
20
|
+
* @returns The interpolated string with all variables replaced
|
|
21
|
+
*/
|
|
22
|
+
function interpolatePrompt(template, variables) {
|
|
23
|
+
let result = template;
|
|
24
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
25
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract request ID from tool output
|
|
31
|
+
* Matches patterns like "Request completed: req001234"
|
|
32
|
+
*
|
|
33
|
+
* @param text - The tool output text to search
|
|
34
|
+
* @returns The request ID (e.g., "req001234") or null if not found
|
|
35
|
+
*/
|
|
36
|
+
function extractRequestId(text) {
|
|
37
|
+
const match = text.match(/Request completed:\s*(req\d+)/i);
|
|
38
|
+
return match ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract task ID from tool output
|
|
42
|
+
* Matches patterns like "Task tas002152 has been marked as completed"
|
|
43
|
+
*
|
|
44
|
+
* @param text - The tool output text to search
|
|
45
|
+
* @returns The task ID (e.g., "tas002152") or null if not found
|
|
46
|
+
*/
|
|
47
|
+
function extractTaskId(text) {
|
|
48
|
+
const match = text.match(/Task\s+(tas\d+).*(?:completed|marked as completed)/i);
|
|
49
|
+
return match ? match[1] : null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract file path from write_file tool output
|
|
53
|
+
* Matches patterns like "Created file path/to/file.ts"
|
|
54
|
+
*
|
|
55
|
+
* @param text - The tool output text to search
|
|
56
|
+
* @returns The file path or null if not found
|
|
57
|
+
*/
|
|
58
|
+
function extractFilePath(text) {
|
|
59
|
+
// Match "Created file X" or "**Created file X**"
|
|
60
|
+
const createdMatch = text.match(/\*?\*?Created file\s+([^\s*]+)/i);
|
|
61
|
+
if (createdMatch)
|
|
62
|
+
return createdMatch[1];
|
|
63
|
+
// Match "File Modified: Yes" with path context
|
|
64
|
+
const modifiedMatch = text.match(/File \d+:\s*([^\n]+)\n.*File Modified:\s*Yes/i);
|
|
65
|
+
if (modifiedMatch)
|
|
66
|
+
return modifiedMatch[1];
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sanitize a value for use in template interpolation
|
|
71
|
+
* Truncates long values and escapes special characters
|
|
72
|
+
*
|
|
73
|
+
* @param value - The value to sanitize
|
|
74
|
+
* @param maxLength - Maximum length before truncation (default: 1000)
|
|
75
|
+
* @returns The sanitized string
|
|
76
|
+
*/
|
|
77
|
+
function sanitizeTemplateValue(value, maxLength = 1000) {
|
|
78
|
+
if (!value)
|
|
79
|
+
return '';
|
|
80
|
+
if (value.length > maxLength) {
|
|
81
|
+
return value.substring(0, maxLength) + '... (truncated)';
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=hook-utils.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hook Condition Evaluator
|
|
4
|
+
*
|
|
5
|
+
* Evaluates hook conditions against event variables to determine
|
|
6
|
+
* if a hook should be triggered.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.evaluateSingleCondition = evaluateSingleCondition;
|
|
10
|
+
exports.evaluatePathsCondition = evaluatePathsCondition;
|
|
11
|
+
exports.evaluateCondition = evaluateCondition;
|
|
12
|
+
/**
|
|
13
|
+
* Simple glob-to-regex converter for pattern matching
|
|
14
|
+
* Handles common glob patterns without external dependencies
|
|
15
|
+
*/
|
|
16
|
+
// Cache compiled glob→regex patterns to avoid recompilation on every condition check
|
|
17
|
+
const _globCache = new Map();
|
|
18
|
+
function globToRegex(pattern) {
|
|
19
|
+
const cached = _globCache.get(pattern);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
let regex = pattern
|
|
23
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
24
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
25
|
+
.replace(/\*/g, '[^/]*')
|
|
26
|
+
.replace(/\?/g, '.')
|
|
27
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
28
|
+
const compiled = new RegExp(`^${regex}$`);
|
|
29
|
+
// Cap cache to prevent unbounded growth
|
|
30
|
+
if (_globCache.size > 500)
|
|
31
|
+
_globCache.clear();
|
|
32
|
+
_globCache.set(pattern, compiled);
|
|
33
|
+
return compiled;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a string matches a glob pattern
|
|
37
|
+
*/
|
|
38
|
+
function isMatch(str, pattern) {
|
|
39
|
+
try {
|
|
40
|
+
return globToRegex(pattern).test(str);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Evaluate a single condition against the field value
|
|
48
|
+
*/
|
|
49
|
+
function evaluateSingleCondition(fieldValue, operator, value) {
|
|
50
|
+
switch (operator) {
|
|
51
|
+
case 'matches':
|
|
52
|
+
return isMatch(fieldValue, value);
|
|
53
|
+
case 'not_matches':
|
|
54
|
+
return !isMatch(fieldValue, value);
|
|
55
|
+
case 'contains':
|
|
56
|
+
return fieldValue.includes(value);
|
|
57
|
+
case 'not_contains':
|
|
58
|
+
return !fieldValue.includes(value);
|
|
59
|
+
case 'equals':
|
|
60
|
+
return fieldValue === value;
|
|
61
|
+
case 'not_equals':
|
|
62
|
+
return fieldValue !== value;
|
|
63
|
+
default:
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Evaluate a condition against a paths array (comma-separated)
|
|
69
|
+
* For 'matches'/'contains' - returns true if ANY path matches
|
|
70
|
+
* For 'not_matches'/'not_contains' - returns true if ALL paths don't match
|
|
71
|
+
*/
|
|
72
|
+
function evaluatePathsCondition(paths, operator, value) {
|
|
73
|
+
switch (operator) {
|
|
74
|
+
case 'matches':
|
|
75
|
+
return paths.some(p => isMatch(p, value));
|
|
76
|
+
case 'not_matches':
|
|
77
|
+
return paths.every(p => !isMatch(p, value));
|
|
78
|
+
case 'contains':
|
|
79
|
+
return paths.some(p => p.includes(value));
|
|
80
|
+
case 'not_contains':
|
|
81
|
+
return paths.every(p => !p.includes(value));
|
|
82
|
+
case 'equals':
|
|
83
|
+
return paths.some(p => p === value);
|
|
84
|
+
case 'not_equals':
|
|
85
|
+
return paths.every(p => p !== value);
|
|
86
|
+
default:
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate hook conditions against current variables
|
|
92
|
+
* Returns true if hook should be triggered, false to skip
|
|
93
|
+
*/
|
|
94
|
+
function evaluateCondition(condition, variables) {
|
|
95
|
+
// No conditions or empty conditions = always match
|
|
96
|
+
if (!condition || !condition.conditions || condition.conditions.length === 0) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const results = condition.conditions.map((c) => {
|
|
100
|
+
const fieldValue = variables[c.field] || '';
|
|
101
|
+
// For 'paths' field, check if any path matches
|
|
102
|
+
if (c.field === 'paths' && fieldValue.includes(', ')) {
|
|
103
|
+
const paths = fieldValue.split(', ');
|
|
104
|
+
return evaluatePathsCondition(paths, c.operator, c.value);
|
|
105
|
+
}
|
|
106
|
+
// Single value evaluation
|
|
107
|
+
return evaluateSingleCondition(fieldValue, c.operator, c.value);
|
|
108
|
+
});
|
|
109
|
+
// Combine results based on logic (default: AND)
|
|
110
|
+
return condition.logic === 'OR'
|
|
111
|
+
? results.some((r) => r)
|
|
112
|
+
: results.every((r) => r);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=condition-evaluator.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Deduplication Utilities
|
|
4
|
+
*
|
|
5
|
+
* Functions to prevent duplicate hook triggers when OpenCode fires
|
|
6
|
+
* the same event multiple times in quick succession.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createToolCallHash = createToolCallHash;
|
|
10
|
+
exports.isDuplicateToolCall = isDuplicateToolCall;
|
|
11
|
+
exports.clearRecentCalls = clearRecentCalls;
|
|
12
|
+
exports.getRecentCallsCount = getRecentCallsCount;
|
|
13
|
+
exports.hasProcessedMessage = hasProcessedMessage;
|
|
14
|
+
exports.markMessageProcessed = markMessageProcessed;
|
|
15
|
+
exports.tryMarkMessageProcessed = tryMarkMessageProcessed;
|
|
16
|
+
exports.clearProcessedMessages = clearProcessedMessages;
|
|
17
|
+
// Track recent tool calls with timestamps
|
|
18
|
+
const recentCalls = new Map();
|
|
19
|
+
// Default window for considering calls as duplicates (2 seconds)
|
|
20
|
+
const DEFAULT_DEDUP_WINDOW = 2000;
|
|
21
|
+
/**
|
|
22
|
+
* Create a hash for a tool call based on tool name and output/input
|
|
23
|
+
* Used to identify potentially duplicate calls
|
|
24
|
+
*
|
|
25
|
+
* @param toolName - The name of the tool being called
|
|
26
|
+
* @param text - The output or input text to include in hash (truncated to 100 chars)
|
|
27
|
+
* @returns A string hash identifying this specific call
|
|
28
|
+
*/
|
|
29
|
+
function createToolCallHash(toolName, text) {
|
|
30
|
+
// Simple hash using tool name + first 100 chars of text
|
|
31
|
+
return `${toolName}:${text?.substring(0, 100) || ''}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if a tool call is a duplicate of a recent call
|
|
35
|
+
* Also cleans up old entries from the tracking map
|
|
36
|
+
*
|
|
37
|
+
* @param hash - The hash created by createToolCallHash
|
|
38
|
+
* @param now - Current timestamp (allows injection for testing)
|
|
39
|
+
* @param dedupWindow - Time window in ms to consider calls as duplicates (default: 2000ms)
|
|
40
|
+
* @returns true if this appears to be a duplicate call, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP_WINDOW) {
|
|
43
|
+
const lastTime = recentCalls.get(hash);
|
|
44
|
+
if (lastTime && (now - lastTime) < dedupWindow) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Clean up old entries (older than 2x the dedup window)
|
|
48
|
+
const cleanupThreshold = dedupWindow * 2;
|
|
49
|
+
for (const [key, time] of recentCalls.entries()) {
|
|
50
|
+
if (now - time > cleanupThreshold) {
|
|
51
|
+
recentCalls.delete(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Record this call
|
|
55
|
+
recentCalls.set(hash, now);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Clear all tracked calls (useful for testing)
|
|
60
|
+
*/
|
|
61
|
+
function clearRecentCalls() {
|
|
62
|
+
recentCalls.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the number of tracked calls (useful for testing)
|
|
66
|
+
*/
|
|
67
|
+
function getRecentCallsCount() {
|
|
68
|
+
return recentCalls.size;
|
|
69
|
+
}
|
|
70
|
+
// Track last processed message ID per session to prevent duplicate triggers
|
|
71
|
+
const lastProcessedMessageId = new Map();
|
|
72
|
+
/**
|
|
73
|
+
* Check if we've already processed this message for a session
|
|
74
|
+
* Returns true if this message was already processed (should skip)
|
|
75
|
+
* @deprecated Use tryMarkMessageProcessed for atomic check-and-mark
|
|
76
|
+
*/
|
|
77
|
+
function hasProcessedMessage(sessionId, messageId) {
|
|
78
|
+
if (!messageId)
|
|
79
|
+
return false;
|
|
80
|
+
const lastId = lastProcessedMessageId.get(sessionId);
|
|
81
|
+
return lastId === messageId;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mark a message as processed for a session
|
|
85
|
+
* @deprecated Use tryMarkMessageProcessed for atomic check-and-mark
|
|
86
|
+
*/
|
|
87
|
+
function markMessageProcessed(sessionId, messageId) {
|
|
88
|
+
if (!messageId)
|
|
89
|
+
return;
|
|
90
|
+
lastProcessedMessageId.set(sessionId, messageId);
|
|
91
|
+
// Clean up old entries (keep max 100 sessions)
|
|
92
|
+
if (lastProcessedMessageId.size > 100) {
|
|
93
|
+
const firstKey = lastProcessedMessageId.keys().next().value;
|
|
94
|
+
if (firstKey)
|
|
95
|
+
lastProcessedMessageId.delete(firstKey);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Atomically check if message was processed and mark it if not.
|
|
100
|
+
* Returns true if this call should proceed (message was NOT already processed).
|
|
101
|
+
* Returns false if should skip (message was already processed by another call).
|
|
102
|
+
*
|
|
103
|
+
* This prevents race conditions where two concurrent calls both check
|
|
104
|
+
* hasProcessedMessage() before either calls markMessageProcessed().
|
|
105
|
+
*/
|
|
106
|
+
function tryMarkMessageProcessed(sessionId, messageId) {
|
|
107
|
+
if (!messageId)
|
|
108
|
+
return true; // No message ID = proceed (can't dedupe)
|
|
109
|
+
const lastId = lastProcessedMessageId.get(sessionId);
|
|
110
|
+
if (lastId === messageId) {
|
|
111
|
+
// Already processed - skip
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// Mark as processed immediately (atomic with check above in single-threaded JS)
|
|
115
|
+
lastProcessedMessageId.set(sessionId, messageId);
|
|
116
|
+
// Clean up old entries (keep max 100 sessions)
|
|
117
|
+
if (lastProcessedMessageId.size > 100) {
|
|
118
|
+
const firstKey = lastProcessedMessageId.keys().next().value;
|
|
119
|
+
if (firstKey)
|
|
120
|
+
lastProcessedMessageId.delete(firstKey);
|
|
121
|
+
}
|
|
122
|
+
return true; // Proceed - we claimed this message
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clear processed message tracking (for testing)
|
|
126
|
+
*/
|
|
127
|
+
function clearProcessedMessages() {
|
|
128
|
+
lastProcessedMessageId.clear();
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=dedup-utils.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hook Utility Functions
|
|
4
|
+
*
|
|
5
|
+
* Shared utilities for hook processing including template interpolation
|
|
6
|
+
* and ID extraction from tool outputs.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.interpolatePrompt = interpolatePrompt;
|
|
10
|
+
exports.extractRequestId = extractRequestId;
|
|
11
|
+
exports.extractTaskId = extractTaskId;
|
|
12
|
+
exports.extractFilePath = extractFilePath;
|
|
13
|
+
exports.sanitizeTemplateValue = sanitizeTemplateValue;
|
|
14
|
+
/**
|
|
15
|
+
* Interpolate template variables in prompt
|
|
16
|
+
* Replaces {{variableName}} patterns with corresponding values from variables object
|
|
17
|
+
*
|
|
18
|
+
* @param template - The template string with {{variable}} placeholders
|
|
19
|
+
* @param variables - Key-value pairs to substitute into the template
|
|
20
|
+
* @returns The interpolated string with all variables replaced
|
|
21
|
+
*/
|
|
22
|
+
function interpolatePrompt(template, variables) {
|
|
23
|
+
let result = template;
|
|
24
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
25
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract request ID from tool output
|
|
31
|
+
* Matches patterns like "Request completed: req001234" or "Request req001234 completed successfully"
|
|
32
|
+
*
|
|
33
|
+
* @param text - The tool output text to search
|
|
34
|
+
* @returns The request ID (e.g., "req001234") or null if not found
|
|
35
|
+
*/
|
|
36
|
+
function extractRequestId(text) {
|
|
37
|
+
const patterns = [
|
|
38
|
+
/Request completed:\s*(req\d+)/i,
|
|
39
|
+
/Request (req\d+) completed/i,
|
|
40
|
+
/completed.*?(req\d{6})/i,
|
|
41
|
+
/Request finalized:\s*(req\d+)/i,
|
|
42
|
+
/Request (req\d+) finalized/i,
|
|
43
|
+
/finalized.*?(req\d{6})/i,
|
|
44
|
+
];
|
|
45
|
+
for (const pattern of patterns) {
|
|
46
|
+
const match = text.match(pattern);
|
|
47
|
+
if (match)
|
|
48
|
+
return match[1];
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract task ID from tool output
|
|
54
|
+
* Matches patterns like "Task tas002152 has been marked as completed"
|
|
55
|
+
*
|
|
56
|
+
* @param text - The tool output text to search
|
|
57
|
+
* @returns The task ID (e.g., "tas002152") or null if not found
|
|
58
|
+
*/
|
|
59
|
+
function extractTaskId(text) {
|
|
60
|
+
const match = text.match(/Task\s+(tas\d+).*(?:completed|marked as completed)/i);
|
|
61
|
+
return match ? match[1] : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract file path from write_file tool output
|
|
65
|
+
* Matches patterns like "Created file path/to/file.ts"
|
|
66
|
+
*
|
|
67
|
+
* @param text - The tool output text to search
|
|
68
|
+
* @returns The file path or null if not found
|
|
69
|
+
*/
|
|
70
|
+
function extractFilePath(text) {
|
|
71
|
+
// Match "Created file X" or "**Created file X**"
|
|
72
|
+
const createdMatch = text.match(/\*?\*?Created file\s+([^\s*]+)/i);
|
|
73
|
+
if (createdMatch)
|
|
74
|
+
return createdMatch[1];
|
|
75
|
+
// Match "File Modified: Yes" with path context
|
|
76
|
+
const modifiedMatch = text.match(/File \d+:\s*([^\n]+)\n.*File Modified:\s*Yes/i);
|
|
77
|
+
if (modifiedMatch)
|
|
78
|
+
return modifiedMatch[1];
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Sanitize a value for use in template interpolation
|
|
83
|
+
* Truncates long values and escapes special characters
|
|
84
|
+
*
|
|
85
|
+
* @param value - The value to sanitize
|
|
86
|
+
* @param maxLength - Maximum length before truncation (default: 1000)
|
|
87
|
+
* @returns The sanitized string
|
|
88
|
+
*/
|
|
89
|
+
function sanitizeTemplateValue(value, maxLength = 1000) {
|
|
90
|
+
if (!value)
|
|
91
|
+
return '';
|
|
92
|
+
if (value.length > maxLength) {
|
|
93
|
+
return value.substring(0, maxLength) + '... (truncated)';
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=hook-utils.js.map
|