alchemylab 1.0.4 → 1.0.5
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 +730 -670
- package/dist/http-server.js +169 -188
- package/dist/index.js +522 -541
- package/dist/mcp-direct.js +733 -752
- package/dist/plugins/hooks/plugin.js +2 -0
- 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
|
@@ -26,6 +26,8 @@ function extractRequestId(text) {
|
|
|
26
26
|
/Request finalized:\s*(req\d+)/i,
|
|
27
27
|
/Request (req\d+) finalized/i,
|
|
28
28
|
/finalized.*?(req\d{6})/i,
|
|
29
|
+
/Request (req\d+) merged/i,
|
|
30
|
+
/merged.*?(req\d{6})/i,
|
|
29
31
|
];
|
|
30
32
|
for (const pattern of patterns) {
|
|
31
33
|
const match = text.match(pattern);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemylab",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AlchemyLab - agentic coding IDEm",
|
|
6
6
|
"author": "Testfire Software",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"build": "npm run build:dev",
|
|
31
31
|
"build:dev": "tsc -p tsconfig.json && chmod +x dist-dev/*.js",
|
|
32
32
|
"build:prod": "webpack --mode=production && chmod +x dist/*.js && npm run copy:plugins",
|
|
33
|
-
"copy:plugins": "cp -
|
|
33
|
+
"copy:plugins": "mkdir -p dist/plugins/hooks/lib && cp dist-dev/plugins/hooks/plugin.js dist/plugins/hooks/ && cp dist-dev/plugins/hooks/lib/*.js dist/plugins/hooks/lib/ && cp dist-dev/plugins/inject-session.js dist/plugins/ && cp dist-dev/plugins/system-logger.js dist/plugins/",
|
|
34
34
|
"build:analyze": "ANALYZE=true webpack --mode=production",
|
|
35
35
|
"test": "npm run build:dev && mocha --require ts-node/register test/**/*.test.ts",
|
|
36
36
|
"test:watch": "mocha --require ts-node/register test/**/*.test.ts --watch --watch-files src/**/*.ts,test/**/*.ts",
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Alchemy Anthropic Auth Plugin for OpenCode
|
|
4
|
-
*
|
|
5
|
-
* Modified version of opencode-anthropic-auth that uses Title Case for tool names
|
|
6
|
-
* to work around Anthropic's detection of third-party clients.
|
|
7
|
-
*
|
|
8
|
-
* Based on opencode-anthropic-auth@0.0.5
|
|
9
|
-
*/
|
|
10
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.default = AlchemyAnthropicAuthPlugin;
|
|
12
|
-
const fs_1 = require("fs");
|
|
13
|
-
const path_1 = require("path");
|
|
14
|
-
// Debug logging to file
|
|
15
|
-
const LOG_FILE = (0, path_1.join)(process.env.HOME || '/tmp', 'anthropic-auth-plugin.log');
|
|
16
|
-
function log(message, data) {
|
|
17
|
-
try {
|
|
18
|
-
const timestamp = new Date().toISOString();
|
|
19
|
-
const line = data
|
|
20
|
-
? `${timestamp} [anthropic-auth] ${message} ${JSON.stringify(data)}\n`
|
|
21
|
-
: `${timestamp} [anthropic-auth] ${message}\n`;
|
|
22
|
-
(0, fs_1.appendFileSync)(LOG_FILE, line);
|
|
23
|
-
}
|
|
24
|
-
catch (e) {
|
|
25
|
-
// ignore logging errors
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Log immediately when module is loaded
|
|
29
|
-
log('MODULE LOADED - anthropic-auth plugin file imported');
|
|
30
|
-
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
31
|
-
/**
|
|
32
|
-
* Convert string to Title Case (first letter uppercase, rest lowercase for each word)
|
|
33
|
-
*/
|
|
34
|
-
function toTitleCase(str) {
|
|
35
|
-
return str.replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase());
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Convert Title Case back to original (lowercase with underscores preserved)
|
|
39
|
-
*/
|
|
40
|
-
function fromTitleCase(str) {
|
|
41
|
-
return str.replace(/\w\S*/g, text => text.toLowerCase());
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Generate PKCE challenge and verifier
|
|
45
|
-
* Simple implementation that doesn't require external dependencies
|
|
46
|
-
*/
|
|
47
|
-
async function generatePKCE() {
|
|
48
|
-
// Generate random verifier (43-128 characters of unreserved URI characters)
|
|
49
|
-
const array = new Uint8Array(32);
|
|
50
|
-
crypto.getRandomValues(array);
|
|
51
|
-
const verifier = btoa(String.fromCharCode(...array))
|
|
52
|
-
.replace(/\+/g, '-')
|
|
53
|
-
.replace(/\//g, '_')
|
|
54
|
-
.replace(/=/g, '');
|
|
55
|
-
// Generate challenge from verifier using SHA-256
|
|
56
|
-
const encoder = new TextEncoder();
|
|
57
|
-
const data = encoder.encode(verifier);
|
|
58
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
59
|
-
const hashArray = new Uint8Array(hashBuffer);
|
|
60
|
-
const challenge = btoa(String.fromCharCode(...hashArray))
|
|
61
|
-
.replace(/\+/g, '-')
|
|
62
|
-
.replace(/\//g, '_')
|
|
63
|
-
.replace(/=/g, '');
|
|
64
|
-
return { verifier, challenge };
|
|
65
|
-
}
|
|
66
|
-
async function authorize(mode) {
|
|
67
|
-
log('authorize called', { mode });
|
|
68
|
-
try {
|
|
69
|
-
const pkce = await generatePKCE();
|
|
70
|
-
log('PKCE generated', { challenge: pkce.challenge?.substring(0, 20) + '...' });
|
|
71
|
-
const url = new URL(`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`);
|
|
72
|
-
url.searchParams.set("code", "true");
|
|
73
|
-
url.searchParams.set("client_id", CLIENT_ID);
|
|
74
|
-
url.searchParams.set("response_type", "code");
|
|
75
|
-
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback");
|
|
76
|
-
url.searchParams.set("scope", "org:create_api_key user:profile user:inference");
|
|
77
|
-
url.searchParams.set("code_challenge", pkce.challenge);
|
|
78
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
79
|
-
url.searchParams.set("state", pkce.verifier);
|
|
80
|
-
log('authorize URL created', { url: url.toString() });
|
|
81
|
-
return {
|
|
82
|
-
url: url.toString(),
|
|
83
|
-
verifier: pkce.verifier,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
log('ERROR in authorize', { error: String(e), stack: e?.stack });
|
|
88
|
-
throw e;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function exchange(code, verifier) {
|
|
92
|
-
log('exchange called');
|
|
93
|
-
try {
|
|
94
|
-
const splits = code.split("#");
|
|
95
|
-
const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
|
96
|
-
method: "POST",
|
|
97
|
-
headers: {
|
|
98
|
-
"Content-Type": "application/json",
|
|
99
|
-
},
|
|
100
|
-
body: JSON.stringify({
|
|
101
|
-
code: splits[0],
|
|
102
|
-
state: splits[1],
|
|
103
|
-
grant_type: "authorization_code",
|
|
104
|
-
client_id: CLIENT_ID,
|
|
105
|
-
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
|
106
|
-
code_verifier: verifier,
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
if (!result.ok) {
|
|
110
|
-
log('exchange failed', { status: result.status });
|
|
111
|
-
return {
|
|
112
|
-
type: "failed",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
const json = await result.json();
|
|
116
|
-
log('exchange successful');
|
|
117
|
-
return {
|
|
118
|
-
type: "success",
|
|
119
|
-
refresh: json.refresh_token,
|
|
120
|
-
access: json.access_token,
|
|
121
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
log('ERROR in exchange', { error: String(e), stack: e?.stack });
|
|
126
|
-
throw e;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function AlchemyAnthropicAuthPlugin({ client }) {
|
|
130
|
-
log('AlchemyAnthropicAuthPlugin called', { hasClient: !!client });
|
|
131
|
-
try {
|
|
132
|
-
return {
|
|
133
|
-
auth: {
|
|
134
|
-
provider: "anthropic",
|
|
135
|
-
async loader(getAuth, provider) {
|
|
136
|
-
log('loader called', { hasGetAuth: !!getAuth, hasProvider: !!provider });
|
|
137
|
-
try {
|
|
138
|
-
const auth = await getAuth();
|
|
139
|
-
log('getAuth returned', { type: auth?.type });
|
|
140
|
-
if (auth.type === "oauth") {
|
|
141
|
-
log('OAuth auth detected, setting up custom fetch');
|
|
142
|
-
// Zero out cost for max plan
|
|
143
|
-
for (const model of Object.values(provider.models || {})) {
|
|
144
|
-
model.cost = {
|
|
145
|
-
input: 0,
|
|
146
|
-
output: 0,
|
|
147
|
-
cache: {
|
|
148
|
-
read: 0,
|
|
149
|
-
write: 0,
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
apiKey: "",
|
|
155
|
-
async fetch(input, init) {
|
|
156
|
-
log('custom fetch called', {
|
|
157
|
-
inputType: typeof input,
|
|
158
|
-
hasInit: !!init
|
|
159
|
-
});
|
|
160
|
-
try {
|
|
161
|
-
const currentAuth = await getAuth();
|
|
162
|
-
if (currentAuth.type !== "oauth") {
|
|
163
|
-
log('Not OAuth, using default fetch');
|
|
164
|
-
return fetch(input, init);
|
|
165
|
-
}
|
|
166
|
-
let authAccess = currentAuth.access;
|
|
167
|
-
if (!authAccess || (currentAuth.expires && currentAuth.expires < Date.now())) {
|
|
168
|
-
log('Token expired or missing, refreshing');
|
|
169
|
-
const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: {
|
|
172
|
-
"Content-Type": "application/json",
|
|
173
|
-
},
|
|
174
|
-
body: JSON.stringify({
|
|
175
|
-
grant_type: "refresh_token",
|
|
176
|
-
refresh_token: currentAuth.refresh,
|
|
177
|
-
client_id: CLIENT_ID,
|
|
178
|
-
}),
|
|
179
|
-
});
|
|
180
|
-
if (!response.ok) {
|
|
181
|
-
log('Token refresh failed', { status: response.status });
|
|
182
|
-
throw new Error(`Token refresh failed: ${response.status}`);
|
|
183
|
-
}
|
|
184
|
-
const json = await response.json();
|
|
185
|
-
log('Token refreshed successfully');
|
|
186
|
-
await client.auth.set({
|
|
187
|
-
path: {
|
|
188
|
-
id: "anthropic",
|
|
189
|
-
},
|
|
190
|
-
body: {
|
|
191
|
-
type: "oauth",
|
|
192
|
-
refresh: json.refresh_token,
|
|
193
|
-
access: json.access_token,
|
|
194
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
authAccess = json.access_token;
|
|
198
|
-
}
|
|
199
|
-
const requestInit = init ?? {};
|
|
200
|
-
const requestHeaders = new Headers();
|
|
201
|
-
if (input instanceof Request) {
|
|
202
|
-
input.headers.forEach((value, key) => {
|
|
203
|
-
requestHeaders.set(key, value);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
if (requestInit.headers) {
|
|
207
|
-
if (requestInit.headers instanceof Headers) {
|
|
208
|
-
requestInit.headers.forEach((value, key) => {
|
|
209
|
-
requestHeaders.set(key, value);
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
else if (Array.isArray(requestInit.headers)) {
|
|
213
|
-
for (const [key, value] of requestInit.headers) {
|
|
214
|
-
if (typeof value !== "undefined") {
|
|
215
|
-
requestHeaders.set(key, String(value));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
for (const [key, value] of Object.entries(requestInit.headers)) {
|
|
221
|
-
if (typeof value !== "undefined") {
|
|
222
|
-
requestHeaders.set(key, String(value));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const incomingBeta = requestHeaders.get("anthropic-beta") || "";
|
|
228
|
-
const incomingBetasList = incomingBeta
|
|
229
|
-
.split(",")
|
|
230
|
-
.map((b) => b.trim())
|
|
231
|
-
.filter(Boolean);
|
|
232
|
-
const includeClaudeCode = incomingBetasList.includes("claude-code-20250219");
|
|
233
|
-
const mergedBetas = [
|
|
234
|
-
"oauth-2025-04-20",
|
|
235
|
-
"interleaved-thinking-2025-05-14",
|
|
236
|
-
...(includeClaudeCode ? ["claude-code-20250219"] : []),
|
|
237
|
-
].join(",");
|
|
238
|
-
requestHeaders.set("authorization", `Bearer ${authAccess}`);
|
|
239
|
-
requestHeaders.set("anthropic-beta", mergedBetas);
|
|
240
|
-
requestHeaders.set("user-agent", "claude-cli/2.1.2 (external, cli)");
|
|
241
|
-
requestHeaders.delete("x-api-key");
|
|
242
|
-
// Transform tool names to Title Case
|
|
243
|
-
let body = requestInit.body;
|
|
244
|
-
if (body && typeof body === "string") {
|
|
245
|
-
try {
|
|
246
|
-
const parsed = JSON.parse(body);
|
|
247
|
-
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
248
|
-
log('Transforming tool names to Title Case', {
|
|
249
|
-
count: parsed.tools.length,
|
|
250
|
-
names: parsed.tools.map((t) => t.name).slice(0, 5)
|
|
251
|
-
});
|
|
252
|
-
parsed.tools = parsed.tools.map((tool) => ({
|
|
253
|
-
...tool,
|
|
254
|
-
name: tool.name ? toTitleCase(tool.name) : tool.name,
|
|
255
|
-
}));
|
|
256
|
-
body = JSON.stringify(parsed);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch (e) {
|
|
260
|
-
log('Error parsing body for tool transformation', { error: String(e) });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
let requestInput = input;
|
|
264
|
-
let requestUrl = null;
|
|
265
|
-
try {
|
|
266
|
-
if (typeof input === "string" || input instanceof URL) {
|
|
267
|
-
requestUrl = new URL(input.toString());
|
|
268
|
-
}
|
|
269
|
-
else if (input instanceof Request) {
|
|
270
|
-
requestUrl = new URL(input.url);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
requestUrl = null;
|
|
275
|
-
}
|
|
276
|
-
if (requestUrl &&
|
|
277
|
-
requestUrl.pathname === "/v1/messages" &&
|
|
278
|
-
!requestUrl.searchParams.has("beta")) {
|
|
279
|
-
requestUrl.searchParams.set("beta", "true");
|
|
280
|
-
requestInput =
|
|
281
|
-
input instanceof Request
|
|
282
|
-
? new Request(requestUrl.toString(), input)
|
|
283
|
-
: requestUrl;
|
|
284
|
-
}
|
|
285
|
-
log('Making authenticated request', {
|
|
286
|
-
url: requestUrl?.toString()?.substring(0, 100)
|
|
287
|
-
});
|
|
288
|
-
const response = await fetch(requestInput, {
|
|
289
|
-
...requestInit,
|
|
290
|
-
body,
|
|
291
|
-
headers: requestHeaders,
|
|
292
|
-
});
|
|
293
|
-
log('Got response', { status: response.status });
|
|
294
|
-
// Transform streaming response to convert Title Case tool names back
|
|
295
|
-
if (response.body) {
|
|
296
|
-
const reader = response.body.getReader();
|
|
297
|
-
const decoder = new TextDecoder();
|
|
298
|
-
const encoder = new TextEncoder();
|
|
299
|
-
const stream = new ReadableStream({
|
|
300
|
-
async pull(controller) {
|
|
301
|
-
const { done, value } = await reader.read();
|
|
302
|
-
if (done) {
|
|
303
|
-
controller.close();
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
let text = decoder.decode(value, { stream: true });
|
|
307
|
-
// Convert Title Case tool names back to lowercase
|
|
308
|
-
text = text.replace(/"name"\s*:\s*"([A-Z][^"]+)"/g, (_match, name) => {
|
|
309
|
-
return `"name": "${fromTitleCase(name)}"`;
|
|
310
|
-
});
|
|
311
|
-
controller.enqueue(encoder.encode(text));
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
return new Response(stream, {
|
|
315
|
-
status: response.status,
|
|
316
|
-
statusText: response.statusText,
|
|
317
|
-
headers: response.headers,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return response;
|
|
321
|
-
}
|
|
322
|
-
catch (e) {
|
|
323
|
-
log('ERROR in custom fetch', { error: String(e), stack: e?.stack });
|
|
324
|
-
throw e;
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
log('Not OAuth auth, returning empty config');
|
|
330
|
-
return {};
|
|
331
|
-
}
|
|
332
|
-
catch (e) {
|
|
333
|
-
log('ERROR in loader', { error: String(e), stack: e?.stack });
|
|
334
|
-
throw e;
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
|
-
methods: [
|
|
338
|
-
{
|
|
339
|
-
label: "Claude Pro/Max",
|
|
340
|
-
type: "oauth",
|
|
341
|
-
authorize: async () => {
|
|
342
|
-
log('Claude Pro/Max authorize method called');
|
|
343
|
-
const { url, verifier } = await authorize("max");
|
|
344
|
-
return {
|
|
345
|
-
url: url,
|
|
346
|
-
instructions: "Paste the authorization code here: ",
|
|
347
|
-
method: "code",
|
|
348
|
-
callback: async (code) => {
|
|
349
|
-
log('Claude Pro/Max callback called');
|
|
350
|
-
const credentials = await exchange(code, verifier);
|
|
351
|
-
return credentials;
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
},
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
label: "Create an API Key",
|
|
358
|
-
type: "oauth",
|
|
359
|
-
authorize: async () => {
|
|
360
|
-
log('Create an API Key authorize method called');
|
|
361
|
-
const { url, verifier } = await authorize("console");
|
|
362
|
-
return {
|
|
363
|
-
url: url,
|
|
364
|
-
instructions: "Paste the authorization code here: ",
|
|
365
|
-
method: "code",
|
|
366
|
-
callback: async (code) => {
|
|
367
|
-
log('Create an API Key callback called');
|
|
368
|
-
const credentials = await exchange(code, verifier);
|
|
369
|
-
if (credentials.type === "failed")
|
|
370
|
-
return credentials;
|
|
371
|
-
const result = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, {
|
|
372
|
-
method: "POST",
|
|
373
|
-
headers: {
|
|
374
|
-
"Content-Type": "application/json",
|
|
375
|
-
authorization: `Bearer ${credentials.access}`,
|
|
376
|
-
},
|
|
377
|
-
}).then((r) => r.json());
|
|
378
|
-
log('API key created');
|
|
379
|
-
return { type: "success", key: result.raw_key };
|
|
380
|
-
},
|
|
381
|
-
};
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
provider: "anthropic",
|
|
386
|
-
label: "Manually enter API Key",
|
|
387
|
-
type: "api",
|
|
388
|
-
},
|
|
389
|
-
],
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
catch (e) {
|
|
394
|
-
log('ERROR in AlchemyAnthropicAuthPlugin', { error: String(e), stack: e?.stack });
|
|
395
|
-
throw e;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
log('Plugin module fully loaded');
|
|
399
|
-
//# sourceMappingURL=anthropic-auth.js.map
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* OpenCode Auto-Journal Plugin
|
|
4
|
-
*
|
|
5
|
-
* Automatically generates journal entries when requests are completed.
|
|
6
|
-
* Hooks into tool.execute.after to detect request-operations complete calls.
|
|
7
|
-
*
|
|
8
|
-
* Checks .alchemy_lab/config.json for autoJournal setting before triggering.
|
|
9
|
-
*/
|
|
10
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
const child_process_1 = require("child_process");
|
|
12
|
-
const path_1 = require("path");
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
|
-
/**
|
|
15
|
-
* Check if autoJournal is enabled in config
|
|
16
|
-
*/
|
|
17
|
-
function isAutoJournalEnabled(directory) {
|
|
18
|
-
try {
|
|
19
|
-
const configPath = (0, path_1.join)(directory || process.cwd(), '.alchemy_lab', 'config.json');
|
|
20
|
-
const configContent = (0, fs_1.readFileSync)(configPath, 'utf8');
|
|
21
|
-
const config = JSON.parse(configContent);
|
|
22
|
-
return config.autoJournal === true;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// Config doesn't exist or can't be read - default to false
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
exports.default = async ({ project, directory }) => {
|
|
30
|
-
return {
|
|
31
|
-
"tool.execute.after": async (input, output) => {
|
|
32
|
-
try {
|
|
33
|
-
// Check if this is request-operations
|
|
34
|
-
const toolName = input?.tool || input?.name;
|
|
35
|
-
if (!toolName || !toolName.includes('request-operations')) {
|
|
36
|
-
return; // Not the tool we're looking for
|
|
37
|
-
}
|
|
38
|
-
// Check if autoJournal is enabled in config
|
|
39
|
-
if (!isAutoJournalEnabled(directory)) {
|
|
40
|
-
return; // Auto-journal is disabled
|
|
41
|
-
}
|
|
42
|
-
// Check the output content for the operation result
|
|
43
|
-
if (output?.content && Array.isArray(output.content)) {
|
|
44
|
-
if (output.content[0]?.text) {
|
|
45
|
-
const resultText = output.content[0].text;
|
|
46
|
-
// Check if this was a complete operation - look for any message about completing a request
|
|
47
|
-
// Format is "Request completed: req123" or similar
|
|
48
|
-
const completionMatch = resultText.match(/Request completed:\s*(req\d+)/i);
|
|
49
|
-
if (completionMatch) {
|
|
50
|
-
const requestId = completionMatch[1];
|
|
51
|
-
// Build the prompt for opencode
|
|
52
|
-
const prompt = `Generate a journal entry for request ${requestId}.
|
|
53
|
-
|
|
54
|
-
Steps:
|
|
55
|
-
1. Use ${toolName.replace(/_request_operations$/, '__generate-request-journal')} tool with requestId: ${requestId}
|
|
56
|
-
2. Write a concise narrative journal entry based on the returned data covering:
|
|
57
|
-
- What was implemented and why
|
|
58
|
-
- Key decisions and trade-offs made
|
|
59
|
-
- Challenges encountered and solutions
|
|
60
|
-
- Files modified and their purposes
|
|
61
|
-
- How acceptance criteria were met
|
|
62
|
-
3. Save as JOURNAL-${requestId}.md using ${toolName.replace(/_request_operations$/, '__document-operations')} (operation: create, type: Report)
|
|
63
|
-
|
|
64
|
-
Keep it story-focused and informative.`;
|
|
65
|
-
// Spawn opencode in background (detached) using 'run' command with journal mode
|
|
66
|
-
// OpenCode will find opencode.json in cwd automatically
|
|
67
|
-
const child = (0, child_process_1.spawn)("opencode", ["run", "--agent", "journal", prompt], {
|
|
68
|
-
detached: true,
|
|
69
|
-
stdio: "ignore",
|
|
70
|
-
cwd: directory || process.cwd()
|
|
71
|
-
});
|
|
72
|
-
child.unref();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
// Silently ignore errors to avoid breaking the main session
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
//# sourceMappingURL=auto-journal.js.map
|
|
@@ -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
|