alchemylab 1.3.5 → 1.4.1

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,17 +1,9 @@
1
- "use strict";
2
1
  /**
3
2
  * Deduplication Utilities
4
3
  *
5
4
  * Functions to prevent duplicate hook triggers when OpenCode fires
6
5
  * the same event multiple times in quick succession.
7
6
  */
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.tryMarkMessageProcessed = tryMarkMessageProcessed;
14
- exports.clearProcessedMessages = clearProcessedMessages;
15
7
  // Track recent tool calls with timestamps
16
8
  const recentCalls = new Map();
17
9
  // Default window for considering calls as duplicates (2 seconds)
@@ -24,7 +16,7 @@ const DEFAULT_DEDUP_WINDOW = 2000;
24
16
  * @param text - The output or input text to include in hash (truncated to 100 chars)
25
17
  * @returns A string hash identifying this specific call
26
18
  */
27
- function createToolCallHash(toolName, text) {
19
+ export function createToolCallHash(toolName, text) {
28
20
  // Simple hash using tool name + first 100 chars of text
29
21
  return `${toolName}:${text?.substring(0, 100) || ''}`;
30
22
  }
@@ -37,7 +29,7 @@ function createToolCallHash(toolName, text) {
37
29
  * @param dedupWindow - Time window in ms to consider calls as duplicates (default: 2000ms)
38
30
  * @returns true if this appears to be a duplicate call, false otherwise
39
31
  */
40
- function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP_WINDOW) {
32
+ export function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP_WINDOW) {
41
33
  const lastTime = recentCalls.get(hash);
42
34
  if (lastTime && (now - lastTime) < dedupWindow) {
43
35
  return true;
@@ -56,13 +48,13 @@ function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP
56
48
  /**
57
49
  * Clear all tracked calls (useful for testing)
58
50
  */
59
- function clearRecentCalls() {
51
+ export function clearRecentCalls() {
60
52
  recentCalls.clear();
61
53
  }
62
54
  /**
63
55
  * Get the number of tracked calls (useful for testing)
64
56
  */
65
- function getRecentCallsCount() {
57
+ export function getRecentCallsCount() {
66
58
  return recentCalls.size;
67
59
  }
68
60
  // Track last processed message ID per session to prevent duplicate triggers
@@ -75,7 +67,7 @@ const lastProcessedMessageId = new Map();
75
67
  * This prevents race conditions where two concurrent calls both check
76
68
  * hasProcessedMessage() before either calls markMessageProcessed().
77
69
  */
78
- function tryMarkMessageProcessed(sessionId, messageId) {
70
+ export function tryMarkMessageProcessed(sessionId, messageId) {
79
71
  if (!messageId)
80
72
  return true; // No message ID = proceed (can't dedupe)
81
73
  const lastId = lastProcessedMessageId.get(sessionId);
@@ -96,7 +88,7 @@ function tryMarkMessageProcessed(sessionId, messageId) {
96
88
  /**
97
89
  * Clear processed message tracking (for testing)
98
90
  */
99
- function clearProcessedMessages() {
91
+ export function clearProcessedMessages() {
100
92
  lastProcessedMessageId.clear();
101
93
  }
102
94
  //# sourceMappingURL=dedup-utils.js.map
@@ -1,16 +1,9 @@
1
- "use strict";
2
1
  /**
3
2
  * Hook Utility Functions
4
3
  *
5
4
  * Shared utilities for hook processing including template interpolation
6
5
  * and ID extraction from tool outputs.
7
6
  */
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
7
  /**
15
8
  * Interpolate template variables in prompt
16
9
  * Replaces {{variableName}} patterns with corresponding values from variables object
@@ -19,7 +12,7 @@ exports.sanitizeTemplateValue = sanitizeTemplateValue;
19
12
  * @param variables - Key-value pairs to substitute into the template
20
13
  * @returns The interpolated string with all variables replaced
21
14
  */
22
- function interpolatePrompt(template, variables) {
15
+ export function interpolatePrompt(template, variables) {
23
16
  let result = template;
24
17
  for (const [key, value] of Object.entries(variables)) {
25
18
  result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
@@ -33,7 +26,7 @@ function interpolatePrompt(template, variables) {
33
26
  * @param text - The tool output text to search
34
27
  * @returns The request ID (e.g., "req001234") or null if not found
35
28
  */
36
- function extractRequestId(text) {
29
+ export function extractRequestId(text) {
37
30
  const patterns = [
38
31
  /Request completed:\s*(req\d+)/i,
39
32
  /Request (req\d+) completed/i,
@@ -56,7 +49,7 @@ function extractRequestId(text) {
56
49
  * @param text - The tool output text to search
57
50
  * @returns The task ID (e.g., "tas002152") or null if not found
58
51
  */
59
- function extractTaskId(text) {
52
+ export function extractTaskId(text) {
60
53
  const match = text.match(/Task\s+(tas\d+).*(?:completed|marked as completed)/i);
61
54
  return match ? match[1] : null;
62
55
  }
@@ -67,7 +60,7 @@ function extractTaskId(text) {
67
60
  * @param text - The tool output text to search
68
61
  * @returns The file path or null if not found
69
62
  */
70
- function extractFilePath(text) {
63
+ export function extractFilePath(text) {
71
64
  // Match "Created file X" or "**Created file X**"
72
65
  const createdMatch = text.match(/\*?\*?Created file\s+([^\s*]+)/i);
73
66
  if (createdMatch)
@@ -86,7 +79,7 @@ function extractFilePath(text) {
86
79
  * @param maxLength - Maximum length before truncation (default: 1000)
87
80
  * @returns The sanitized string
88
81
  */
89
- function sanitizeTemplateValue(value, maxLength = 1000) {
82
+ export function sanitizeTemplateValue(value, maxLength = 1000) {
90
83
  if (!value)
91
84
  return '';
92
85
  if (value.length > maxLength) {
@@ -1,320 +1,150 @@
1
- "use strict";
2
1
  /**
3
- * OpenCode Event Hooks Plugin — Thin Event Emitter
2
+ * OpenCode Event Hooks Plugin — Raw Event Emitter
4
3
  *
5
4
  * Detects events from OpenCode (tool execution, session lifecycle) and POSTs
6
- * them to the action server's /api/hook-event endpoint. All hook logic
7
- * (fetching, condition evaluation, template interpolation, execution) lives
8
- * in the action server.
5
+ * them to the action server. The plugin itself does no parsing or event-type
6
+ * derivation: it ships raw envelopes and the action server's hook layer
7
+ * handles extraction, derivation, condition evaluation, and execution.
9
8
  */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- const path_1 = require("path");
12
- const fs_1 = require("fs");
13
- const ACTION_SERVER_PORT = 3456;
14
- const ACTION_SERVER_URL = `http://localhost:${ACTION_SERVER_PORT}/api/hook-event`;
15
- // Singleton guard — OpenCode calls plugin init twice
16
- let initializedDirectory = null;
17
- let cachedHandlers = null;
9
+ import { join } from "path";
10
+ import { readFileSync, existsSync } from "fs";
11
+ const DEFAULT_ACTION_SERVER_PORT = 3456;
18
12
  /**
19
- * Extract request ID from tool output
13
+ * Read the action server's actual port from `${directory}/.alchemy_lab/action-server.json`.
14
+ * The action server auto-increments from 3456 when the preferred port is busy
15
+ * (multiple alchemylab installs on one machine), and writes the resolved port
16
+ * to that file. Without this discovery we'd POST events to whatever first
17
+ * action server happens to own 3456 — wrong project, hooks never fire.
18
+ *
19
+ * Falls back to 3456 if the file is missing or malformed.
20
20
  */
21
- function extractRequestId(text) {
22
- const patterns = [
23
- /Request completed:\s*(req\d+)/i,
24
- /Request (req\d+) completed/i,
25
- /completed.*?(req\d{6})/i,
26
- /Request finalized:\s*(req\d+)/i,
27
- /Request (req\d+) finalized/i,
28
- /finalized.*?(req\d{6})/i,
29
- /Request (req\d+) merged/i,
30
- /merged.*?(req\d{6})/i,
31
- ];
32
- for (const pattern of patterns) {
33
- const match = text.match(pattern);
34
- if (match)
35
- return match[1];
21
+ function readActionServerPort(directory) {
22
+ try {
23
+ const filePath = join(directory, '.alchemy_lab', 'action-server.json');
24
+ if (!existsSync(filePath))
25
+ return DEFAULT_ACTION_SERVER_PORT;
26
+ const data = JSON.parse(readFileSync(filePath, 'utf-8'));
27
+ if (typeof data.port === 'number' && data.port > 0 && data.port < 65536) {
28
+ return data.port;
29
+ }
36
30
  }
37
- return null;
31
+ catch { /* fall through */ }
32
+ return DEFAULT_ACTION_SERVER_PORT;
38
33
  }
39
- /**
40
- * Extract task ID from tool output
41
- */
42
- function extractTaskId(text) {
43
- const match = text.match(/Task\s+(tas\d+).*(?:completed|marked as completed)/i);
44
- return match ? match[1] : null;
45
- }
46
- /**
47
- * Fire-and-forget POST to action server. Does not throw.
48
- */
49
- function postHookEvent(eventType, sessionId, variables, timing) {
50
- fetch(ACTION_SERVER_URL, {
34
+ // Singleton guard — OpenCode calls plugin init twice
35
+ let initializedDirectory = null;
36
+ let cachedHandlers = null;
37
+ function postJson(url, body) {
38
+ fetch(url, {
51
39
  method: 'POST',
52
40
  headers: { 'Content-Type': 'application/json' },
53
- body: JSON.stringify({ eventType, sessionId, variables, timing }),
41
+ body: JSON.stringify(body),
54
42
  }).catch(() => { }); // Fire-and-forget
55
43
  }
56
- /**
57
- * Await POST to action server. Returns silently on failure.
58
- */
59
- async function postHookEventAsync(eventType, sessionId, variables, timing) {
44
+ async function postJsonAwaited(url, body) {
60
45
  try {
61
- await fetch(ACTION_SERVER_URL, {
46
+ await fetch(url, {
62
47
  method: 'POST',
63
48
  headers: { 'Content-Type': 'application/json' },
64
- body: JSON.stringify({ eventType, sessionId, variables, timing }),
49
+ body: JSON.stringify(body),
65
50
  });
66
51
  }
67
- catch {
68
- // Non-fatal
69
- }
52
+ catch { /* non-fatal */ }
70
53
  }
71
54
  /**
72
- * Detect tool type and determine event type + variables from tool execution context.
73
- */
74
- function extractToolEventInfo(toolName, input, resultText, sessionId) {
75
- const events = [];
76
- const baseVars = {
77
- output: resultText || '',
78
- toolName,
79
- sessionId: sessionId || '',
80
- };
81
- const isRequestOps = toolName.includes('request_operations') || toolName.includes('complete_request');
82
- const isTaskOps = toolName.includes('task_operations') || toolName.includes('complete_task');
83
- const isWriteFile = toolName.includes('write_file');
84
- const isReplaceInFiles = toolName.includes('replace_in_files');
85
- const isQuickEdit = toolName.includes('quick_write') || toolName.includes('quick_replace') || toolName.includes('quick_manage');
86
- const isReadFiles = toolName.includes('read_files');
87
- const isSearch = toolName.includes('search');
88
- const isManageFiles = toolName.includes('manage_files');
89
- const isDocumentOps = toolName.includes('document_operations');
90
- const isQueryHistory = toolName.includes('query_history');
91
- if (isRequestOps && resultText) {
92
- const toolArgs = input?.args || input?.input || {};
93
- const operation = toolArgs.operation;
94
- const requestId = extractRequestId(resultText);
95
- // Debug: log what we see for request ops to diagnose hook firing
96
- try {
97
- const fs = require('fs'), path = require('path');
98
- const debugPath = path.join(process.env.ALCHEMY_PROJECT_PATH || process.cwd(), '.alchemy_lab', 'hook-debug.log');
99
- const inputKeys = Object.keys(input || {}).join(',');
100
- const inputInputKeys = Object.keys(input?.input || {}).join(',');
101
- fs.appendFileSync(debugPath, `[${new Date().toISOString()}] request_ops: tool=${toolName} op=${operation} reqId=${requestId} inputKeys=[${inputKeys}] input.inputKeys=[${inputInputKeys}] input.input.op=${input?.input?.operation} input.operation=${input?.operation} result=${resultText?.substring(0, 80)}\n`);
102
- }
103
- catch { }
104
- if (requestId) {
105
- if (operation === 'merge' || operation === 'finalize') {
106
- // Actual merge to main
107
- events.push({
108
- eventType: 'request_completed',
109
- variables: { ...baseVars, requestId },
110
- });
111
- }
112
- else if (operation === 'complete') {
113
- // Phase 1 — work done, ready for merge
114
- events.push({
115
- eventType: 'request_ready_for_merge',
116
- variables: { ...baseVars, requestId },
117
- });
118
- }
119
- }
120
- }
121
- else if (isTaskOps && resultText) {
122
- const taskId = extractTaskId(resultText);
123
- if (taskId) {
124
- events.push({
125
- eventType: 'task_completed',
126
- variables: { ...baseVars, taskId },
127
- });
128
- }
129
- }
130
- else if (isWriteFile) {
131
- const pathMatch = resultText?.match(/(?:Created|Updated|Wrote)\s+(?:file\s+)?([^\s\n]+)/i);
132
- events.push({
133
- eventType: 'write_file',
134
- variables: { ...baseVars, path: pathMatch?.[1] || '' },
135
- });
136
- }
137
- else if (isReplaceInFiles) {
138
- const replaceArgs = input?.args || input?.input || {};
139
- let operations = replaceArgs.operations || [];
140
- if (typeof operations === 'string') {
141
- try {
142
- operations = JSON.parse(operations);
143
- }
144
- catch {
145
- operations = [];
146
- }
147
- }
148
- const paths = operations.map((op) => op.path || op.documentId).filter(Boolean);
149
- events.push({
150
- eventType: 'replace_in_files',
151
- variables: { ...baseVars, paths: paths.join(', '), fileCount: String(paths.length) },
152
- });
153
- }
154
- else if (isQuickEdit) {
155
- const vars = {
156
- ...baseVars,
157
- path: (input?.args || input?.input)?.operationArgs?.path || '',
158
- title: (input?.args || input?.input)?.title || '',
159
- };
160
- const qeRequestMatch = resultText?.match(/\*\*Request:\*\*\s*(req\d+)/i);
161
- if (qeRequestMatch)
162
- vars.requestId = qeRequestMatch[1];
163
- events.push({ eventType: 'quick_edit', variables: vars });
164
- // Quick-edit auto-completes requests — also emit request_completed
165
- if (vars.requestId) {
166
- events.push({
167
- eventType: 'request_completed',
168
- variables: { ...baseVars, requestId: vars.requestId },
169
- });
170
- }
171
- }
172
- else if (isReadFiles) {
173
- let pathsInput = (input?.args || input?.input)?.paths || [];
174
- if (typeof pathsInput === 'string') {
175
- try {
176
- pathsInput = JSON.parse(pathsInput);
177
- }
178
- catch {
179
- pathsInput = [];
180
- }
181
- }
182
- const paths = pathsInput.map((p) => p.path || p.documentId).filter(Boolean);
183
- events.push({
184
- eventType: 'read_files',
185
- variables: { ...baseVars, paths: paths.join(', '), fileCount: String(paths.length) },
186
- });
187
- }
188
- else if (isSearch) {
189
- events.push({
190
- eventType: 'search',
191
- variables: {
192
- ...baseVars,
193
- query: (input?.args || input?.input)?.needle || '',
194
- directory: (input?.args || input?.input)?.directory || '',
195
- fileExtension: (input?.args || input?.input)?.options?.fileExtension || '*',
196
- },
197
- });
198
- }
199
- else if (isManageFiles) {
200
- const manageArgs = input?.args || input?.input || {};
201
- let manageOps = manageArgs.operations || [];
202
- if (typeof manageOps === 'string') {
203
- try {
204
- manageOps = JSON.parse(manageOps);
205
- }
206
- catch {
207
- manageOps = [];
208
- }
209
- }
210
- const opTypes = manageOps.map((op) => op.operationType).filter(Boolean);
211
- events.push({
212
- eventType: 'manage_files',
213
- variables: { ...baseVars, operationTypes: [...new Set(opTypes)].join(', ') },
214
- });
215
- }
216
- else if (isDocumentOps) {
217
- events.push({ eventType: 'document_operations', variables: baseVars });
218
- }
219
- else if (isQueryHistory) {
220
- events.push({ eventType: 'query_history', variables: baseVars });
221
- }
222
- // Always emit tool_executed for any tool
223
- events.push({
224
- eventType: 'tool_executed',
225
- variables: {
226
- tool: toolName,
227
- toolName,
228
- output: resultText || '',
229
- sessionId: sessionId || '',
230
- },
231
- });
232
- return events;
233
- }
234
- /**
235
- * Fetch recent output from a session (for lastOutput variable in session_idle).
55
+ * Fetch recent assistant output from a session. Used by the session.idle
56
+ * handler to populate the lastOutput hook variable; OpenCode's local server
57
+ * is reachable only from the plugin process.
236
58
  */
237
59
  async function fetchRecentSessionOutput(sessionId, directory) {
238
60
  try {
239
- const configPath = (0, path_1.join)(directory, '.alchemy_lab', 'opencode.json');
240
- if (!(0, fs_1.existsSync)(configPath))
61
+ const configPath = join(directory, '.alchemy_lab', 'opencode.json');
62
+ if (!existsSync(configPath))
241
63
  return { output: '' };
242
- const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
64
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
243
65
  const serverUrl = config.port ? `http://127.0.0.1:${config.port}` : null;
244
66
  if (!serverUrl)
245
67
  return { output: '' };
246
- const url = `${serverUrl}/session/${sessionId}/message?limit=5`;
247
- const response = await fetch(url);
68
+ const response = await fetch(`${serverUrl}/session/${sessionId}/message?limit=5`);
248
69
  if (!response.ok)
249
70
  return { output: '' };
250
71
  const messages = await response.json();
251
72
  const assistantMessages = messages.filter(m => m.info.role === 'assistant');
252
- const lastAssistantMessage = assistantMessages[0];
253
73
  const assistantOutput = assistantMessages
254
74
  .flatMap(m => m.parts)
255
75
  .filter(p => p.type === 'text' && p.text)
256
76
  .map(p => p.text)
257
77
  .join('\n');
258
- return { output: assistantOutput, lastMessageId: lastAssistantMessage?.info?.id };
78
+ return { output: assistantOutput, lastMessageId: assistantMessages[0]?.info?.id };
259
79
  }
260
80
  catch {
261
81
  return { output: '' };
262
82
  }
263
83
  }
264
- exports.default = async ({ directory }) => {
84
+ export default async ({ directory }) => {
265
85
  const dir = directory || process.cwd();
266
- // Singleton guard
267
86
  if (initializedDirectory === dir && cachedHandlers) {
268
87
  return cachedHandlers;
269
88
  }
270
89
  initializedDirectory = dir;
90
+ // Discover the action server's actual port from the project's
91
+ // .alchemy_lab/action-server.json. Multiple alchemylab installs on one
92
+ // machine race for 3456; without this discovery hooks for any non-first
93
+ // project would silently land at the wrong action server (or nowhere).
94
+ const actionServerPort = readActionServerPort(dir);
95
+ const actionServerBase = `http://localhost:${actionServerPort}`;
96
+ const HOOK_EVENT_URL = `${actionServerBase}/api/hook-event`;
97
+ const TOOL_EVENT_URL = `${actionServerBase}/api/tool-event`;
271
98
  const handlers = {
272
99
  "tool.execute.before": async (input) => {
273
100
  const toolName = input?.tool || input?.name;
274
101
  if (!toolName)
275
102
  return;
276
- const sessionId = input?.sessionID || '';
277
- const inputText = JSON.stringify(input?.args || input?.input || {}).substring(0, 1000);
278
- postHookEvent('tool_executed', sessionId, {
279
- tool: toolName,
103
+ postJson(TOOL_EVENT_URL, {
280
104
  toolName,
281
- input: inputText,
282
- }, 'before');
105
+ sessionId: input?.sessionID || '',
106
+ input: input?.args || input?.input || {},
107
+ timing: 'before',
108
+ });
283
109
  },
284
110
  "tool.execute.after": async (input, output) => {
285
111
  const toolName = input?.tool || input?.name;
286
112
  if (!toolName)
287
113
  return;
288
- const sessionId = input?.sessionID || '';
289
114
  const resultText = output?.content?.[0]?.text;
290
115
  if (!resultText)
291
116
  return;
292
- const events = extractToolEventInfo(toolName, input, resultText, sessionId);
293
- for (const { eventType, variables } of events) {
294
- await postHookEventAsync(eventType, sessionId, variables, 'after');
295
- }
117
+ await postJsonAwaited(TOOL_EVENT_URL, {
118
+ toolName,
119
+ sessionId: input?.sessionID || '',
120
+ input: input?.args || input?.input || {},
121
+ output: resultText,
122
+ timing: 'after',
123
+ });
296
124
  },
297
125
  event: async ({ event }) => {
298
126
  switch (event.type) {
299
127
  case 'session.idle': {
300
128
  const sessionId = event.properties.sessionID || '';
301
129
  const { output: lastOutput, lastMessageId } = await fetchRecentSessionOutput(sessionId, dir);
302
- // Fire-and-forget — process is dying
303
- postHookEvent('session_idle', sessionId, {
130
+ postJson(HOOK_EVENT_URL, {
131
+ eventType: 'session_idle',
304
132
  sessionId,
305
- lastOutput,
306
- lastMessageId: lastMessageId || '',
133
+ variables: { sessionId, lastOutput, lastMessageId: lastMessageId || '' },
307
134
  });
308
135
  return;
309
136
  }
310
137
  case 'session.created': {
311
- const sessionInfo = event.properties.info;
312
- const sessionId = sessionInfo?.id || '';
138
+ const sessionId = event.properties.info?.id || '';
313
139
  if (!sessionId)
314
140
  return;
315
- // Delay to allow workflow assignment
141
+ // Delay so workflow assignment lands before the hook fires.
316
142
  setTimeout(() => {
317
- postHookEvent('session_created', sessionId, { sessionId });
143
+ postJson(HOOK_EVENT_URL, {
144
+ eventType: 'session_created',
145
+ sessionId,
146
+ variables: { sessionId },
147
+ });
318
148
  }, 500);
319
149
  return;
320
150
  }
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * OpenCode Inject Session Plugin
4
3
  *
@@ -7,30 +6,29 @@
7
6
  *
8
7
  * Hooks into tool.execute.before to modify args before the MCP tool executes.
9
8
  */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- const fs_1 = require("fs");
12
- const os_1 = require("os");
13
- const path_1 = require("path");
9
+ import { appendFileSync } from 'fs';
10
+ import { homedir } from 'os';
11
+ import { join } from 'path';
14
12
  // Log immediately when module is loaded
15
13
  try {
16
- const logFile = (0, path_1.join)((0, os_1.homedir)(), 'inject-session-plugin.log');
17
- (0, fs_1.appendFileSync)(logFile, `[${new Date().toISOString()}] MODULE LOADED - file was imported\n`);
14
+ const logFile = join(homedir(), 'inject-session-plugin.log');
15
+ appendFileSync(logFile, `[${new Date().toISOString()}] MODULE LOADED - file was imported\n`);
18
16
  }
19
17
  catch (e) {
20
18
  // ignore
21
19
  }
22
20
  const debugLog = (msg, data) => {
23
- const logFile = (0, path_1.join)((0, os_1.homedir)(), 'inject-session-plugin.log');
21
+ const logFile = join(homedir(), 'inject-session-plugin.log');
24
22
  const line = `[${new Date().toISOString()}] ${msg}${data ? ' ' + JSON.stringify(data) : ''}\n`;
25
23
  try {
26
- (0, fs_1.appendFileSync)(logFile, line);
24
+ appendFileSync(logFile, line);
27
25
  }
28
26
  catch (e) {
29
27
  // ignore
30
28
  }
31
29
  console.log('[inject-session-plugin]', msg, data || '');
32
30
  };
33
- exports.default = async ({ project, directory }) => {
31
+ export default async ({ project, directory }) => {
34
32
  debugLog('Plugin loaded', { project: project?.id, directory });
35
33
  return {
36
34
  "tool.execute.before": async (context, data) => {
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * OpenCode System Logger Plugin
4
3
  *
@@ -6,15 +5,14 @@
6
5
  * to .alchemy_lab/system-prompt.log. Used to diagnose whether entity context
7
6
  * (from @ mentions) reaches the LLM via the user.system field.
8
7
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- const fs_1 = require("fs");
11
- const path_1 = require("path");
12
- const getLogFile = (directory) => (0, path_1.join)(directory, '.alchemy_lab', 'system-prompt.log');
8
+ import { appendFileSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ const getLogFile = (directory) => join(directory, '.alchemy_lab', 'system-prompt.log');
13
11
  const debugLog = (logFile, msg, data) => {
14
12
  const line = `[${new Date().toISOString()}] ${msg}${data ? ' ' + JSON.stringify(data) : ''}\n`;
15
13
  try {
16
- (0, fs_1.mkdirSync)((0, path_1.join)(logFile, '..'), { recursive: true });
17
- (0, fs_1.appendFileSync)(logFile, line);
14
+ mkdirSync(join(logFile, '..'), { recursive: true });
15
+ appendFileSync(logFile, line);
18
16
  }
19
17
  catch (e) {
20
18
  // ignore
@@ -40,7 +38,7 @@ function stripTaskReferences(system) {
40
38
  system[i] = s;
41
39
  }
42
40
  }
43
- exports.default = async ({ directory }) => {
41
+ export default async ({ directory }) => {
44
42
  const logFile = getLogFile(directory);
45
43
  debugLog(logFile, 'system-logger plugin loaded', { directory });
46
44
  return {
@@ -71,10 +69,10 @@ exports.default = async ({ directory }) => {
71
69
  '',
72
70
  ];
73
71
  try {
74
- (0, fs_1.mkdirSync)((0, path_1.join)(logFile, '..'), { recursive: true });
75
- (0, fs_1.appendFileSync)(logFile, lines.join('\n') + '\n');
72
+ mkdirSync(join(logFile, '..'), { recursive: true });
73
+ appendFileSync(logFile, lines.join('\n') + '\n');
76
74
  // Also write full untruncated prompt to separate file (overwrite each time)
77
- const fullLogFile = (0, path_1.join)(logFile, '..', 'system-prompt-full.log');
75
+ const fullLogFile = join(logFile, '..', 'system-prompt-full.log');
78
76
  const fullLines = [
79
77
  `Timestamp: ${new Date().toISOString()}`,
80
78
  `Session: ${hookInput.sessionID || 'N/A'}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemylab",
3
- "version": "1.3.5",
3
+ "version": "1.4.1",
4
4
  "private": false,
5
5
  "description": "AlchemyLab - agentic coding IDEm",
6
6
  "author": "Testfire Software",
@@ -49,7 +49,7 @@
49
49
  "ws": "^8.14.2",
50
50
  "zod": "^3.22.4",
51
51
  "zod-to-json-schema": "^3.22.3",
52
- "alchemylab-opencode": "^1.0.14"
52
+ "alchemylab-opencode": "^1.0.15"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/chai": "^4.3.5",