alchemylab 1.4.14 → 2.0.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.
- package/README.md +39 -0
- package/dist/129.js +25 -0
- package/dist/19.js +260 -0
- package/dist/{800.js → 237.js} +5 -5
- package/dist/301.js +138 -0
- package/dist/380.js +17 -0
- package/dist/403.js +4 -0
- package/dist/491.js +31 -0
- package/dist/540.js +56 -0
- package/dist/554.js +26 -0
- package/dist/{60.js → 565.js} +8 -8
- package/dist/636.js +11 -0
- package/dist/674.js +15 -0
- package/dist/729.js +89 -0
- package/dist/796.js +47 -0
- package/dist/cli.js +6571 -1623
- package/dist/document-textconv.js +484 -0
- package/dist/http-server.js +7243 -5436
- package/dist/index.js +7254 -5457
- package/dist/mcp-direct.js +7147 -5301
- package/dist/plugins/hooks/plugin.js +1 -157
- package/dist/plugins/inject-session.js +1 -66
- package/dist/plugins/system-logger.js +1 -93
- package/package.json +26 -15
- package/dist/414.js +0 -9
- package/dist/478.js +0 -109
- package/dist/939.js +0 -17
- package/dist/994.js +0 -126
- package/dist/mcp-cli-bridge.js +0 -96
- package/dist/plugins/hooks/lib/condition-evaluator.js +0 -114
- package/dist/plugins/hooks/lib/dedup-utils.js +0 -94
- package/dist/plugins/hooks/lib/hook-utils.js +0 -90
|
@@ -1,157 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* OpenCode Event Hooks Plugin — Raw Event Emitter
|
|
3
|
-
*
|
|
4
|
-
* Detects events from OpenCode (tool execution, session lifecycle) and POSTs
|
|
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.
|
|
8
|
-
*/
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { readFileSync, existsSync } from "fs";
|
|
11
|
-
const DEFAULT_ACTION_SERVER_PORT = 3456;
|
|
12
|
-
/**
|
|
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
|
-
*/
|
|
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
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch { /* fall through */ }
|
|
32
|
-
return DEFAULT_ACTION_SERVER_PORT;
|
|
33
|
-
}
|
|
34
|
-
// Singleton guard — OpenCode calls plugin init twice
|
|
35
|
-
let initializedDirectory = null;
|
|
36
|
-
let cachedHandlers = null;
|
|
37
|
-
function postJson(url, body) {
|
|
38
|
-
fetch(url, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
|
-
body: JSON.stringify(body),
|
|
42
|
-
}).catch(() => { }); // Fire-and-forget
|
|
43
|
-
}
|
|
44
|
-
async function postJsonAwaited(url, body) {
|
|
45
|
-
try {
|
|
46
|
-
await fetch(url, {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify(body),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch { /* non-fatal */ }
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
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.
|
|
58
|
-
*/
|
|
59
|
-
async function fetchRecentSessionOutput(sessionId, directory) {
|
|
60
|
-
try {
|
|
61
|
-
const configPath = join(directory, '.alchemy_lab', 'opencode.json');
|
|
62
|
-
if (!existsSync(configPath))
|
|
63
|
-
return { output: '' };
|
|
64
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
65
|
-
const serverUrl = config.port ? `http://127.0.0.1:${config.port}` : null;
|
|
66
|
-
if (!serverUrl)
|
|
67
|
-
return { output: '' };
|
|
68
|
-
const response = await fetch(`${serverUrl}/session/${sessionId}/message?limit=5`);
|
|
69
|
-
if (!response.ok)
|
|
70
|
-
return { output: '' };
|
|
71
|
-
const messages = await response.json();
|
|
72
|
-
const assistantMessages = messages.filter(m => m.info.role === 'assistant');
|
|
73
|
-
const assistantOutput = assistantMessages
|
|
74
|
-
.flatMap(m => m.parts)
|
|
75
|
-
.filter(p => p.type === 'text' && p.text)
|
|
76
|
-
.map(p => p.text)
|
|
77
|
-
.join('\n');
|
|
78
|
-
return { output: assistantOutput, lastMessageId: assistantMessages[0]?.info?.id };
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return { output: '' };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export default async ({ directory }) => {
|
|
85
|
-
const dir = directory || process.cwd();
|
|
86
|
-
if (initializedDirectory === dir && cachedHandlers) {
|
|
87
|
-
return cachedHandlers;
|
|
88
|
-
}
|
|
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`;
|
|
98
|
-
const handlers = {
|
|
99
|
-
"tool.execute.before": async (input) => {
|
|
100
|
-
const toolName = input?.tool || input?.name;
|
|
101
|
-
if (!toolName)
|
|
102
|
-
return;
|
|
103
|
-
postJson(TOOL_EVENT_URL, {
|
|
104
|
-
toolName,
|
|
105
|
-
sessionId: input?.sessionID || '',
|
|
106
|
-
input: input?.args || input?.input || {},
|
|
107
|
-
timing: 'before',
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
"tool.execute.after": async (input, output) => {
|
|
111
|
-
const toolName = input?.tool || input?.name;
|
|
112
|
-
if (!toolName)
|
|
113
|
-
return;
|
|
114
|
-
const resultText = output?.content?.[0]?.text;
|
|
115
|
-
if (!resultText)
|
|
116
|
-
return;
|
|
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
|
-
});
|
|
124
|
-
},
|
|
125
|
-
event: async ({ event }) => {
|
|
126
|
-
switch (event.type) {
|
|
127
|
-
case 'session.idle': {
|
|
128
|
-
const sessionId = event.properties.sessionID || '';
|
|
129
|
-
const { output: lastOutput, lastMessageId } = await fetchRecentSessionOutput(sessionId, dir);
|
|
130
|
-
postJson(HOOK_EVENT_URL, {
|
|
131
|
-
eventType: 'session_idle',
|
|
132
|
-
sessionId,
|
|
133
|
-
variables: { sessionId, lastOutput, lastMessageId: lastMessageId || '' },
|
|
134
|
-
});
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
case 'session.created': {
|
|
138
|
-
const sessionId = event.properties.info?.id || '';
|
|
139
|
-
if (!sessionId)
|
|
140
|
-
return;
|
|
141
|
-
// Delay so workflow assignment lands before the hook fires.
|
|
142
|
-
setTimeout(() => {
|
|
143
|
-
postJson(HOOK_EVENT_URL, {
|
|
144
|
-
eventType: 'session_created',
|
|
145
|
-
sessionId,
|
|
146
|
-
variables: { sessionId },
|
|
147
|
-
});
|
|
148
|
-
}, 500);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
cachedHandlers = handlers;
|
|
155
|
-
return handlers;
|
|
156
|
-
};
|
|
157
|
-
//# sourceMappingURL=plugin.js.map
|
|
1
|
+
import*as t from"path";import{join as e}from"path";import{readFileSync as n,existsSync as o}from"fs";const a=2e5;function s(t,e,n){if(e.length<=a)return null;const o=(e.length/1048576).toFixed(1);return[`[AlchemyLab did not pass this result on: ${e.length.toLocaleString()} characters (about ${o} MB).]`,"",`"${t}" returned more than fits usefully in context, so it was held back rather than`,"filling the window. The first part of it was:","",e.slice(0,2e3),"","[...held back...]","",n?`The whole result is stored: ${n}\nRead the part you need with \`result_inspect\` (operation "read", that address, plus a line range or \`find\`), or ask this connection for a summary or a narrower query.`:"Ask this connection for a path, a summary, or a narrower query instead of the whole result."].join("\n")}let r=null,i=null;function c(t,e){fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}).catch((e=>{console.warn("[hooks-plugin] hook event delivery failed",t,e instanceof Error?e.message:String(e))}))}async function l(t,e){try{await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),signal:AbortSignal.timeout(3e4)})}catch(e){e instanceof Error&&"TimeoutError"===e.name&&console.warn(`[hooks-plugin] after-event POST to ${t} aborted after 30000ms; blocking hooks may not have run`)}}let u=null;async function h(t){if(u&&Date.now()-u.at<6e4)return u.names;try{const e=await fetch(`${t}/api/mcp/namespaces`,{signal:AbortSignal.timeout(5e3)}),n=await e.json(),o=n?.data?.namespaces??[];return u={at:Date.now(),names:Array.isArray(o)?o:[]},u.names}catch{return u={at:Date.now(),names:u?.names??[]},u.names}}const p=new Set(["read","write","edit","patch","multiedit","apply_patch","bash","grep","glob","list","ls","todowrite","todoread","webfetch","websearch","task","skill","invalid"]);function d(t,e){return e.some((e=>t.startsWith(`${e}_`)&&t.length>e.length+1))}async function f(t,e){return!!d(e,await h(t))||!e.startsWith("alchemy_")&&!p.has(e)&&(u=null,d(e,await h(t)))}export default async({directory:u})=>{const h=u||process.cwd();if(r===h&&i)return i;r=h;const p=function(t){try{const a=e(t,".alchemy_lab","action-server.json");if(!o(a))return 3456;const s=JSON.parse(n(a,"utf-8"));if("number"==typeof s.port&&s.port>0&&s.port<65536)return s.port}catch{}return 3456}(h),d=`http://localhost:${p}`;!async function(t,e,n){try{const o=await fetch(`http://localhost:${e}/health`,{signal:AbortSignal.timeout(1500)});if(!o.ok)return!1;const a=await o.json(),s="string"==typeof a?.projectPath?a.projectPath:"",r=!!s&&(s===t||s.endsWith(t)||t.endsWith(s));return r||n?.(`[alchemy hooks] port ${e} serves ${s||"an unknown project"}, not ${t}: hooks would fire into the wrong project`),r}catch{return n?.(`[alchemy hooks] nothing answered /health on port ${e}: hook events will not be delivered`),!1}}(h,p,(t=>console.error(t)));const g=`${d}/api/hook-event`,m=`${d}/api/tool-event`,w={"tool.execute.before":async(e,n)=>{const o=e?.tool||e?.name;if(!o)return;const a=e?.args||e?.input||{},s=e?.sessionID||"";"bash"===o&&n?.args&&"object"==typeof n.args&&await async function(e,n,o,a){const s="string"==typeof a.workdir&&a.workdir.trim()?a.workdir.trim():".";if(function(e,n){const o=t.resolve(e),a=t.resolve(o,n);if(a!==o&&!a.startsWith(o+t.sep))return!1;const s=t.join(o,".alchemy_lab");return!(a===s||a.startsWith(s+t.sep))}(n,s))try{const r=await fetch(`${e}/api/session-workdir?sessionId=${encodeURIComponent(o)}`,{signal:AbortSignal.timeout(5e3)}),i=r.ok?await r.json():null,c=i?.data?.workdir;if("string"!=typeof c||!c)return;const l=t.relative(t.resolve(n),t.resolve(n,s));a.workdir=l&&!l.startsWith("..")?t.join(c,l):c}catch{}}(d,h,s,n.args),await f(d,o)&&await async function(t,e,n,o,a){let s;try{const r=await fetch(`${t}/api/mcp/gate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({toolName:e,sessionId:n,args:o,callId:a}),signal:AbortSignal.timeout(66e4)}),i=await r.json();if(s=i?.data,!r.ok||!s)throw new Error(i?.error||`gate returned HTTP ${r.status}`)}catch(t){const n=t instanceof Error?t.message:String(t);throw console.warn("[hooks-plugin] MCP gate unreachable, refusing the call",e,n),new Error(`${e} was not run: its approval gate could not be reached (${n}).`)}if(!s.allow)throw new Error(`${e} was not run: ${s.reason}.`)}(d,o,s,a,e?.callID||""),c(m,{toolName:o,sessionId:s,input:a,timing:"before"})},"tool.execute.after":async(t,e)=>{const n=t?.tool||t?.name;if(!n)return;const o=e?.content?.[0]?.text;if("string"==typeof o&&o.length>a){const a=await async function(t,e,n,o){try{const a=await fetch(`${t}/api/results/spill`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:n,toolName:e,...o?{callId:o}:{}}),signal:AbortSignal.timeout(3e4)});if(!a.ok)return;const s=await a.json();return s?.data?.address??void 0}catch{return}}(d,String(n),o,t?.callID),r=s(String(n),o,a);if(r)return e.content[0].text=r,void await l(m,{toolName:n,sessionId:t?.sessionID||"",input:t?.args||t?.input||{},output:r,timing:"after",...t?.callID?{callId:t.callID}:{}})}var r;o?await l(m,{toolName:n,sessionId:t?.sessionID||"",input:t?.args||t?.input||{},output:(r=o,r.length<=8192?r:r.slice(0,6144)+"\n...[truncated]...\n"+r.slice(-2048)),timing:"after",callId:t?.callID||""}):t?.callID&&await f(d,n)&&await l(m,{toolName:n,sessionId:t?.sessionID||"",input:t?.args||t?.input||{},timing:"after",callId:t.callID})},event:async({event:t})=>{switch(t.type){case"session.idle":{const a=t.properties.sessionID||"",{output:s,lastMessageId:r}=await async function(t,a){try{const s=e(a,".alchemy_lab","opencode.json");if(!o(s))return{output:""};const r=JSON.parse(n(s,"utf8")),i=r.port?`http://127.0.0.1:${r.port}`:null;if(!i)return{output:""};const c=process.env.OPENCODE_SERVER_PASSWORD,l=c?{Authorization:`Basic ${Buffer.from(`${process.env.OPENCODE_SERVER_USERNAME||"opencode"}:${c}`).toString("base64")}`}:{},u=await fetch(`${i}/session/${t}/message?limit=5`,{headers:l});if(!u.ok)return{output:""};const h=(await u.json()).filter((t=>"assistant"===t.info.role));return{output:h.flatMap((t=>t.parts)).filter((t=>"text"===t.type&&t.text)).map((t=>t.text)).join("\n"),lastMessageId:h[0]?.info?.id}}catch{return{output:""}}}(a,h);return void c(g,{eventType:"session_idle",sessionId:a,variables:{sessionId:a,lastOutput:s,lastMessageId:r||""}})}case"session.created":{const e=t.properties.info?.id||"";if(!e)return;return void setTimeout((()=>{c(g,{eventType:"session_created",sessionId:e,variables:{sessionId:e}})}),500)}}}};return i=w,w};export const internals={ceilingNotice:s,MCP_RESULT_CEILING_CHARS:a};
|
|
@@ -1,66 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* OpenCode Inject Session Plugin
|
|
3
|
-
*
|
|
4
|
-
* Injects the current session ID into spawn-background-work tool calls
|
|
5
|
-
* so that spawned background sessions can be linked as children of the parent.
|
|
6
|
-
*
|
|
7
|
-
* Hooks into tool.execute.before to modify args before the MCP tool executes.
|
|
8
|
-
*/
|
|
9
|
-
import { appendFileSync } from 'fs';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
// Log immediately when module is loaded
|
|
13
|
-
try {
|
|
14
|
-
const logFile = join(homedir(), 'inject-session-plugin.log');
|
|
15
|
-
appendFileSync(logFile, `[${new Date().toISOString()}] MODULE LOADED - file was imported\n`);
|
|
16
|
-
}
|
|
17
|
-
catch (e) {
|
|
18
|
-
// ignore
|
|
19
|
-
}
|
|
20
|
-
const debugLog = (msg, data) => {
|
|
21
|
-
const logFile = join(homedir(), 'inject-session-plugin.log');
|
|
22
|
-
const line = `[${new Date().toISOString()}] ${msg}${data ? ' ' + JSON.stringify(data) : ''}\n`;
|
|
23
|
-
try {
|
|
24
|
-
appendFileSync(logFile, line);
|
|
25
|
-
}
|
|
26
|
-
catch (e) {
|
|
27
|
-
// ignore
|
|
28
|
-
}
|
|
29
|
-
console.log('[inject-session-plugin]', msg, data || '');
|
|
30
|
-
};
|
|
31
|
-
export default async ({ project, directory }) => {
|
|
32
|
-
debugLog('Plugin loaded', { project: project?.id, directory });
|
|
33
|
-
return {
|
|
34
|
-
"tool.execute.before": async (context, data) => {
|
|
35
|
-
debugLog('tool.execute.before called', {
|
|
36
|
-
tool: context.tool,
|
|
37
|
-
sessionID: context.sessionID,
|
|
38
|
-
hasArgs: !!data?.args
|
|
39
|
-
});
|
|
40
|
-
try {
|
|
41
|
-
// Check if this is a spawn-background-work tool
|
|
42
|
-
// MCP tool names can use dashes or underscores depending on how they're registered
|
|
43
|
-
const toolName = context.tool;
|
|
44
|
-
if (!toolName || (!toolName.includes('spawn_background_work') && !toolName.includes('spawn-background-work'))) {
|
|
45
|
-
return; // Not logging every non-matching tool to avoid spam
|
|
46
|
-
}
|
|
47
|
-
debugLog('Matched spawn-background-work tool!');
|
|
48
|
-
// Inject parentSessionId into args if not already set
|
|
49
|
-
if (!data.args.parentSessionId && context.sessionID) {
|
|
50
|
-
data.args.parentSessionId = context.sessionID;
|
|
51
|
-
debugLog('Injected parentSessionId', context.sessionID);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
debugLog('parentSessionId already set or no sessionID', {
|
|
55
|
-
existing: data.args.parentSessionId,
|
|
56
|
-
sessionID: context.sessionID
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
debugLog('Error', { error: String(error) });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
//# sourceMappingURL=inject-session.js.map
|
|
1
|
+
import{appendFileSync as s}from"fs";import{homedir as o}from"os";import{join as e}from"path";const n="true"===process.env.ALCHEMY_DEBUG_LOGS;if(n)try{const n=e(o(),"inject-session-plugin.log");s(n,`[${(new Date).toISOString()}] MODULE LOADED - file was imported\n`)}catch(s){}const t=(t,r)=>{if(n){const n=e(o(),"inject-session-plugin.log"),i=`[${(new Date).toISOString()}] ${t}${r?" "+JSON.stringify(r):""}\n`;try{s(n,i)}catch(s){}console.log("[inject-session-plugin]",t,r||"")}};export default async({project:s,directory:o})=>(t("Plugin loaded",{project:s?.id,directory:o}),{"tool.execute.before":async(s,o)=>{t("tool.execute.before called",{tool:s.tool,sessionID:s.sessionID,hasArgs:!!o?.args});try{const e=s.tool;if(!e||!e.includes("spawn_background_work")&&!e.includes("spawn-background-work"))return;t("Matched spawn-background-work tool!"),!o.args.parentSessionId&&s.sessionID?(o.args.parentSessionId=s.sessionID,t("Injected parentSessionId",s.sessionID)):t("parentSessionId already set or no sessionID",{existing:o.args.parentSessionId,sessionID:s.sessionID})}catch(s){t("Error",{error:String(s)})}}});
|
|
@@ -1,93 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* OpenCode System Logger Plugin
|
|
3
|
-
*
|
|
4
|
-
* Hooks into experimental.chat.system.transform to log the full system prompt
|
|
5
|
-
* to .alchemy_lab/system-prompt.log. Used to diagnose whether entity context
|
|
6
|
-
* (from @ mentions) reaches the LLM via the user.system field.
|
|
7
|
-
*/
|
|
8
|
-
import { appendFileSync, mkdirSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
const getLogFile = (directory) => join(directory, '.alchemy_lab', 'system-prompt.log');
|
|
11
|
-
const debugLog = (logFile, msg, data) => {
|
|
12
|
-
const line = `[${new Date().toISOString()}] ${msg}${data ? ' ' + JSON.stringify(data) : ''}\n`;
|
|
13
|
-
try {
|
|
14
|
-
mkdirSync(join(logFile, '..'), { recursive: true });
|
|
15
|
-
appendFileSync(logFile, line);
|
|
16
|
-
}
|
|
17
|
-
catch (e) {
|
|
18
|
-
// ignore
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Strip Task tool references from system prompt for alchemist sessions.
|
|
23
|
-
* The OpenCode base prompt heavily promotes using the Task tool, which causes
|
|
24
|
-
* the alchemist to hallucinate the tool even when it's disabled in tool config.
|
|
25
|
-
*/
|
|
26
|
-
function stripTaskReferences(system) {
|
|
27
|
-
for (let i = 0; i < system.length; i++) {
|
|
28
|
-
let s = system[i];
|
|
29
|
-
// Remove lines that promote the Task tool
|
|
30
|
-
s = s.replace(/^- When doing file search, prefer to use the Task tool.*$/gm, '');
|
|
31
|
-
s = s.replace(/^- You should proactively use the Task tool.*$/gm, '');
|
|
32
|
-
s = s.replace(/^- VERY IMPORTANT:.*use the Task tool instead of running search commands directly\.$/gm, '');
|
|
33
|
-
s = s.replace(/^- If the user specifies that they want you to run tools "in parallel".*multiple Task tool calls\.$/gm, '');
|
|
34
|
-
// Remove Task tool examples
|
|
35
|
-
s = s.replace(/<example>\n.*?\[Uses the Task tool.*?<\/example>/gs, '');
|
|
36
|
-
// Clean up excessive blank lines left behind
|
|
37
|
-
s = s.replace(/\n{3,}/g, '\n\n');
|
|
38
|
-
system[i] = s;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export default async ({ directory }) => {
|
|
42
|
-
const logFile = getLogFile(directory);
|
|
43
|
-
debugLog(logFile, 'system-logger plugin loaded', { directory });
|
|
44
|
-
return {
|
|
45
|
-
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
46
|
-
// Strip Task tool references for alchemist sessions
|
|
47
|
-
const isAlchemist = output.system.some((s) => s.includes('head of engineering'));
|
|
48
|
-
debugLog(logFile, 'Alchemist detection', { isAlchemist, sessionID: hookInput.sessionID, systemParts: output.system.length, partLengths: output.system.map((s) => s.length), hasSpecialist: output.system.map((s) => s.includes('specialist_ops')) });
|
|
49
|
-
if (isAlchemist) {
|
|
50
|
-
stripTaskReferences(output.system);
|
|
51
|
-
debugLog(logFile, 'Stripped Task tool references for alchemist session', { sessionID: hookInput.sessionID });
|
|
52
|
-
}
|
|
53
|
-
const hasEntityContext = output.system.some((s) => s.includes('referenced the following') || s.includes('entity'));
|
|
54
|
-
const lines = [
|
|
55
|
-
'='.repeat(80),
|
|
56
|
-
`Timestamp: ${new Date().toISOString()}`,
|
|
57
|
-
`Session: ${hookInput.sessionID || 'N/A'}`,
|
|
58
|
-
`Model: ${hookInput.model?.providerID || '?'}/${hookInput.model?.modelID || '?'}`,
|
|
59
|
-
`System parts: ${output.system.length}`,
|
|
60
|
-
`Has entity context: ${hasEntityContext}`,
|
|
61
|
-
'-'.repeat(80),
|
|
62
|
-
...output.system.map((s, i) => {
|
|
63
|
-
const preview = s.length > 500
|
|
64
|
-
? s.slice(0, 250) + '\n...[truncated]...\n' + s.slice(-250)
|
|
65
|
-
: s;
|
|
66
|
-
return `--- Part ${i} (${s.length} chars) ---\n${preview}`;
|
|
67
|
-
}),
|
|
68
|
-
'='.repeat(80),
|
|
69
|
-
'',
|
|
70
|
-
];
|
|
71
|
-
try {
|
|
72
|
-
mkdirSync(join(logFile, '..'), { recursive: true });
|
|
73
|
-
appendFileSync(logFile, lines.join('\n') + '\n');
|
|
74
|
-
// Also write full untruncated prompt to separate file (overwrite each time)
|
|
75
|
-
const fullLogFile = join(logFile, '..', 'system-prompt-full.log');
|
|
76
|
-
const fullLines = [
|
|
77
|
-
`Timestamp: ${new Date().toISOString()}`,
|
|
78
|
-
`Session: ${hookInput.sessionID || 'N/A'}`,
|
|
79
|
-
`IsAlchemist: ${isAlchemist}`,
|
|
80
|
-
`System parts: ${output.system.length}`,
|
|
81
|
-
...output.system.map((s, i) => `--- Part ${i} (${s.length} chars) ---\n${s}`),
|
|
82
|
-
'='.repeat(80),
|
|
83
|
-
];
|
|
84
|
-
const { writeFileSync } = require('fs');
|
|
85
|
-
writeFileSync(fullLogFile, fullLines.join('\n') + '\n');
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
// ignore
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
};
|
|
93
|
-
//# sourceMappingURL=system-logger.js.map
|
|
1
|
+
import{appendFileSync as e,mkdirSync as s}from"fs";import{join as t}from"path";const n="true"===process.env.ALCHEMY_DEBUG_LOGS,r=(r,o,l)=>{if(!n)return;const i=`[${(new Date).toISOString()}] ${o}${l?" "+JSON.stringify(l):""}\n`;try{s(t(r,".."),{recursive:!0}),e(r,i)}catch(e){}};export default async({directory:o})=>{const l=(e=>t(e,".alchemy_lab","system-prompt.log"))(o);return r(l,"system-logger plugin loaded",{directory:o}),{"experimental.chat.system.transform":async(o,i)=>{const a=i.system.some((e=>e.includes("head of engineering")));if(n&&r(l,"Alchemist detection",{isAlchemist:a,sessionID:o.sessionID,systemParts:i.system.length,partLengths:i.system.map((e=>e.length)),hasSpecialist:i.system.map((e=>e.includes("specialist_ops")))}),a&&(function(e){for(let s=0;s<e.length;s++){let t=e[s];t=t.replace(/^- When doing file search, prefer to use the Task tool.*$/gm,""),t=t.replace(/^- You should proactively use the Task tool.*$/gm,""),t=t.replace(/^- VERY IMPORTANT:.*use the Task tool instead of running search commands directly\.$/gm,""),t=t.replace(/^- If the user specifies that they want you to run tools "in parallel".*multiple Task tool calls\.$/gm,""),t=t.replace(/<example>\n.*?\[Uses the Task tool.*?<\/example>/gs,""),t=t.replace(/\n{3,}/g,"\n\n"),e[s]=t}}(i.system),r(l,"Stripped Task tool references for alchemist session",{sessionID:o.sessionID})),!n)return;const c=i.system.some((e=>e.includes("referenced the following")||e.includes("entity"))),m=["=".repeat(80),`Timestamp: ${(new Date).toISOString()}`,`Session: ${o.sessionID||"N/A"}`,`Model: ${o.model?.providerID||"?"}/${o.model?.modelID||"?"}`,`System parts: ${i.system.length}`,`Has entity context: ${c}`,"-".repeat(80),...i.system.map(((e,s)=>{const t=e.length>500?e.slice(0,250)+"\n...[truncated]...\n"+e.slice(-250):e;return`--- Part ${s} (${e.length} chars) ---\n${t}`})),"=".repeat(80),""];try{s(t(l,".."),{recursive:!0}),e(l,m.join("\n")+"\n");const n=t(l,"..","system-prompt-full.log"),r=[`Timestamp: ${(new Date).toISOString()}`,`Session: ${o.sessionID||"N/A"}`,`IsAlchemist: ${a}`,`System parts: ${i.system.length}`,...i.system.map(((e,s)=>`--- Part ${s} (${e.length} chars) ---\n${e}`)),"=".repeat(80)],{writeFileSync:c}=require("fs");c(n,r.join("\n")+"\n")}catch(e){}}}};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemylab",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "AlchemyLab
|
|
5
|
+
"description": "AlchemyLab: the agentic workbench. Work is a request, with criteria, gates and a receipt.",
|
|
6
6
|
"author": "Testfire Software",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
8
8
|
"homepage": "https://alchemylabcode.com",
|
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
"harness"
|
|
17
17
|
],
|
|
18
18
|
"main": "dist/index.js",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=24"
|
|
21
|
+
},
|
|
19
22
|
"bin": {
|
|
20
23
|
"alchemy": "dist/cli.js",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
24
|
+
"alchemylab": "dist/cli.js",
|
|
25
|
+
"alchemy_mcp": "dist/mcp-direct.js"
|
|
23
26
|
},
|
|
24
27
|
"files": [
|
|
25
28
|
"dist/**/*.js",
|
|
@@ -29,30 +32,38 @@
|
|
|
29
32
|
],
|
|
30
33
|
"scripts": {
|
|
31
34
|
"build": "npm run build:dev",
|
|
32
|
-
"build:dev": "tsc -p tsconfig.json && chmod +x dist-dev/*.js",
|
|
35
|
+
"build:dev": "tsc -p tsconfig.json && chmod +x dist-dev/*.js && node scripts/write-build-info.mjs",
|
|
33
36
|
"dev:watch": "tsc -p tsconfig.json --watch",
|
|
34
|
-
"build:prod": "npm run build:dev && webpack --mode=production &&
|
|
35
|
-
"copy:plugins": "
|
|
37
|
+
"build:prod": "npm run build:dev && webpack --mode=production && npm run copy:plugins && npm run minify:plugins && chmod +x dist/*.js",
|
|
38
|
+
"copy:plugins": "rm -rf dist/plugins && mkdir -p dist/plugins/hooks && cp dist-dev/plugins/hooks/plugin.js dist/plugins/hooks/ && cp dist-dev/plugins/inject-session.js dist/plugins/ && cp dist-dev/plugins/system-logger.js dist/plugins/",
|
|
36
39
|
"build:analyze": "ANALYZE=true webpack --mode=production",
|
|
37
40
|
"test": "npm run build:dev && mocha --require ts-node/register test/**/*.test.ts",
|
|
38
41
|
"test:watch": "mocha --require ts-node/register test/**/*.test.ts --watch --watch-files src/**/*.ts,test/**/*.ts",
|
|
39
|
-
"
|
|
42
|
+
"check:stranded": "node scripts/check-stranded-requests.js",
|
|
43
|
+
"minify:plugins": "node scripts/minify-plugins.mjs",
|
|
44
|
+
"prepack": "npm run build:prod"
|
|
40
45
|
},
|
|
41
46
|
"dependencies": {
|
|
42
|
-
"chalk": "^4.1.2",
|
|
43
47
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
48
|
+
"alchemylab-opencode": "^1.0.20",
|
|
49
|
+
"chalk": "^4.1.2",
|
|
50
|
+
"compression": "^1.8.1",
|
|
44
51
|
"cors": "^2.8.5",
|
|
45
52
|
"express": "^4.18.2",
|
|
53
|
+
"jszip": "^3.10.2",
|
|
46
54
|
"micromatch": "^4.0.5",
|
|
47
|
-
"
|
|
55
|
+
"pdf-lib": "^1.17.1",
|
|
56
|
+
"playwright-core": "^1.61.1",
|
|
57
|
+
"simple-git": "^3.36.0",
|
|
58
|
+
"unpdf": "^1.8.1",
|
|
48
59
|
"winston": "^3.8.2",
|
|
49
60
|
"ws": "^8.14.2",
|
|
50
61
|
"zod": "^3.22.4",
|
|
51
|
-
"zod-to-json-schema": "^3.22.3"
|
|
52
|
-
"alchemylab-opencode": "^1.0.17"
|
|
62
|
+
"zod-to-json-schema": "^3.22.3"
|
|
53
63
|
},
|
|
54
64
|
"devDependencies": {
|
|
55
65
|
"@types/chai": "^4.3.5",
|
|
66
|
+
"@types/compression": "^1.8.1",
|
|
56
67
|
"@types/cors": "^2.8.13",
|
|
57
68
|
"@types/express": "^4.17.17",
|
|
58
69
|
"@types/micromatch": "^4.0.6",
|
|
@@ -63,14 +74,14 @@
|
|
|
63
74
|
"chai": "^4.3.7",
|
|
64
75
|
"mocha": "^10.2.0",
|
|
65
76
|
"sinon": "^15.2.0",
|
|
77
|
+
"terser-webpack-plugin": "^5.3.9",
|
|
78
|
+
"ts-loader": "^9.4.4",
|
|
66
79
|
"ts-node": "^10.9.1",
|
|
67
80
|
"ts-node-dev": "^2.0.0",
|
|
68
81
|
"typescript": "^5.0.4",
|
|
69
82
|
"webpack": "^5.88.0",
|
|
70
|
-
"webpack-cli": "^5.1.4",
|
|
71
83
|
"webpack-bundle-analyzer": "^4.9.0",
|
|
72
|
-
"
|
|
73
|
-
"ts-loader": "^9.4.4",
|
|
84
|
+
"webpack-cli": "^5.1.4",
|
|
74
85
|
"webpack-node-externals": "^3.0.0"
|
|
75
86
|
},
|
|
76
87
|
"overrides": {
|
package/dist/414.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict"
|
|
2
|
-
exports.id=414,exports.ids=[414],exports.modules={9414(t,exports,e){exports.getActionServerPort=function(t){const e=Date.now(),l=!t
|
|
3
|
-
if(l&&null!==i&&u>e-p)return i
|
|
4
|
-
const a=(0,o.join)(t||process.env.ALCHEMY_PROJECT_PATH||process.cwd(),s,c)
|
|
5
|
-
if((0,r.existsSync)(a))try{const t=(0,r.readFileSync)(a,"utf-8"),o=JSON.parse(t)
|
|
6
|
-
if("number"==typeof o.port&&o.port>0&&65536>o.port)return l&&(i=o.port,p=e),o.port}catch{}return l&&(i=n,p=e),n}
|
|
7
|
-
const r=e(9896),o=e(6928),n=3456,s=".alchemy_lab",c="action-server.json"
|
|
8
|
-
let i=null,p=0
|
|
9
|
-
const u=5e3}}
|
package/dist/478.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict"
|
|
2
|
-
exports.id=478,exports.ids=[478],exports.modules={2096(e,exports,t){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
3
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.ProjectConfigService=void 0
|
|
4
|
-
const o=r(t(1943)),i=r(t(6928)),s=r(t(857)),a=t(4139),n=t(8824)
|
|
5
|
-
exports.ProjectConfigService=class{constructor(e){this.logger=e.child({component:"project-config"})}async readProjectId(e){try{const t=i.default.join(e,a.ALCHEMY_DIR_NAME,"project.json"),r=await o.default.readFile(t,"utf-8"),s=JSON.parse(r)
|
|
6
|
-
return s.id?s.id+"":null}catch(e){return this.logger.debug("Could not read project ID",{error:(0,n.getErrorMessage)(e)}),null}}async readProjectConfig(e){try{const t=i.default.join(e,a.ALCHEMY_DIR_NAME,"config.json"),r=await o.default.readFile(t,"utf-8")
|
|
7
|
-
return JSON.parse(r)}catch(e){return this.logger.debug("Could not read project config",{error:(0,n.getErrorMessage)(e)}),{}}}async saveProjectConfig(e,t){try{const r=i.default.join(e,a.ALCHEMY_DIR_NAME),s=i.default.join(r,"config.json")
|
|
8
|
-
await o.default.mkdir(r,{recursive:!0})
|
|
9
|
-
let n={}
|
|
10
|
-
try{const e=await o.default.readFile(s,"utf-8")
|
|
11
|
-
n=JSON.parse(e)}catch(e){this.logger.debug("No existing config.json, creating new one")}void 0!==t&&(n.autoJournal=t),n.lockTimeout||(n.lockTimeout=3600),n.worktreeCleanupDelay||(n.worktreeCleanupDelay=900),n.maxWorktreeAge||(n.maxWorktreeAge=24),n.cleanupInterval||(n.cleanupInterval=15),void 0===n.preserveBranchesOnCleanup&&(n.preserveBranchesOnCleanup=!0),n.maxConcurrentSessions||(n.maxConcurrentSessions=10),n.sessionTimeoutWarning||(n.sessionTimeoutWarning=45),await o.default.writeFile(s,JSON.stringify(n,null,2),"utf-8"),this.logger.info("Saved project config",{configPath:s})}catch(e){this.logger.error("Failed to save project config",{error:(0,n.getErrorMessage)(e)})}}getToolPreferencesPath(){const e=s.default.homedir()
|
|
12
|
-
return i.default.join(e,".alchemy_lab","tool-preferences.json")}async loadToolPreferences(e){const t=this.getToolPreferencesPath()
|
|
13
|
-
try{const r=await o.default.readFile(t,"utf-8"),i=JSON.parse(r)
|
|
14
|
-
return e&&i[e]?i[e]:e?null:i}catch(t){return this.logger.debug("No user preferences found",{error:(0,n.getErrorMessage)(t)}),e?null:{}}}async saveToolPreferences(e,t,r,s){const a=this.getToolPreferencesPath(),n=i.default.dirname(a)
|
|
15
|
-
this.logger.info("Saving user tool preferences",{clientType:e,preset:t,disabledToolsCount:r?.length||0,allowedToolsCount:s?.length||0}),await o.default.mkdir(n,{recursive:!0})
|
|
16
|
-
let c={}
|
|
17
|
-
try{const e=await o.default.readFile(a,"utf-8")
|
|
18
|
-
c=JSON.parse(e)}catch(e){}c[e]={preset:t,disabledTools:r||[],allowedAlchemyTools:s||[],updatedAt:(new Date).toISOString()},await o.default.writeFile(a,JSON.stringify(c,null,2),"utf-8"),this.logger.info("User tool preferences saved",{clientType:e,prefsFile:a})}}},2568(e,exports,t){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
19
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.ProjectSynchronizer=void 0
|
|
20
|
-
const o=r(t(6928)),i=t(8029),s=t(8824),a=t(2096)
|
|
21
|
-
exports.ProjectSynchronizer=class{constructor(e,t,r,o,i={}){this.logger=e.child({component:"project-synchronizer"}),this.registry=t,this.remoteApi=r,this.fileSystem=o,this.options={createRemoteProjects:i.createRemoteProjects??!0,maxRetryAttempts:i.maxRetryAttempts??3,retryDelayMs:i.retryDelayMs??1e3},this.logger.info("ProjectSynchronizer initialized",{options:this.options})}async ensureProjectExists(e){const t=Math.random().toString(36).substring(2,8)
|
|
22
|
-
this.logger.info("\u{1f4de} SYNC-TRACE: ensureProjectExists called",{callId:t,projectId:e,stackTrace:Error().stack?.split("\n").slice(1,4).map((e=>e.trim())).join(" -> ")||"unknown"})
|
|
23
|
-
try{if(!this.remoteApi.isOnline())return this.logger.info("\u{1f4de} SYNC-TRACE: System offline, returning original ID",{callId:t,projectId:e}),e
|
|
24
|
-
if(this.logger.info("\u{1f4de} SYNC-TRACE: System online, proceeding",{callId:t,projectId:e}),this.registry.isVerified(e))return this.logger.info("\u{1f4de} SYNC-TRACE: Cache hit - already verified",{callId:t,projectId:e}),e
|
|
25
|
-
this.logger.info("\u{1f4de} SYNC-TRACE: Cache miss - checking remote server",{callId:t,projectId:e})
|
|
26
|
-
const r=await this.remoteApi.fetchProject(e)
|
|
27
|
-
if(this.logger.info("\u{1f4de} SYNC-TRACE: Remote fetch completed",{callId:t,projectId:e,found:!!r,remoteProject:r?{id:r.id,name:r.name}:null}),r)return this.logger.info("Project exists on remote server",{projectId:e}),this.registry.register(r),r.id
|
|
28
|
-
if(this.logger.info("\u{1f4de} SYNC-TRACE: Project not found, will create",{callId:t,projectId:e}),!this.options.createRemoteProjects)throw this.logger.info("Project does not exist on remote and creation is disabled",{projectId:e}),Error(`Project ${e} does not exist on remote server and creation is disabled`)
|
|
29
|
-
{this.logger.info("\u{1f4de} SYNC-TRACE: Creating project on remote server",{callId:t,projectId:e})
|
|
30
|
-
let r={id:e,name:"Remote Synchronized Project",description:"Automatically created during synchronization",status:"active"}
|
|
31
|
-
const o=this.registry.getProject(e)
|
|
32
|
-
o&&(this.logger.info("\u{1f4de} SYNC-TRACE: Using local project data",{callId:t,projectId:e,projectName:o.name}),r={...o})
|
|
33
|
-
try{this.logger.info("\u{1f4de} SYNC-TRACE: Calling remoteApi.createProject",{callId:t,projectId:e,projectName:r.name})
|
|
34
|
-
const o=await this.remoteApi.createProject(r)
|
|
35
|
-
return this.logger.info("Create project API call succeeded",{returnedId:o?.id,returnedName:o?.name}),this.registry.register(o),this.logger.info("Successfully created project on remote server",{projectId:o.id,projectName:o.name}),o.id}catch(t){if(t&&"object"==typeof t&&"status"in t&&409===t.status)return this.logger.info("Project already exists on remote (409 conflict)",{projectId:e}),this.registry.markVerified(e),e
|
|
36
|
-
throw this.logger.error("Failed to create project on remote server",{projectId:e,error:t instanceof Error?t.stack:t+"",status:t?.status,message:t instanceof Error?t.message:t+""}),t}}}catch(t){throw this.logger.error("Error verifying project existence",{projectId:e,error:t instanceof Error?t.stack:t+"",message:(0,s.getErrorMessage)(t)}),t}}async createProject(e,t){try{if(e.id){if(!i.ProjectIDFactory.isValid(e.id)){this.logger.warn("Invalid project ID format, generating new ID",{originalId:e.id})
|
|
37
|
-
const t=i.ProjectIDFactory.create().toString()
|
|
38
|
-
e.id=t}}else{const t=i.ProjectIDFactory.create().toString()
|
|
39
|
-
e.id=t,this.logger.info("Generated new project ID",{projectId:t})}const r=(new Date).toISOString(),o={id:e.id,name:e.name||"Unnamed Project",description:e.description||"No description provided",status:e.status||"active",createdAt:e.createdAt||r,updatedAt:e.updatedAt||r,metadata:e.metadata||{}}
|
|
40
|
-
if(this.registry.register(o),t&&await this.fileSystem.writeProjectFile(o,t),this.remoteApi.isOnline())try{this.logger.info("Syncing new project to remote server",{projectId:o.id}),await this.ensureProjectExists(o.id)}catch(e){this.logger.warn("Failed to sync new project to remote, will retry later",{projectId:o.id,error:e instanceof Error?e.message:e+""})}return o}catch(t){throw this.logger.error("Failed to create project",{projectData:e,error:t instanceof Error?t.stack:t+""}),t}}async syncAllProjects(e){try{if(!this.remoteApi.isOnline())return void this.logger.info("Skipping project synchronization - system is offline")
|
|
41
|
-
this.logger.info("Starting project synchronization from directory",{basePath:e})
|
|
42
|
-
const t=await this.fileSystem.readProjectFile(e)
|
|
43
|
-
if(!t||!t.id)return void this.logger.info("No valid local project found, skipping synchronization",{basePath:e})
|
|
44
|
-
if(this.logger.info("Found local project",{projectId:t.id,projectName:t.name}),this.logger.info("Registering project in registry before synchronization",{projectId:t.id,projectName:t.name}),this.registry.register(t),this.logger.info("Ensuring project exists on remote server",{projectId:t.id}),await this.ensureProjectExists(t.id)){this.logger.info("Project exists on remote, checking metadata sync",{projectId:t.id,projectName:t.name,basePath:e}),t.metadata&&Object.keys(t.metadata).length>0&&(this.logger.info("Syncing project metadata to remote (async)",{projectId:t.id,metadataKeys:Object.keys(t.metadata)}),this.remoteApi.updateProject(t.id,{metadata:t.metadata}).then((()=>{this.logger.info("Project metadata synced successfully",{projectId:t.id})})).catch((e=>{this.logger.warn("Failed to sync project metadata",{projectId:t.id,error:e instanceof Error?e.message:e+""})})))
|
|
45
|
-
try{const r=await this.remoteApi.fetchProject(t.id)
|
|
46
|
-
if(r?.metadata){const{autoJournal:o}=r.metadata
|
|
47
|
-
if(void 0!==o){const r=new a.ProjectConfigService(this.logger)
|
|
48
|
-
await r.saveProjectConfig(e,o),this.logger.info("Synced remote settings to local config",{projectId:t.id,autoJournal:o})}}}catch(e){this.logger.warn("Failed to sync remote settings to local config",{projectId:t.id,error:e instanceof Error?e.message:e+""})}this.logger.info("Successfully synchronized project",{projectId:t.id,projectName:t.name,basePath:e})}else this.logger.warn("Failed to verify/create project on remote server",{projectId:t.id,basePath:e})}catch(t){this.logger.error("Error during project synchronization",{basePath:e,error:t instanceof Error?t.stack:t+""})}}async loadLocalProject(e){try{this.logger.info("Loading local project from directory",{basePath:e})
|
|
49
|
-
const t=await this.fileSystem.readProjectFile(e)
|
|
50
|
-
return t&&t.id?(this.registry.register(t),this.logger.info("Successfully loaded local project",{projectId:t.id,basePath:e}),t):(this.logger.info("No valid local project found",{basePath:e}),null)}catch(t){return this.logger.error("Error loading local project",{basePath:e,error:t instanceof Error?t.stack:t+""}),null}}async getOrCreateProject(e,t={}){this.logger.info("\u{1f680} getOrCreateProject called",{basePath:e,hasProjectData:!!Object.keys(t).length,projectData:Object.keys(t)})
|
|
51
|
-
try{this.logger.info("Attempting to load existing local project",{basePath:e})
|
|
52
|
-
const r=await this.loadLocalProject(e)
|
|
53
|
-
if(r){this.logger.info("Found existing local project",{projectId:r.id,projectName:r.name,basePath:e})
|
|
54
|
-
const t=this.remoteApi.isOnline()
|
|
55
|
-
if(this.logger.info("Checking online status for verification",{isOnline:t,projectId:r.id}),!t)return this.logger.info("Using existing project (offline, skipping server verification)",{projectId:r.id,basePath:e}),r
|
|
56
|
-
{this.logger.info("System is online - starting project verification",{projectId:r.id,basePath:e})
|
|
57
|
-
const t=await this.ensureProjectExists(r.id)
|
|
58
|
-
if(this.logger.info("Project verification completed",{projectId:r.id,exists:t,basePath:e}),t)return this.logger.info("Using existing project (verified on server)",{projectId:r.id,basePath:e}),r
|
|
59
|
-
this.logger.warn("Local project not found on server, creating new project",{invalidProjectId:r.id,basePath:e})}}this.logger.info("No existing project found, creating new one",{basePath:e}),t.name||(t.name=o.default.basename(e))
|
|
60
|
-
const i=await this.createProject(t,e)
|
|
61
|
-
return this.logger.info("Created new project for directory",{projectId:i.id,basePath:e}),i}catch(t){this.logger.error("Error getting or creating project",{basePath:e,error:t instanceof Error?t.stack:t+""})
|
|
62
|
-
const r=await this.createProject({name:"Fallback Project for "+o.default.basename(e),description:"Created as fallback due to error"},e)
|
|
63
|
-
return this.logger.info("Created fallback project due to error",{projectId:r.id,basePath:e}),r}}async verifyAndFixProjectName(e){try{if(!this.remoteApi.isOnline())return this.logger.info("Skipping project name verification - system is offline"),!0
|
|
64
|
-
if(this.registry.isVerifiedWithName&&this.registry.isVerifiedWithName(e.id,e.name))return this.logger.info("Project name already verified, skipping verification",{projectId:e.id,projectName:e.name}),!0
|
|
65
|
-
const t=await this.remoteApi.fetchProject(e.id)
|
|
66
|
-
if(!t)return this.logger.warn("Cannot verify project name - project not found on remote",{projectId:e.id}),!1
|
|
67
|
-
if(t.name===e.name)return this.logger.info("Project name matches remote, marking as verified",{projectId:e.id,projectName:e.name}),this.registry.markVerifiedWithName&&this.registry.markVerifiedWithName(e.id,e.name),!0
|
|
68
|
-
this.logger.info("Project name mismatch, updating remote name",{projectId:e.id,localName:e.name,remoteName:t.name})
|
|
69
|
-
const r=this.options.maxRetryAttempts,o=this.options.retryDelayMs
|
|
70
|
-
for(let t=1;r>=t;t++)try{return await this.remoteApi.updateProject(e.id,{name:e.name}),this.logger.info("Project name updated successfully",{projectId:e.id,newName:e.name}),this.registry.markVerifiedWithName&&this.registry.markVerifiedWithName(e.id,e.name),!0}catch(i){if(t>=r)throw i
|
|
71
|
-
this.logger.warn(`Failed to update project name, retrying (${t}/${r})`,{projectId:e.id,error:i instanceof Error?i.message:i+""}),await new Promise((e=>setTimeout(e,o)))}return!1}catch(t){return this.logger.error("Failed to verify/fix project name",{projectId:e.id,projectName:e.name,error:t instanceof Error?t.stack:t+""}),!1}}async syncProject(e,t){try{if(!this.remoteApi.isOnline())return this.logger.info("Skipping project synchronization - system is offline",{projectId:e}),!1
|
|
72
|
-
let r=null
|
|
73
|
-
if(t&&(r=await this.loadLocalProject(t),r&&r.id!==e&&(this.logger.warn("Project ID mismatch between request and loaded project",{requestedId:e,loadedId:r.id}),r=null)),!r){const t=this.registry.getProject(e)
|
|
74
|
-
t&&(r=t)}if(!r)return this.logger.info("Project not found locally, attempting to fetch from remote",{projectId:e}),r=await this.remoteApi.fetchProject(e),r?(this.registry.register(r),t&&await this.fileSystem.writeProjectFile(r,t),!0):(this.logger.warn("Project not found locally or remotely",{projectId:e}),!1)
|
|
75
|
-
if(!await this.ensureProjectExists(e))return!1
|
|
76
|
-
if(await this.verifyAndFixProjectName(r)||this.logger.warn("Failed to verify/fix project name",{projectId:r.id,projectName:r.name}),r.metadata&&Object.keys(r.metadata).length>0)try{this.logger.info("Syncing project metadata to remote",{projectId:r.id,metadataKeys:Object.keys(r.metadata)}),await this.remoteApi.updateProject(r.id,{metadata:r.metadata}),this.logger.info("Project metadata synced successfully",{projectId:r.id})}catch(e){this.logger.warn("Failed to sync project metadata",{projectId:r.id,error:e instanceof Error?e.message:e+""})}return this.logger.info("Project synchronization completed successfully",{projectId:e}),!0}catch(r){return this.logger.error("Error synchronizing project",{projectId:e,basePath:t,error:r instanceof Error?r.stack:r+""}),!1}}}},3102(e,exports,t){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
77
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.DefaultProjectFileSystem=void 0
|
|
78
|
-
const o=r(t(9896)),i=r(t(6928)),s=t(4139)
|
|
79
|
-
exports.DefaultProjectFileSystem=class{constructor(e){this.logger=e.child({component:"project-file-system"})}async readProjectFile(e){try{const t=i.default.join(e,"project.json"),r=i.default.join(e,s.ALCHEMY_DIR_NAME,"project.json")
|
|
80
|
-
let a=r
|
|
81
|
-
if(o.default.existsSync(t))a=t,this.logger.info("Found project file in base directory",{path:a})
|
|
82
|
-
else{if(!o.default.existsSync(r))return this.logger.info("Project file does not exist in either location",{directPath:t,nestedPath:r}),null
|
|
83
|
-
this.logger.info("Found project file in alchemy_lab subdirectory",{path:a})}const n=await o.default.promises.readFile(a,"utf8"),c=JSON.parse(n)
|
|
84
|
-
return c.id?(this.logger.info("Read project file successfully",{projectId:c.id,path:a}),c):(this.logger.warn("Project file has no ID",{path:a}),null)}catch(t){return this.logger.error("Failed to read project file",{basePath:e,error:t instanceof Error?t.stack:t+""}),null}}async writeProjectFile(e,t){try{const r=i.default.join(t,"project.json"),a=i.default.join(t,s.ALCHEMY_DIR_NAME,"project.json")
|
|
85
|
-
let n=r
|
|
86
|
-
!o.default.existsSync(r)&&o.default.existsSync(a)?(n=a,this.logger.info("Using existing nested path for project file",{path:n})):this.logger.info("Using direct path for project file",{path:n}),await this.ensureDirectoryExists(i.default.dirname(n)),await o.default.promises.writeFile(n,JSON.stringify(e,null,2),"utf8"),this.logger.info("Wrote project file successfully",{projectId:e.id,path:n})}catch(r){throw this.logger.error("Failed to write project file",{projectId:e.id,basePath:t,error:r instanceof Error?r.stack:r+""}),r}}async ensureDirectoryExists(e){try{o.default.existsSync(e)||(await o.default.promises.mkdir(e,{recursive:!0}),this.logger.info("Created directory",{path:e}))}catch(t){throw this.logger.error("Failed to create directory",{path:e,error:t instanceof Error?t.stack:t+""}),t}}}},5478(e,exports,t){exports.ProjectSynchronizer=exports.ProjectFileSystem=exports.ProjectRegistry=void 0
|
|
87
|
-
var r=t(9955)
|
|
88
|
-
Object.defineProperty(exports,"ProjectRegistry",{enumerable:!0,get:()=>r.ProjectRegistry})
|
|
89
|
-
var o=t(3102)
|
|
90
|
-
Object.defineProperty(exports,"ProjectFileSystem",{enumerable:!0,get:()=>o.DefaultProjectFileSystem})
|
|
91
|
-
var i=t(2568)
|
|
92
|
-
Object.defineProperty(exports,"ProjectSynchronizer",{enumerable:!0,get:()=>i.ProjectSynchronizer}),t(6975)},8029(e,exports,t){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
93
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.ProjectIDBackwardCompat=exports.ProjectIDFactory=exports.ProjectID=void 0
|
|
94
|
-
const o=r(t(6982))
|
|
95
|
-
class i{static create(){return exports.ProjectIDFactory.create()}static fromString(e){return exports.ProjectIDFactory.fromString(e)}static isValid(e){return exports.ProjectIDFactory.isValid(e)}constructor(e){this.id=e}toString(){return this.id}equals(e){return this.id===e.toString()}isStandardFormat(){return this.id.includes("_")&&this.id.startsWith("prj_")}}exports.ProjectID=i,exports.ProjectIDFactory={create(){const e=Date.now().toString().substring(5),t=o.default.randomBytes(4).toString("hex")
|
|
96
|
-
return i.fromString(`prj_${e}_${t}`)},fromString(e){return this.isValid(e)?new i(e):null},isValid(e){if(!e||"string"!=typeof e||5>e.length||!e.startsWith("prj"))return!1
|
|
97
|
-
if(e.includes("_")){const t=e.split("_")
|
|
98
|
-
return t.length>=3&&"prj"===t[0]}return!1}},exports.ProjectIDBackwardCompat={create:exports.ProjectIDFactory.create,fromString:exports.ProjectIDFactory.fromString,isValid:exports.ProjectIDFactory.isValid},Object.assign(i,exports.ProjectIDBackwardCompat)},9955(e,exports,t){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
99
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.ProjectRegistry=void 0
|
|
100
|
-
const o=r(t(9896)),i=r(t(6928))
|
|
101
|
-
exports.ProjectRegistry=class{constructor(e,t={}){this.projects=new Map,this.verifiedIds=new Set,this.verifiedNames=new Map,this.logger=e.child({component:"project-registry"}),this.options={persistToDisk:t.persistToDisk??!1,storageDirectory:t.storageDirectory??"./temp-projects"},this.options.persistToDisk?this.logger.warn("Project persistence to disk is enabled - this may be legacy functionality"):this.logger.info("Project persistence to disk is disabled - this is the recommended configuration"),this.logger.info("ProjectRegistry initialized",{options:this.options,persistenceEnabled:this.options.persistToDisk})}register(e,t={}){if(!e.id)return void this.logger.warn("Attempted to register project without ID")
|
|
102
|
-
const r=t.markVerified??!1,o=t.saveToFile??this.options.persistToDisk
|
|
103
|
-
this.projects.set(e.id,e),r&&(this.verifiedIds.add(e.id),e.name&&this.verifiedNames.set(e.id,e.name)),this.logger.info("Registered project",{projectId:e.id,projectName:e.name,markVerified:r,saveToFile:o}),o&&(this.options.persistToDisk?this.saveProjectToFile(e):this.logger.debug("Skipping project file persistence (disabled in configuration)",{projectId:e.id}))}hasProject(e){return this.projects.has(e)}markVerified(e){this.verifiedIds.add(e),this.logger.info("Marked project as verified",{projectId:e})}isVerified(e){return this.verifiedIds.has(e)}markVerifiedWithName(e,t){this.verifiedIds.add(e),this.verifiedNames.set(e,t),this.logger.info("Marked project as verified with name",{projectId:e,projectName:t})}isVerifiedWithName(e,t){return this.verifiedIds.has(e)&&this.verifiedNames.get(e)===t}getVerifiedName(e){return this.verifiedNames.get(e)}getProject(e){return this.projects.get(e)}getAllProjects(){return Array.from(this.projects.values())}clearVerificationCache(e){e?(this.verifiedIds.delete(e),this.verifiedNames.delete(e),this.logger.info("Cleared verification for project",{projectId:e})):(this.verifiedIds.clear(),this.verifiedNames.clear(),this.logger.info("Cleared all project verifications"))}saveProjectToFile(e){try{if(!o.default.existsSync(this.options.storageDirectory))try{this.logger.info("Creating storage directory",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory)}),o.default.mkdirSync(this.options.storageDirectory,{recursive:!0}),this.logger.info("Storage directory created successfully",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory)})}catch(e){return void this.logger.error("Failed to create storage directory",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory),error:e instanceof Error?e.stack:e+""})}const t=i.default.join(this.options.storageDirectory,`project-${e.id}.json`)
|
|
104
|
-
o.default.writeFileSync(t,JSON.stringify(e,null,2),"utf8"),this.logger.info("Saved project to file",{projectId:e.id,filePath:t})}catch(t){this.logger.error("Failed to save project to file",{projectId:e.id,error:t instanceof Error?t.stack:t+""})}}async loadFromDisk(){try{if(!o.default.existsSync(this.options.storageDirectory))try{return this.logger.info("Storage directory does not exist, creating it",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory)}),o.default.mkdirSync(this.options.storageDirectory,{recursive:!0}),void this.logger.info("Storage directory created successfully",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory)})}catch(e){return void this.logger.error("Failed to create storage directory",{directory:this.options.storageDirectory,absolutePath:i.default.resolve(this.options.storageDirectory),error:e instanceof Error?e.stack:e+""})}const e=o.default.readdirSync(this.options.storageDirectory).filter((e=>e.startsWith("project-")&&e.endsWith(".json")))
|
|
105
|
-
this.logger.info("Loading projects from disk",{directory:this.options.storageDirectory,fileCount:e.length})
|
|
106
|
-
let t=0
|
|
107
|
-
for(const r of e)try{const e=i.default.join(this.options.storageDirectory,r),s=o.default.readFileSync(e,"utf8"),a=JSON.parse(s)
|
|
108
|
-
if(!a.id){this.logger.warn("Skipping project file with missing ID",{filePath:e})
|
|
109
|
-
continue}this.register(a,{saveToFile:!1}),t++}catch(e){this.logger.error("Error loading project file",{file:r,error:e instanceof Error?e.stack:e+""})}this.logger.info("Finished loading projects from disk",{totalLoaded:t,totalFiles:e.length})}catch(e){this.logger.error("Failed to load projects from disk",{error:e instanceof Error?e.stack:e+""})}}setStorageDirectory(e){this.options.storageDirectory=e,this.logger.info("Set storage directory",{directory:e})}setPersistToDisk(e){this.options.persistToDisk=e,this.logger.info("Set persist to disk",{persistToDisk:e})}}}}
|
package/dist/939.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict"
|
|
2
|
-
exports.id=939,exports.ids=[939],exports.modules={9939(e,exports,t){function r(e){return i.default.createHash("md5").update(e).digest("hex")}async function s(e){try{return r(await a.default.promises.readFile(e))}catch{return null}}var n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
3
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.syncPluginsGlobal=async function(e,t,n){const i={synced:[],skipped:[],errors:[]},c=n||function(){const e=process.argv[1]
|
|
4
|
-
let t=e
|
|
5
|
-
try{t=a.default.realpathSync(e)}catch{}return o.default.dirname(t)}(),d=o.default.join((0,u.getGlobalConfigDir)(e),"plugin")
|
|
6
|
-
try{await a.default.promises.mkdir(d,{recursive:!0})}catch(e){return t.error("Failed to create global plugin directory",{destDir:d,error:e instanceof Error?e.message:"Unknown error"}),i}const p=o.default.join(d,"package.json"),f='{"type":"module"}\n'
|
|
7
|
-
try{await a.default.promises.readFile(p,"utf8").catch((()=>null))!==f&&await a.default.promises.writeFile(p,f,"utf8")}catch(e){t.warn("Failed to write global plugin package.json",{error:e instanceof Error?e.message:"Unknown error"})}for(const e of l){const n=o.default.join(c,e.sourceName),u=o.default.join(d,e.destName)
|
|
8
|
-
try{if(!a.default.existsSync(n)){t.warn("Plugin source not found: "+e.sourceName,{sourcePath:n}),i.errors.push(e.destName)
|
|
9
|
-
continue}const o=await a.default.promises.readFile(n,"utf8").catch((()=>null))
|
|
10
|
-
if(null===o){t.warn("Could not read plugin source: "+e.sourceName),i.errors.push(e.destName)
|
|
11
|
-
continue}const l=r(o),c=await s(u)
|
|
12
|
-
null===c||l!==c?(await a.default.promises.writeFile(u,o,"utf8"),t.info("Synced plugin: "+e.destName,{destDir:d,reason:null===c?"not installed":"updated"}),i.synced.push(e.destName)):i.skipped.push(e.destName)}catch(r){t.error("Failed to sync plugin: "+e.destName,{error:r instanceof Error?r.message:"Unknown error"}),i.errors.push(e.destName)}}return i.synced.length>0&&t.info("Synced global plugins",{destDir:d,synced:i.synced}),i},exports.cleanupLegacyProjectPlugins=async function(e,t){const r=o.default.join(e,".opencode","plugin"),s=[]
|
|
13
|
-
if(!a.default.existsSync(r))return{removed:s}
|
|
14
|
-
for(const e of c){const t=o.default.join(r,e)
|
|
15
|
-
if(a.default.existsSync(t))try{await a.default.promises.rm(t,{recursive:!0,force:!0}),s.push(e)}catch{}}s.length>0&&t.info("Removed legacy per-project plugin copies",{projectPluginDir:r,removed:s})
|
|
16
|
-
try{0===(await a.default.promises.readdir(r)).length&&await a.default.promises.rmdir(r)}catch{}return{removed:s}}
|
|
17
|
-
const a=n(t(9896)),o=n(t(6928)),i=n(t(6982)),u=t(5419),l=[{sourceName:"plugins/hooks/plugin.js",destName:"event-hooks.js"},{sourceName:"plugins/inject-session.js",destName:"inject-session.js"},{sourceName:"plugins/system-logger.js",destName:"system-logger.js"}],c=[...l.map((e=>e.destName)),"package.json","lib"]}}
|