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
|
@@ -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);
|
|
@@ -87,12 +89,33 @@ function extractToolEventInfo(toolName, input, resultText, sessionId) {
|
|
|
87
89
|
const isDocumentOps = toolName.includes('document_operations');
|
|
88
90
|
const isQueryHistory = toolName.includes('query_history');
|
|
89
91
|
if (isRequestOps && resultText) {
|
|
92
|
+
const toolArgs = input?.args || input?.input || {};
|
|
93
|
+
const operation = toolArgs.operation;
|
|
90
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 { }
|
|
91
104
|
if (requestId) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
else if (isTaskOps && resultText) {
|
|
@@ -112,7 +135,16 @@ function extractToolEventInfo(toolName, input, resultText, sessionId) {
|
|
|
112
135
|
});
|
|
113
136
|
}
|
|
114
137
|
else if (isReplaceInFiles) {
|
|
115
|
-
const
|
|
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
|
+
}
|
|
116
148
|
const paths = operations.map((op) => op.path || op.documentId).filter(Boolean);
|
|
117
149
|
events.push({
|
|
118
150
|
eventType: 'replace_in_files',
|
|
@@ -122,8 +154,8 @@ function extractToolEventInfo(toolName, input, resultText, sessionId) {
|
|
|
122
154
|
else if (isQuickEdit) {
|
|
123
155
|
const vars = {
|
|
124
156
|
...baseVars,
|
|
125
|
-
path: input?.input?.operationArgs?.path || '',
|
|
126
|
-
title: input?.input?.title || '',
|
|
157
|
+
path: (input?.args || input?.input)?.operationArgs?.path || '',
|
|
158
|
+
title: (input?.args || input?.input)?.title || '',
|
|
127
159
|
};
|
|
128
160
|
const qeRequestMatch = resultText?.match(/\*\*Request:\*\*\s*(req\d+)/i);
|
|
129
161
|
if (qeRequestMatch)
|
|
@@ -138,7 +170,15 @@ function extractToolEventInfo(toolName, input, resultText, sessionId) {
|
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
else if (isReadFiles) {
|
|
141
|
-
|
|
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
|
+
}
|
|
142
182
|
const paths = pathsInput.map((p) => p.path || p.documentId).filter(Boolean);
|
|
143
183
|
events.push({
|
|
144
184
|
eventType: 'read_files',
|
|
@@ -150,15 +190,24 @@ function extractToolEventInfo(toolName, input, resultText, sessionId) {
|
|
|
150
190
|
eventType: 'search',
|
|
151
191
|
variables: {
|
|
152
192
|
...baseVars,
|
|
153
|
-
query: input?.input?.needle || '',
|
|
154
|
-
directory: input?.input?.directory || '',
|
|
155
|
-
fileExtension: input?.input?.options?.fileExtension || '*',
|
|
193
|
+
query: (input?.args || input?.input)?.needle || '',
|
|
194
|
+
directory: (input?.args || input?.input)?.directory || '',
|
|
195
|
+
fileExtension: (input?.args || input?.input)?.options?.fileExtension || '*',
|
|
156
196
|
},
|
|
157
197
|
});
|
|
158
198
|
}
|
|
159
199
|
else if (isManageFiles) {
|
|
160
|
-
const
|
|
161
|
-
|
|
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);
|
|
162
211
|
events.push({
|
|
163
212
|
eventType: 'manage_files',
|
|
164
213
|
variables: { ...baseVars, operationTypes: [...new Set(opTypes)].join(', ') },
|
|
@@ -225,7 +274,7 @@ exports.default = async ({ directory }) => {
|
|
|
225
274
|
if (!toolName)
|
|
226
275
|
return;
|
|
227
276
|
const sessionId = input?.sessionID || '';
|
|
228
|
-
const inputText = JSON.stringify(input?.input || {}).substring(0, 1000);
|
|
277
|
+
const inputText = JSON.stringify(input?.args || input?.input || {}).substring(0, 1000);
|
|
229
278
|
postHookEvent('tool_executed', sessionId, {
|
|
230
279
|
tool: toolName,
|
|
231
280
|
toolName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemylab",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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
|