alchemylab 1.3.4 → 1.4.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.
@@ -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,269 +1,69 @@
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");
9
+ import { join } from "path";
10
+ import { readFileSync, existsSync } from "fs";
13
11
  const ACTION_SERVER_PORT = 3456;
14
- const ACTION_SERVER_URL = `http://localhost:${ACTION_SERVER_PORT}/api/hook-event`;
12
+ const ACTION_SERVER_BASE = `http://localhost:${ACTION_SERVER_PORT}`;
13
+ const HOOK_EVENT_URL = `${ACTION_SERVER_BASE}/api/hook-event`;
14
+ const TOOL_EVENT_URL = `${ACTION_SERVER_BASE}/api/tool-event`;
15
15
  // Singleton guard — OpenCode calls plugin init twice
16
16
  let initializedDirectory = null;
17
17
  let cachedHandlers = null;
18
- /**
19
- * Extract request ID from tool output
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];
36
- }
37
- return null;
38
- }
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, {
18
+ function postJson(url, body) {
19
+ fetch(url, {
51
20
  method: 'POST',
52
21
  headers: { 'Content-Type': 'application/json' },
53
- body: JSON.stringify({ eventType, sessionId, variables, timing }),
22
+ body: JSON.stringify(body),
54
23
  }).catch(() => { }); // Fire-and-forget
55
24
  }
56
- /**
57
- * Await POST to action server. Returns silently on failure.
58
- */
59
- async function postHookEventAsync(eventType, sessionId, variables, timing) {
25
+ async function postJsonAwaited(url, body) {
60
26
  try {
61
- await fetch(ACTION_SERVER_URL, {
27
+ await fetch(url, {
62
28
  method: 'POST',
63
29
  headers: { 'Content-Type': 'application/json' },
64
- body: JSON.stringify({ eventType, sessionId, variables, timing }),
30
+ body: JSON.stringify(body),
65
31
  });
66
32
  }
67
- catch {
68
- // Non-fatal
69
- }
33
+ catch { /* non-fatal */ }
70
34
  }
71
35
  /**
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).
36
+ * Fetch recent assistant output from a session. Used by the session.idle
37
+ * handler to populate the lastOutput hook variable; OpenCode's local server
38
+ * is reachable only from the plugin process.
236
39
  */
237
40
  async function fetchRecentSessionOutput(sessionId, directory) {
238
41
  try {
239
- const configPath = (0, path_1.join)(directory, '.alchemy_lab', 'opencode.json');
240
- if (!(0, fs_1.existsSync)(configPath))
42
+ const configPath = join(directory, '.alchemy_lab', 'opencode.json');
43
+ if (!existsSync(configPath))
241
44
  return { output: '' };
242
- const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
45
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
243
46
  const serverUrl = config.port ? `http://127.0.0.1:${config.port}` : null;
244
47
  if (!serverUrl)
245
48
  return { output: '' };
246
- const url = `${serverUrl}/session/${sessionId}/message?limit=5`;
247
- const response = await fetch(url);
49
+ const response = await fetch(`${serverUrl}/session/${sessionId}/message?limit=5`);
248
50
  if (!response.ok)
249
51
  return { output: '' };
250
52
  const messages = await response.json();
251
53
  const assistantMessages = messages.filter(m => m.info.role === 'assistant');
252
- const lastAssistantMessage = assistantMessages[0];
253
54
  const assistantOutput = assistantMessages
254
55
  .flatMap(m => m.parts)
255
56
  .filter(p => p.type === 'text' && p.text)
256
57
  .map(p => p.text)
257
58
  .join('\n');
258
- return { output: assistantOutput, lastMessageId: lastAssistantMessage?.info?.id };
59
+ return { output: assistantOutput, lastMessageId: assistantMessages[0]?.info?.id };
259
60
  }
260
61
  catch {
261
62
  return { output: '' };
262
63
  }
263
64
  }
264
- exports.default = async ({ directory }) => {
65
+ export default async ({ directory }) => {
265
66
  const dir = directory || process.cwd();
266
- // Singleton guard
267
67
  if (initializedDirectory === dir && cachedHandlers) {
268
68
  return cachedHandlers;
269
69
  }
@@ -273,48 +73,51 @@ exports.default = async ({ directory }) => {
273
73
  const toolName = input?.tool || input?.name;
274
74
  if (!toolName)
275
75
  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,
76
+ postJson(TOOL_EVENT_URL, {
280
77
  toolName,
281
- input: inputText,
282
- }, 'before');
78
+ sessionId: input?.sessionID || '',
79
+ input: input?.args || input?.input || {},
80
+ timing: 'before',
81
+ });
283
82
  },
284
83
  "tool.execute.after": async (input, output) => {
285
84
  const toolName = input?.tool || input?.name;
286
85
  if (!toolName)
287
86
  return;
288
- const sessionId = input?.sessionID || '';
289
87
  const resultText = output?.content?.[0]?.text;
290
88
  if (!resultText)
291
89
  return;
292
- const events = extractToolEventInfo(toolName, input, resultText, sessionId);
293
- for (const { eventType, variables } of events) {
294
- await postHookEventAsync(eventType, sessionId, variables, 'after');
295
- }
90
+ await postJsonAwaited(TOOL_EVENT_URL, {
91
+ toolName,
92
+ sessionId: input?.sessionID || '',
93
+ input: input?.args || input?.input || {},
94
+ output: resultText,
95
+ timing: 'after',
96
+ });
296
97
  },
297
98
  event: async ({ event }) => {
298
99
  switch (event.type) {
299
100
  case 'session.idle': {
300
101
  const sessionId = event.properties.sessionID || '';
301
102
  const { output: lastOutput, lastMessageId } = await fetchRecentSessionOutput(sessionId, dir);
302
- // Fire-and-forget — process is dying
303
- postHookEvent('session_idle', sessionId, {
103
+ postJson(HOOK_EVENT_URL, {
104
+ eventType: 'session_idle',
304
105
  sessionId,
305
- lastOutput,
306
- lastMessageId: lastMessageId || '',
106
+ variables: { sessionId, lastOutput, lastMessageId: lastMessageId || '' },
307
107
  });
308
108
  return;
309
109
  }
310
110
  case 'session.created': {
311
- const sessionInfo = event.properties.info;
312
- const sessionId = sessionInfo?.id || '';
111
+ const sessionId = event.properties.info?.id || '';
313
112
  if (!sessionId)
314
113
  return;
315
- // Delay to allow workflow assignment
114
+ // Delay so workflow assignment lands before the hook fires.
316
115
  setTimeout(() => {
317
- postHookEvent('session_created', sessionId, { sessionId });
116
+ postJson(HOOK_EVENT_URL, {
117
+ eventType: 'session_created',
118
+ sessionId,
119
+ variables: { sessionId },
120
+ });
318
121
  }, 500);
319
122
  return;
320
123
  }
@@ -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.4",
3
+ "version": "1.4.0",
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",