alchemylab 1.0.4 → 1.0.6

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.
@@ -1,109 +0,0 @@
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
@@ -1,66 +0,0 @@
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
@@ -1,85 +0,0 @@
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
@@ -1,95 +0,0 @@
1
- "use strict";
2
- /**
3
- * OpenCode Notify Parent Plugin
4
- *
5
- * Sends a toast notification to the parent session when a request is completed.
6
- * Only active when OPENCODE_PARENT_SERVER env var is set (indicating this is
7
- * a background subagent spawned by another session).
8
- *
9
- * Hooks into tool.execute.after to detect request-operations complete calls.
10
- */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- // Deduplication: track recent tool calls to prevent double-triggering
13
- // OpenCode sometimes fires tool.execute.after twice for the same call
14
- const recentToolCalls = new Map();
15
- const DEDUP_WINDOW = 2000; // 2 second window
16
- function createToolCallHash(toolName, outputText) {
17
- return `${toolName}:${outputText?.substring(0, 100) || ''}`;
18
- }
19
- function isDuplicateToolCall(hash, now) {
20
- const lastTime = recentToolCalls.get(hash);
21
- if (lastTime && (now - lastTime) < DEDUP_WINDOW) {
22
- return true;
23
- }
24
- // Clean up old entries
25
- for (const [key, time] of recentToolCalls.entries()) {
26
- if (now - time > DEDUP_WINDOW * 2) {
27
- recentToolCalls.delete(key);
28
- }
29
- }
30
- recentToolCalls.set(hash, now);
31
- return false;
32
- }
33
- exports.default = async ({ project, directory }) => {
34
- // Only activate if we have a parent server to notify
35
- const parentServerUrl = process.env.OPENCODE_PARENT_SERVER;
36
- if (!parentServerUrl) {
37
- return {}; // No hooks if no parent to notify
38
- }
39
- return {
40
- "tool.execute.after": async (input, output) => {
41
- try {
42
- // Check if this is request-operations (with underscores)
43
- const toolName = input?.tool || input?.name;
44
- if (!toolName || !toolName.includes('request_operations')) {
45
- return; // Not the tool we're looking for
46
- }
47
- // Deduplication check - OpenCode fires hooks twice
48
- const outputText = output?.content?.[0]?.text || '';
49
- const callHash = createToolCallHash(toolName, outputText);
50
- if (isDuplicateToolCall(callHash, Date.now())) {
51
- return; // Skip duplicate
52
- }
53
- // Check the output content for the operation result
54
- if (output?.content && Array.isArray(output.content)) {
55
- if (output.content[0]?.text) {
56
- const resultText = output.content[0].text;
57
- // Check if this was a complete operation
58
- const completionMatch = resultText.match(/(?:Request|request)\s+(req\d+)\s+(?:completed|complete)/i);
59
- if (completionMatch) {
60
- const requestId = completionMatch[1];
61
- // Send toast notification to parent
62
- await sendToastToParent(parentServerUrl, {
63
- title: "Background Work Complete",
64
- message: `Request ${requestId} has been completed`,
65
- variant: "success",
66
- duration: 8000
67
- });
68
- }
69
- }
70
- }
71
- }
72
- catch (error) {
73
- // Silently ignore errors to avoid breaking the main session
74
- // Could log to file if debugging is needed
75
- }
76
- }
77
- };
78
- };
79
- /**
80
- * Send a toast notification to the parent OpenCode session
81
- */
82
- async function sendToastToParent(parentServerUrl, toast) {
83
- const toastUrl = `${parentServerUrl.replace(/\/$/, '')}/tui/toast`;
84
- const response = await fetch(toastUrl, {
85
- method: 'POST',
86
- headers: {
87
- 'Content-Type': 'application/json'
88
- },
89
- body: JSON.stringify(toast)
90
- });
91
- if (!response.ok) {
92
- throw new Error(`Failed to send toast: ${response.status} ${response.statusText}`);
93
- }
94
- }
95
- //# sourceMappingURL=notify-parent.js.map