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.
- package/dist/939.js +1 -1
- package/dist/cli.js +734 -671
- package/dist/http-server.js +196 -213
- package/dist/index.js +543 -560
- package/dist/mcp-direct.js +751 -768
- package/dist/plugins/hooks/plugin.js +63 -14
- package/package.json +2 -2
- package/dist/plugins/anthropic-auth.js +0 -399
- package/dist/plugins/auto-journal.js +0 -83
- package/dist/plugins/hooks/condition-evaluator.js +0 -109
- package/dist/plugins/hooks/dedup-utils.js +0 -66
- package/dist/plugins/hooks/hook-utils.js +0 -85
- package/dist/plugins/notify-parent.js +0 -95
|
@@ -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
|