alchemylab 1.4.13 → 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 -1611
- package/dist/document-textconv.js +484 -0
- package/dist/http-server.js +7244 -5396
- package/dist/index.js +7257 -5419
- package/dist/mcp-direct.js +7137 -5250
- 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
package/dist/994.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"use strict"
|
|
2
|
-
exports.id=994,exports.ids=[994],exports.modules={5994(t,exports,e){var r=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}}
|
|
3
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.InteractiveShell=void 0
|
|
4
|
-
const s=r(e(3785)),a=e(5317),i=e(3807),n=r(e(6928)),o=r(e(9896)),l=r(e(6895)),c=e(4139),u=e(4192),h=e(1152)
|
|
5
|
-
exports.InteractiveShell=class{constructor(t){if(this.currentBranch="main",this.currentStatus=null,this.history=[],this.statusCache={map:new Map,ts:0},this.pathExecutableCache=null,this.projectPath=t.projectPath,this.currentSymlink=n.default.join(t.projectPath,c.ALCHEMY_DIR_NAME,"current"),this.currentDir=t.projectPath,this.logger=t.logger,this.historyFile=n.default.join(t.projectPath,c.ALCHEMY_DIR_NAME,"shell-history.txt"),this.projectId=t.projectId,t.remoteServerUrl){const e=(0,h.createMetricsCollector)(!1)
|
|
6
|
-
this.apiClient=new u.BaseApiClient(t.remoteServerUrl,t.logger,e,t.authToken)}}async start(t){return this.shutdownCallback=t,this.currentBranch="main",this.updateCurrentSymlink(),this.loadHistory(),this.rl=s.default.createInterface({input:process.stdin,output:process.stdout,prompt:this.buildPrompt(),historySize:500,removeHistoryDuplicates:!0,completer:t=>this.completer(t)}),this.history.length>0&&(this.rl.history=[...this.history].reverse()),this.printHelp(!0),this.rl.prompt(),new Promise((t=>{this.rl.on("line",(async e=>{const r=e.trim()
|
|
7
|
-
if(r){if(this.appendHistory(r),await this.processCommand(r))return this.rl.close(),void t()
|
|
8
|
-
this.rl.prompt()}else this.rl.prompt()}))
|
|
9
|
-
let e,r=!1
|
|
10
|
-
this.rl.on("SIGINT",(()=>{r?(e&&clearTimeout(e),this.rl.close()):(r=!0,console.log(""),console.log(l.default.gray("Press Ctrl+C again to exit, or continue typing.")),this.rl.prompt(),e=setTimeout((()=>{r=!1}),2e3))})),this.rl.on("close",(async()=>{this.shutdownCallback&&await this.shutdownCallback(),t()}))}))}buildPrompt(){const t=l.default.green(this.currentBranch),e=this.currentStatus&&"main"!==this.currentBranch?` ${l.default.gray("|")} ${this.colorStatus(this.currentStatus)}`:""
|
|
11
|
-
return`${l.default.cyan("[")}${t}${e}${l.default.cyan("]")} ${l.default.yellow("alchemy")}${l.default.gray(">")} `}colorStatus(t){switch(t){case"completed":return l.default.green(t)
|
|
12
|
-
case"in_progress":return l.default.yellow(t)
|
|
13
|
-
case"open":return l.default.blue(t)
|
|
14
|
-
case"ready_for_review":return l.default.magenta("review")
|
|
15
|
-
case"canceled":default:return l.default.gray(t)
|
|
16
|
-
case"reverted":return l.default.red(t)}}async fetchRequestStatus(t){if(!this.apiClient||!this.projectId)return null
|
|
17
|
-
try{const e=await this.apiClient.get(`/api/projects/${this.projectId}/requests/${t}`),r=e?.data||e
|
|
18
|
-
return r?.status||null}catch{return null}}async fetchBulkStatuses(){if(1e4>Date.now()-this.statusCache.ts&&this.statusCache.map.size>0)return this.statusCache.map
|
|
19
|
-
if(!this.apiClient||!this.projectId)return new Map
|
|
20
|
-
try{const t=await this.apiClient.get(`/api/projects/${this.projectId}/requests?limit=100&sortBy=updated&sortDirection=desc`),e=t?.data||t||[],r=new Map
|
|
21
|
-
for(const t of Array.isArray(e)?e:[])t.localId&&t.status&&r.set(t.localId,t.status)
|
|
22
|
-
return this.statusCache={map:r,ts:Date.now()},r}catch{return this.statusCache.map}}formatRequestWithStatus(t,e,r){return`${r?l.default.green(t):t}${e?" "+this.colorStatus(e):""}`}updatePrompt(){this.rl.setPrompt(this.buildPrompt())}getPathExecutables(){if(this.pathExecutableCache)return this.pathExecutableCache
|
|
23
|
-
const t=new Set,e=(process.env.PATH||"").split(n.default.delimiter)
|
|
24
|
-
for(const r of e)try{if(!o.default.existsSync(r))continue
|
|
25
|
-
for(const e of o.default.readdirSync(r))t.add(e)}catch{}return this.pathExecutableCache=Array.from(t).sort(),this.pathExecutableCache}completer(t){const e=["br","branch","logs","pwd","cd ","help","exit","quit"],r=t.includes(" "),s=t.indexOf(" "),a=r?t.slice(0,s).toLowerCase():""
|
|
26
|
-
if(r&&("br"===a||"branch"===a))return setImmediate((()=>this.rl.write("\n"))),[[],""]
|
|
27
|
-
if(!r){const r=e.filter((e=>e.startsWith(t))),s=t?this.getPathExecutables().filter((e=>e.startsWith(t))):[],a=[...new Set([...r,...s])]
|
|
28
|
-
if(a.length>0)return[a,t]
|
|
29
|
-
if(!t)return[e,t]}const i=t.lastIndexOf(" "),l=0>i?t:t.slice(i+1),c=0>i?"":t.slice(0,i+1)
|
|
30
|
-
try{const e=l.includes(n.default.sep)||l.includes("/"),r=e?n.default.resolve(this.currentDir,n.default.dirname(l)):this.currentDir,s=e?n.default.basename(l):l,a=e?n.default.dirname(l)+"/":""
|
|
31
|
-
if(!o.default.existsSync(r))return[[],t]
|
|
32
|
-
const i=o.default.readdirSync(r,{withFileTypes:!0}).filter((t=>t.name.startsWith(s)&&!t.name.startsWith("."))).map((t=>{const e=t.isDirectory()?t.name+"/":t.name
|
|
33
|
-
return c+a+e}))
|
|
34
|
-
if(i.length>0)return[i,t]}catch{}return[[],t]}async detectCurrentBranch(){try{const t=(0,i.simpleGit)(this.currentDir),e=(await t.branch()).current||"main"
|
|
35
|
-
this.currentBranch=this.extractRequestId(e)||e}catch{this.currentBranch="main"}}async processCommand(t){const e=t.split(/\s+/),r=e[0].toLowerCase(),s=e.slice(1)
|
|
36
|
-
switch(r){case"exit":case"quit":return this.shutdownCallback&&await this.shutdownCallback(),!0
|
|
37
|
-
case"help":return this.printHelp(!1),!1
|
|
38
|
-
case"br":case"branch":return await this.listBranches(s.length>0?s.join(" "):void 0),!1
|
|
39
|
-
case"logs":return await this.tailLogs(s),!1
|
|
40
|
-
case"pwd":return console.log(this.getVirtualCwd()),!1
|
|
41
|
-
case"cd":return this.changeDirectory(s[0]||"~"),!1
|
|
42
|
-
default:return this.executeCommand(t),!1}}changeDirectory(t){try{let e
|
|
43
|
-
if(e="~"===t||""===t?this.currentSymlink:n.default.isAbsolute(t)?t:n.default.resolve(this.currentDir,t),!o.default.existsSync(e)||!o.default.statSync(e).isDirectory())return void console.log(l.default.red("cd: no such directory: "+t))
|
|
44
|
-
this.currentDir=e}catch(t){console.log(l.default.red("cd: "+t.message))}}async tailLogs(t){const r=n.default.join(this.projectPath,c.ALCHEMY_DIR_NAME,"server.log")
|
|
45
|
-
if(!o.default.existsSync(r))return void console.log(l.default.gray("No log file found yet. Logs will appear once the server processes requests."))
|
|
46
|
-
const s=t[0]&&/^\d+$/.test(t[0])?parseInt(t[0],10):50
|
|
47
|
-
console.log(l.default.gray(`Tailing ${r} (last ${s} lines, Ctrl+C to stop)`))
|
|
48
|
-
const{spawn:a}=await Promise.resolve().then(e.t.bind(e,5317,23)),i=a("tail",["-n",s+"","-f",r],{stdio:["ignore","inherit","inherit"]})
|
|
49
|
-
return new Promise((t=>{const e=process.stdin,r=e.isRaw
|
|
50
|
-
this.rl.pause(),e.setRawMode(!0),e.resume()
|
|
51
|
-
const s=()=>{e.removeListener("data",a)
|
|
52
|
-
try{i.kill("SIGTERM")}catch{}try{e.setRawMode(r??!1)}catch{}this.rl.resume()},a=t=>{3!==t[0]&&"q"!==t.toString()||i.kill("SIGTERM")}
|
|
53
|
-
e.on("data",a),i.on("close",(()=>{s(),t()}))}))}updateCurrentSymlink(){try{let t
|
|
54
|
-
if("main"===this.currentBranch)t=this.projectPath
|
|
55
|
-
else{const e=n.default.join(this.projectPath,c.ALCHEMY_DIR_NAME,"worktrees",this.currentBranch)
|
|
56
|
-
t=o.default.existsSync(e)?e:this.projectPath}try{o.default.unlinkSync(this.currentSymlink)}catch{}o.default.symlinkSync(t,this.currentSymlink),this.currentDir=this.currentSymlink}catch(t){this.logger.warn("Failed to update current symlink",{error:t.message})}}getVirtualCwd(){return this.currentDir}executeCommand(t){try{(0,a.execSync)(t,{cwd:this.currentDir,stdio:"inherit",env:{...process.env,PWD:this.currentDir,GIT_DIR:void 0,GIT_WORK_TREE:void 0}})}catch(t){void 0!==t.status&&null!==t.status||console.log(l.default.red("Error: "+t.message))}}getWorktreeRequestIds(){const t=n.default.join(this.projectPath,c.ALCHEMY_DIR_NAME,"worktrees"),e=new Set
|
|
57
|
-
if(o.default.existsSync(t))for(const r of o.default.readdirSync(t,{withFileTypes:!0}))r.isDirectory()&&e.add(r.name)
|
|
58
|
-
return e}extractRequestId(t){const e=t.match(/^(?:requests\/)?(req\d+)/)
|
|
59
|
-
return e?e[1]:null}async listBranches(t){try{const e=(0,i.simpleGit)(this.projectPath),r=await e.branch(),s=this.getWorktreeRequestIds(),a=await this.fetchBulkStatuses(),n=new Set,o=[],c=[]
|
|
60
|
-
for(const t of r.all){if(t.startsWith("remotes/"))continue
|
|
61
|
-
if("main"===t)continue
|
|
62
|
-
const e=this.extractRequestId(t)
|
|
63
|
-
e&&!n.has(e)&&(n.add(e),o.push(e))}for(const t of s)n.has(t)||(n.add(t),o.push(t))
|
|
64
|
-
o.sort(((t,e)=>t.localeCompare(e)))
|
|
65
|
-
for(const t of o){const e=this.currentBranch===t||this.currentBranch==="requests/"+t
|
|
66
|
-
c.push(this.formatRequestWithStatus(t,a.get(t),e))}if(o.push("main"),c.push("main"===this.currentBranch?l.default.green("main"):"main"),0===o.length)return void console.log(l.default.gray("No branches found."))
|
|
67
|
-
if(t&&o.includes(t))return void(t!==this.currentBranch&&await this.switchBranch(t))
|
|
68
|
-
let u=o.indexOf(this.currentBranch)
|
|
69
|
-
0>u&&(u=o.length-1)
|
|
70
|
-
const h=await this.selectFromList(o,"Select branch:",c,t?0:u,!0,t)
|
|
71
|
-
this.rl.pause(),h&&h!==this.currentBranch?await this.switchBranch(h):h||console.log(l.default.gray("Cancelled.")),this.rl.resume()}catch(t){console.log(l.default.red("Failed to list branches: "+t.message))}}async ensureWorktree(t,e){const r=n.default.join(this.projectPath,c.ALCHEMY_DIR_NAME,"worktrees"),s=n.default.join(r,t)
|
|
72
|
-
if(o.default.existsSync(s)&&o.default.statSync(s).isDirectory())return s
|
|
73
|
-
console.log(l.default.gray(`Creating worktree for ${t}...`)),o.default.mkdirSync(r,{recursive:!0})
|
|
74
|
-
const a=(0,i.simpleGit)(this.projectPath)
|
|
75
|
-
return await a.raw(["worktree","add",s,e]),console.log(l.default.gray("Worktree created at "+s)),s}async selectFromList(t,e,r,s,a,i){const n=t,o=r||t
|
|
76
|
-
return new Promise((t=>{let r=0
|
|
77
|
-
const c=process.stdin,u=c.isRaw
|
|
78
|
-
let h=!1,d=i||"",f=n.map(((t,e)=>e))
|
|
79
|
-
null!=s&&s>=0&&s<n.length&&(r=s)
|
|
80
|
-
const p=()=>{if(!d)return void(f=n.map(((t,e)=>e)))
|
|
81
|
-
const t=d.toLowerCase()
|
|
82
|
-
let e=n.map(((t,e)=>({item:t,i:e}))).filter((({item:e})=>e.toLowerCase().startsWith(t)))
|
|
83
|
-
0===e.length&&/^\d+$/.test(t)&&(e=n.map(((t,e)=>({item:t,i:e}))).filter((({item:e})=>e.replace(/\D/g,"").endsWith(t)))),0===e.length&&(e=n.map(((t,e)=>({item:t,i:e}))).filter((({item:e})=>e.toLowerCase().includes(t)))),f=e.map((t=>t.i))},y=process.stdout.rows||24,g=Math.max(3,y-4)
|
|
84
|
-
let m=0,w=0,S=0
|
|
85
|
-
this.rl.pause()
|
|
86
|
-
const C=()=>{const t=f.length,s=t>g
|
|
87
|
-
s?m>r?m=r:m+g>r||(m=r-g+1):m=0
|
|
88
|
-
const i=(()=>{const t=f.length
|
|
89
|
-
if(0===t)return 1
|
|
90
|
-
const e=t>g
|
|
91
|
-
let r=e?Math.min(g,t-m):t
|
|
92
|
-
return e&&m>0&&r++,e&&t>m+g&&r++,r})(),n=i+1
|
|
93
|
-
$&&S>1&&process.stdout.write(`\r\x1b[${S-1}A\r`),n>w&&(w=n),S=w
|
|
94
|
-
const c=w-n
|
|
95
|
-
for(let t=0;c>t;t++)process.stdout.write("\x1b[2K\n")
|
|
96
|
-
if(0===t)process.stdout.write(`\x1b[2K ${l.default.gray("no matches")}\n`)
|
|
97
|
-
else{const e=s&&m>0,a=s&&t>m+g,i=s?Math.min(m+g,t):t,n=s?m:0
|
|
98
|
-
e&&process.stdout.write(`\x1b[2K ${l.default.gray(`\u25b2 ${m} more`)}\n`)
|
|
99
|
-
for(let t=n;i>t;t++){const e=f[t],s=t===r?l.default.cyan("\u276f "):" ",a=t===r?l.default.cyan(o[e]):o[e]
|
|
100
|
-
process.stdout.write(`\x1b[2K${s}${a}\n`)}a&&process.stdout.write(`\x1b[2K ${l.default.gray(`\u25bc ${t-i} more`)}\n`)}a&&d?process.stdout.write(`\x1b[2K${l.default.gray(">")} ${l.default.yellow(d)}${l.default.gray("_")}`):a?process.stdout.write(`\x1b[2K${l.default.gray(">")} ${l.default.gray("type to filter...")}`):process.stdout.write("\x1b[2K"+l.default.gray(e)),$=!0}
|
|
101
|
-
d&&(p(),r=0)
|
|
102
|
-
let $=!1
|
|
103
|
-
C(),c.setRawMode(!0),c.resume()
|
|
104
|
-
const b=()=>{if(!h){h=!0,c.removeListener("data",v)
|
|
105
|
-
try{c.setRawMode(u??!1)}catch{}$&&S>1&&process.stdout.write(`\r\x1b[${S-1}A\r`),process.stdout.write("\x1b[J"),this.rl.resume()}},v=e=>{if(!h)try{const s=e.toString()
|
|
106
|
-
"\x1b[A"===s?f.length>0&&(r=Math.max(0,r-1),C()):"\x1b[B"===s?f.length>0&&(r=Math.min(f.length-1,r+1),C()):"\r"===s||"\n"===s?f.length>0&&(b(),t(n[f[r]])):"\x1b"===s||"\x03"===s?(b(),t(null)):a&&"\x7f"===s?d.length>0&&(d=d.slice(0,-1),p(),r=0,m=0,C()):!a||1!==s.length||" ">s||s>"~"||(d+=s,p(),r=0,m=0,C())}catch{b(),t(null)}}
|
|
107
|
-
c.on("data",v)}))}findMatchingBranches(t,e,r){const s=new Set
|
|
108
|
-
for(const t of e){const e=this.extractRequestId(t)
|
|
109
|
-
e&&s.add(e)}for(const t of r)s.add(t)
|
|
110
|
-
const a=Array.from(s)
|
|
111
|
-
if(s.has(t))return[t]
|
|
112
|
-
const i=a.filter((e=>e.startsWith(t)))
|
|
113
|
-
if(i.length>0)return i.sort()
|
|
114
|
-
if(/^\d+$/.test(t)){const e=a.filter((e=>e.replace(/\D/g,"").endsWith(t)))
|
|
115
|
-
if(e.length>0)return e.sort()}return[]}async switchBranch(t){try{if("main"===t)return this.currentBranch="main",this.currentStatus=null,this.updateCurrentSymlink(),this.updatePrompt(),console.log(l.default.green("Switched to main")),void console.log(l.default.gray(" "+this.currentDir))
|
|
116
|
-
const e=(0,i.simpleGit)(this.projectPath),r=await e.branch(),s=this.getWorktreeRequestIds(),a=this.findMatchingBranches(t,r.all,s)
|
|
117
|
-
if(0===a.length)return console.log(l.default.red("No branch matching: "+t)),void console.log(l.default.gray(' Use "branches" to see available branches'))
|
|
118
|
-
let n
|
|
119
|
-
if(1===a.length)n=a[0]
|
|
120
|
-
else{const e=await this.fetchBulkStatuses(),r=a.map((t=>this.formatRequestWithStatus(t,e.get(t),!1))),s=await this.selectFromList(a,`Multiple matches for "${t}" \u2014 select one:`,r,void 0,!0)
|
|
121
|
-
if(!s)return void console.log(l.default.gray("Cancelled."))
|
|
122
|
-
n=s}const o=r.all.includes("requests/"+n)?"requests/"+n:n
|
|
123
|
-
await this.ensureWorktree(n,o),this.currentBranch=n,this.updateCurrentSymlink(),this.currentStatus=await this.fetchRequestStatus(n),this.updatePrompt()
|
|
124
|
-
const c=this.currentStatus?` ${l.default.gray("|")} ${this.colorStatus(this.currentStatus)}`:""
|
|
125
|
-
console.log(l.default.green("Switched to "+n)+c),console.log(l.default.gray(" "+this.getVirtualCwd()))}catch(t){console.log(l.default.red("Failed to switch: "+t.message))}}printHelp(t){t?(console.log(""),console.log(l.default.gray('Type "help" for available commands. Commands run in the current branch directory.')),console.log("")):(console.log(""),console.log(l.default.bold("Interactive Shell Commands:")),console.log(` ${l.default.yellow("branch")} / ${l.default.yellow("br")} Browse and switch branches`),console.log(` ${l.default.yellow("logs")} ${l.default.gray("[N]")} Tail action server log (Ctrl+C to stop, N = lines of history)`),console.log(` ${l.default.yellow("cd")} ${l.default.gray("<dir>")} Change directory within current branch`),console.log(` ${l.default.yellow("pwd")} Show current working directory`),console.log(` ${l.default.yellow("help")} Show this help`),console.log(` ${l.default.yellow("exit")} / ${l.default.yellow("quit")} Stop the server and exit`),console.log(""),console.log(l.default.gray(" Any other input is executed as a shell command in the current directory.")),console.log(""))}loadHistory(){try{if(o.default.existsSync(this.historyFile)){const t=o.default.readFileSync(this.historyFile,"utf-8")
|
|
126
|
-
this.history=t.split("\n").filter((t=>""!==t.trim())),this.history.length>500&&(this.history=this.history.slice(-500))}}catch{}}appendHistory(t){try{this.history.push(t),o.default.appendFileSync(this.historyFile,t+"\n")}catch{}}}}}
|
package/dist/mcp-cli-bridge.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
(()=>{"use strict"
|
|
2
|
-
function e(n){var i=s[n]
|
|
3
|
-
if(void 0!==i)return i.exports
|
|
4
|
-
var module=s[n]={exports:{}}
|
|
5
|
-
return t[n].call(module.exports,module,module.exports,e),module.exports}var t={1569(module){module.exports=require("zod")},2904(e,exports,t){function s(e){return{content:[{type:"text",text:e}]}}function n(e){return{content:[{type:"text",text:"Error: "+e}]}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.CLIBridgeOperationsSchema=void 0,exports.handleCLIBridgeOperations=async function(e){const t=(0,o.getCLIBridge)()
|
|
6
|
-
if(!t)return n("CLI Bridge not initialized. The action server may not be running.")
|
|
7
|
-
const{operation:i}=e
|
|
8
|
-
switch(i){case"list-sessions":{const i=await t.sendCommand("list-sessions",{includeArchived:e.includeArchived,status:e.status,agentType:e.agentType,search:e.search,limit:e.limit})
|
|
9
|
-
return i.success?s(function(e){if(!e||0===e.length)return"No sessions found."
|
|
10
|
-
const t=e.map((e=>{const t=[`[${e.status?.toUpperCase()||"?"}]`,e.id,e.agentType?`(${e.agentType})`:"",e.title||"(untitled)"]
|
|
11
|
-
return e.parentId&&t.push(" parent: "+e.parentId),t.filter(Boolean).join(" ")}))
|
|
12
|
-
return`Sessions (${e.length}):\n${t.join("\n")}`}(i.data)):n(i.error||"Failed to list sessions")}case"get-messages":{if(!e.sessionId)return n("sessionId is required for get-messages")
|
|
13
|
-
const i=await t.sendCommand("get-messages",{sessionId:e.sessionId,limit:e.limit})
|
|
14
|
-
return i.success?s((r=i.data)&&0!==r.length?r.map((e=>{const t=e.role?.toUpperCase()||"?",s=e.agent?` [${e.agent}]`:"",n=e.time?new Date(e.time).toLocaleTimeString():"",i=e.completedAt?"":" (streaming...)",o=(e.parts||[]).filter((e=>"text"===e.type&&e.text)).map((e=>e.text)).join("\n"),r=(e.parts||[]).filter((e=>"tool"===e.type||"patch"===e.type)).map((e=>{if("tool"===e.type){const t=e.state?.status||"unknown"
|
|
15
|
-
return` [tool: ${e.tool} \u2192 ${t}]`}return"patch"===e.type?` [patch: ${(Array.isArray(e.files)?e.files:[]).map((e=>e.split("/").pop())).join(", ")}]`:""})).filter(Boolean).join("\n")
|
|
16
|
-
return`--- ${t}${s} ${n}${i} ---\n${[o,r].filter(Boolean).join("\n")}`})).join("\n\n"):"No messages found."):n(i.error||"Failed to get messages")}case"send-message":{if(!e.sessionId)return n("sessionId is required for send-message")
|
|
17
|
-
if(!e.message)return n("message is required for send-message")
|
|
18
|
-
let i=e.agent
|
|
19
|
-
if(!i){const s=await t.sendCommand("get-session-status",{sessionId:e.sessionId})
|
|
20
|
-
s.success&&s.data?.agentType&&(i=s.data.agentType)}const o=await t.sendCommand("send-message",{sessionId:e.sessionId,message:e.message,agent:i,variant:e.variant,system:e.system})
|
|
21
|
-
return o.success?s("Message sent successfully."+(o.data?.messageId?" Message ID: "+o.data.messageId:"")):n(o.error||"Failed to send message")}case"create-session":{const i=await t.sendCommand("create-session",{directory:e.directory,title:e.title,parentId:e.parentId,agent:e.agent})
|
|
22
|
-
if(!i.success)return n(i.error||"Failed to create session")
|
|
23
|
-
const o=i.data
|
|
24
|
-
return s(`Session created: ${o.id}\nTitle: ${o.title||"(none)"}\nAgent: ${o.agentType||"default"}\nStatus: ${o.status}`)}case"get-session-status":{if(!e.sessionId)return n("sessionId is required for get-session-status")
|
|
25
|
-
const i=await t.sendCommand("get-session-status",{sessionId:e.sessionId})
|
|
26
|
-
if(!i.success)return n(i.error||"Failed to get session status")
|
|
27
|
-
const o=i.data
|
|
28
|
-
return s(`Session: ${o.id}\nTitle: ${o.title||"(none)"}\nStatus: ${o.status}\nAgent: ${o.agentType||"default"}`)}case"list-notifications":{const i=await t.sendCommand("list-notifications",{sessionId:e.sessionId,messageType:e.messageType,status:e.status,limit:e.limit})
|
|
29
|
-
return i.success?s(function(e){if(!e||0===e.length)return"No notifications found."
|
|
30
|
-
const t=e.map((e=>{const t="question"===e.messageType?"Q":"R",s="question"===e.messageType?"pending"===e.status?"PENDING":e.status?.toUpperCase():"unread"===e.readStatus?"UNREAD":e.readStatus?.toUpperCase(),n=e.content?.question||e.content?.summary||"(no summary)",i=e.createdAt?new Date(e.createdAt).toLocaleTimeString():""
|
|
31
|
-
return`[${t}] [${s}] ${e.messageCode} ${n} (${i})`}))
|
|
32
|
-
return`Notifications (${e.length}):\n${t.join("\n")}`}(i.data)):n(i.error||"Failed to list notifications")}case"get-notification":{if(!e.messageCode)return n("messageCode is required for get-notification")
|
|
33
|
-
const i=await t.sendCommand("get-notification",{messageCode:e.messageCode})
|
|
34
|
-
return i.success?s(function(e){if(!e)return"Notification not found."
|
|
35
|
-
const t=[]
|
|
36
|
-
return t.push("Code: "+e.messageCode),t.push("Type: "+e.messageType),t.push("Status: "+e.status),t.push("From: "+e.fromSessionId),e.toSessionId&&t.push("To: "+e.toSessionId),t.push("Created: "+e.createdAt),"question"===e.messageType?(t.push("Question: "+(e.content?.question||"(none)")),e.content?.questionType&&t.push("Question Type: "+e.content.questionType),e.content?.context&&t.push("Context: "+e.content.context),e.content?.options?.length&&(t.push("Options:"),e.content.options.forEach((e=>{t.push(` - ${e.id}: ${e.label}${e.description?" \u2014 "+e.description:""}`)})))):"report"===e.messageType&&(t.push("Read Status: "+e.readStatus),e.content?.reportStatus&&t.push("Report Status: "+e.content.reportStatus),t.push("Summary: "+(e.content?.summary||"(none)")),e.content?.details&&t.push("Details: "+e.content.details)),e.response&&(t.push("Response: "+JSON.stringify(e.response)),e.respondedBySessionId&&t.push("Responded By: "+e.respondedBySessionId),e.respondedAt&&t.push("Responded At: "+e.respondedAt)),t.join("\n")}(i.data)):n(i.error||"Failed to get notification")}case"respond-to-notification":{if(!e.messageCode)return n("messageCode is required for respond-to-notification")
|
|
37
|
-
const i=await t.sendCommand("respond-to-notification",{messageCode:e.messageCode,responseText:e.responseText,selectedOptions:e.selectedOptions})
|
|
38
|
-
return i.success?s(`Response sent for ${e.messageCode}.`):n(i.error||"Failed to respond to notification")}case"list-requests":{const i=await t.sendCommand("list-requests",{status:e.status,type:e.type,limit:e.limit,offset:e.offset,sortBy:e.sortBy,sortDirection:e.sortDirection})
|
|
39
|
-
return i.success?s(function(e){if(!e||0===e.length)return"No requests found."
|
|
40
|
-
const t=e.map((e=>{const t=e.status?.toUpperCase()||"?",s=e.type||"",n=e.updatedAt?new Date(e.updatedAt).toLocaleDateString():""
|
|
41
|
-
return`[${t}] ${e.id} ${s} ${e.title||"(untitled)"} (${n})`}))
|
|
42
|
-
return`Requests (${e.length}):\n${t.join("\n")}`}(i.data)):n(i.error||"Failed to list requests")}case"get-request":{if(!e.requestId)return n("requestId is required for get-request")
|
|
43
|
-
const i=await t.sendCommand("get-request",{requestId:e.requestId})
|
|
44
|
-
return i.success?s(function(e){if(!e)return"Request not found."
|
|
45
|
-
const t=[]
|
|
46
|
-
return t.push("ID: "+e.id),t.push("Title: "+(e.title||"(none)")),t.push("Type: "+(e.type||"(none)")),t.push("Status: "+e.status),e.description&&t.push("Description: "+e.description),e.acceptanceCriteria?.length&&(t.push("Acceptance Criteria:"),e.acceptanceCriteria.forEach(((e,s)=>{const n="string"==typeof e?e:e.description||JSON.stringify(e)
|
|
47
|
-
t.push(` ${s+1}. ${n}`)}))),t.push("Created: "+e.createdAt),t.push("Updated: "+e.updatedAt),e.tasks?.length&&(t.push(""),t.push(`Tasks (${e.tasks.length}):`),e.tasks.forEach((e=>{const s=e.status?.toUpperCase()||"?"
|
|
48
|
-
t.push(` [${s}] ${e.id} ${e.title||"(untitled)"}`)}))),t.join("\n")}(i.data)):n(i.error||"Failed to get request")}case"wait-for-idle":{if(!e.sessionId)return n("sessionId is required for wait-for-idle")
|
|
49
|
-
const i=e.timeoutSeconds||120,o=Date.now()+1e3*i,r=2e3
|
|
50
|
-
let a=!1
|
|
51
|
-
const c=Date.now()+1e4
|
|
52
|
-
for(;Date.now()<c&&Date.now()<o;){const s=await t.sendCommand("get-session-status",{sessionId:e.sessionId})
|
|
53
|
-
if(!s.success)return n(s.error||"Failed to check session status")
|
|
54
|
-
if("busy"===s.data.status){a=!0
|
|
55
|
-
break}await new Promise((e=>setTimeout(e,500)))}for(;Date.now()<o;){const i=await t.sendCommand("get-session-status",{sessionId:e.sessionId})
|
|
56
|
-
if(!i.success)return n(i.error||"Failed to check session status")
|
|
57
|
-
const o=i.data
|
|
58
|
-
if(("idle"===o.status||"error"===o.status)&&a)return s(`Session ${o.id} is now ${o.status}. Title: ${o.title||"(none)"}`)
|
|
59
|
-
"busy"===o.status&&(a=!0),await new Promise((e=>setTimeout(e,r)))}return n(`Session ${e.sessionId} did not become idle within ${i}s`)}default:return n("Unknown operation: "+i)}var r}
|
|
60
|
-
const i=t(1569),o=t(8524)
|
|
61
|
-
exports.CLIBridgeOperationsSchema=i.z.object({operation:i.z.enum(["list-sessions","get-messages","send-message","create-session","get-session-status","wait-for-idle","list-notifications","get-notification","respond-to-notification","list-requests","get-request"]).describe("The CLI operation to perform"),sessionId:i.z.string().optional().describe("OpenCode session ID"),message:i.z.string().optional().describe("Message text to send"),agent:i.z.string().optional().describe("Agent to use for the message"),variant:i.z.string().optional().describe('Thinking variant (e.g., "high", "max")'),system:i.z.string().optional().describe("System instructions for the message"),limit:i.z.number().optional().describe("Max number of messages to return"),directory:i.z.string().optional().describe("Directory for the new session"),title:i.z.string().optional().describe("Title for the new session"),parentId:i.z.string().optional().describe("Parent session ID for child sessions"),timeoutSeconds:i.z.number().optional().describe("Max seconds to wait (default: 120)"),includeArchived:i.z.boolean().optional().describe("Include archived sessions"),status:i.z.string().optional().describe('Filter sessions by status (e.g., "idle", "busy")'),agentType:i.z.string().optional().describe("Filter sessions by agent type"),search:i.z.string().optional().describe("Search sessions by title (case-insensitive substring match)"),requestId:i.z.string().optional().describe('Request ID (e.g., "req004643")'),type:i.z.string().optional().describe('Filter requests by type (e.g., "feature", "bugfix")'),offset:i.z.number().optional().describe("Offset for paginated results"),sortBy:i.z.string().optional().describe('Sort field (e.g., "created", "updated")'),sortDirection:i.z.string().optional().describe('Sort direction ("asc" or "desc")'),messageCode:i.z.string().optional().describe('Session message code (e.g., "Q-abc-001" or "R-abc-001")'),responseText:i.z.string().optional().describe("Text answer for free-form questions"),selectedOptions:i.z.array(i.z.string()).optional().describe("Selected option IDs for alternatives questions"),messageType:i.z.string().optional().describe('Filter by message type: "question" or "report"')})},5086(module){module.exports=require("ws")},5106(module){module.exports=require("zod-to-json-schema")},5802(module){module.exports=require("@modelcontextprotocol/sdk/server/stdio.js")},6865(module){module.exports=require("@modelcontextprotocol/sdk/server/index.js")},6928(module){module.exports=require("path")},6982(module){module.exports=require("crypto")},8524(e,exports,t){var s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
62
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.CLIBridgeWebSocket=void 0,exports.setCLIBridge=function(e){o=e},exports.getCLIBridge=function(){return o}
|
|
63
|
-
const n=t(5086),i=s(t(6982))
|
|
64
|
-
exports.CLIBridgeWebSocket=class{constructor(e,t=3e4){this.wss=null,this.uiClient=null,this.pendingCommands=new Map,this.heartbeatInterval=null,this.logger=e,this.defaultTimeout=t}attach(e){this.wss=new n.WebSocketServer({server:e,path:"/ws/cli-bridge"}),this.wss.on("connection",(e=>{this.logger.info("[CLIBridge] UI client connected"),this.uiClient&&(this.logger.warn("[CLIBridge] Replacing existing UI client connection"),this.uiClient.close(1e3,"Replaced by new connection")),this.uiClient=e,this.startHeartbeat(),e.on("message",(e=>{try{const t=JSON.parse(e.toString())
|
|
65
|
-
this.handleResponse(t)}catch(e){this.logger.error("[CLIBridge] Failed to parse message from UI",{error:e instanceof Error?e.message:e+""})}})),e.on("close",(()=>{this.logger.info("[CLIBridge] UI client disconnected"),this.uiClient===e&&(this.uiClient=null),this.stopHeartbeat()
|
|
66
|
-
for(const[e,t]of this.pendingCommands)clearTimeout(t.timer),t.resolve({id:e,success:!1,error:"UI client disconnected during command. The command may have been partially executed."}),this.pendingCommands.delete(e)})),e.on("error",(e=>{this.logger.error("[CLIBridge] WebSocket error",{error:e.message})}))})),this.logger.info("[CLIBridge] WebSocket server attached at /ws/cli-bridge")}isUIConnected(){return null!==this.uiClient&&this.uiClient.readyState===n.WebSocket.OPEN}async sendCommand(e,t={},s){if(!this.isUIConnected())return{id:"",success:!1,error:"alchemy-ui is not connected. Please open alchemy-ui in your browser first."}
|
|
67
|
-
const n=i.default.randomUUID(),o={id:n,action:e,params:t}
|
|
68
|
-
return new Promise(((t,i)=>{const r=setTimeout((()=>{this.pendingCommands.delete(n),t({id:n,success:!1,error:`Command '${e}' timed out after ${(s||this.defaultTimeout)/1e3}s`})}),s||this.defaultTimeout)
|
|
69
|
-
this.pendingCommands.set(n,{resolve:t,reject:i,timer:r})
|
|
70
|
-
try{this.uiClient.send(JSON.stringify(o))}catch(e){clearTimeout(r),this.pendingCommands.delete(n),t({id:n,success:!1,error:"Failed to send command: "+(e instanceof Error?e.message:e+"")})}}))}handleResponse(e){const t=this.pendingCommands.get(e.id)
|
|
71
|
-
t?(clearTimeout(t.timer),this.pendingCommands.delete(e.id),t.resolve(e)):this.logger.warn("[CLIBridge] Received response for unknown command",{id:e.id})}startHeartbeat(){this.stopHeartbeat(),this.heartbeatInterval=setInterval((()=>{this.uiClient&&this.uiClient.readyState===n.WebSocket.OPEN&&this.uiClient.ping()}),3e4)}stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}close(){this.stopHeartbeat()
|
|
72
|
-
for(const[e,t]of this.pendingCommands)clearTimeout(t.timer),t.reject(Error("Bridge shutting down")),this.pendingCommands.delete(e)
|
|
73
|
-
this.uiClient&&(this.uiClient.close(1e3,"Server shutting down"),this.uiClient=null),this.wss&&(this.wss.close(),this.wss=null)}}
|
|
74
|
-
let o=null},9150(module){module.exports=require("@modelcontextprotocol/sdk/types.js")},9414(e,exports,t){function s(e){const t=Date.now(),s=!e
|
|
75
|
-
if(s&&null!==c&&u>t-d)return c
|
|
76
|
-
const l=(0,i.join)(e||process.env.ALCHEMY_PROJECT_PATH||process.cwd(),r,a)
|
|
77
|
-
if((0,n.existsSync)(l))try{const e=(0,n.readFileSync)(l,"utf-8"),i=JSON.parse(e)
|
|
78
|
-
if("number"==typeof i.port&&i.port>0&&65536>i.port)return s&&(c=i.port,d=t),i.port}catch{}return s&&(c=o,d=t),o}Object.defineProperty(exports,"__esModule",{value:!0}),exports.getActionServerPort=s,exports.getActionServerBaseUrl=function(){return"http://localhost:"+s()}
|
|
79
|
-
const n=t(9896),i=t(6928),o=3456,r=".alchemy_lab",a="action-server.json"
|
|
80
|
-
let c=null,d=0
|
|
81
|
-
const u=5e3},9896(module){module.exports=require("fs")}},s={},n={};(()=>{Object.defineProperty(n,"__esModule",{value:!0})
|
|
82
|
-
const t=e(6865),s=e(5802),i=e(9150),o=e(5106),r=e(2904),a=e(8524),c=process.argv.slice(2)
|
|
83
|
-
let d=null
|
|
84
|
-
for(let e=0;e<c.length;e++)"--port"===c[e]&&c[e+1]&&(d=parseInt(c[e+1],10),e++)
|
|
85
|
-
if(null===d)try{const{getActionServerPort:t}=e(9414)
|
|
86
|
-
d=t()}catch{d=3456}const u=(...e)=>process.stderr.write(`[cli-bridge] ${e.join(" ")}\n`),l=new class{isUIConnected(){return!0}async sendCommand(e,t={},s){return async function(e,t,s){const n=`http://127.0.0.1:${d}/api/cli-bridge/command`,i=new AbortController,o=s||3e4,r=setTimeout((()=>i.abort()),o+5e3)
|
|
87
|
-
try{const s=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,params:t,timeout:o}),signal:i.signal})
|
|
88
|
-
if(!s.ok){const e=await s.text()
|
|
89
|
-
return{success:!1,error:`Action server error (${s.status}): ${e}`}}return await s.json()}catch(e){return"AbortError"===e.name?{success:!1,error:`Command timed out after ${o/1e3}s`}:{success:!1,error:`Cannot reach action server at port ${d}: ${e.message}`}}finally{clearTimeout(r)}}(e,t,s)}};(0,a.setCLIBridge)(l)
|
|
90
|
-
const p=new t.Server({name:"alchemy-cli-bridge",version:"1.0.0"},{capabilities:{tools:{}}})
|
|
91
|
-
p.setRequestHandler(i.ListToolsRequestSchema,(async()=>({tools:[{name:"cli_operations",description:"Interact with OpenCode CLI sessions through alchemy-ui.\n\nOperations:\n list-sessions \u2014 List all active sessions with status and agent type\n Filters: status?, agentType?, search? (title), limit?, includeArchived?\n get-messages \u2014 Get messages from a session (params: sessionId, limit?)\n send-message \u2014 Send a message to a session (params: sessionId, message, agent?, variant?, system?)\n create-session \u2014 Create a new session (params: directory?, title?, parentId?)\n get-session-status \u2014 Check if a session is busy/idle (params: sessionId)\n wait-for-idle \u2014 Wait until a session finishes (params: sessionId, timeoutSeconds?)\n\nRequest operations (no UI connection needed):\n list-requests \u2014 List recent requests (params: status?, type?, limit?, offset?)\n get-request \u2014 Get a specific request with its tasks (params: requestId)\n\nNotification operations (no UI connection needed):\n list-notifications \u2014 List recent notifications (params: sessionId?, messageType?, status?, limit?)\n get-notification \u2014 Get a specific notification by code (params: messageCode)\n respond-to-notification \u2014 Answer a question or acknowledge a report (params: messageCode, responseText?, selectedOptions?)\n\nRequires: alchemy action server running (port 3456) and OpenCode running.",inputSchema:(0,o.zodToJsonSchema)(r.CLIBridgeOperationsSchema)}]}))),p.setRequestHandler(i.CallToolRequestSchema,(async e=>{const{name:t,arguments:s={}}=e.params
|
|
92
|
-
if("cli_operations"!==t)return{content:[{type:"text",text:"Unknown tool: "+t}],isError:!0}
|
|
93
|
-
try{const e=r.CLIBridgeOperationsSchema.parse(s)
|
|
94
|
-
return await(0,r.handleCLIBridgeOperations)(e)}catch(e){return{content:[{type:"text",text:"Error: "+e.message}],isError:!0}}})),async function(){u("Starting CLI Bridge MCP server"),u("Action server port: "+d)
|
|
95
|
-
const e=new s.StdioServerTransport
|
|
96
|
-
await p.connect(e),u("CLI Bridge MCP server ready (stdio)")}().catch((e=>{u("Fatal error:",e),process.exit(1)})),process.on("SIGTERM",(()=>process.exit(0))),process.on("SIGINT",(()=>process.exit(0)))})(),module.exports=n})()
|
|
@@ -1,114 +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
|
-
// Cache compiled glob→regex patterns to avoid recompilation on every condition check
|
|
17
|
-
const _globCache = new Map();
|
|
18
|
-
function globToRegex(pattern) {
|
|
19
|
-
const cached = _globCache.get(pattern);
|
|
20
|
-
if (cached)
|
|
21
|
-
return cached;
|
|
22
|
-
let regex = pattern
|
|
23
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
24
|
-
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
25
|
-
.replace(/\*/g, '[^/]*')
|
|
26
|
-
.replace(/\?/g, '.')
|
|
27
|
-
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
28
|
-
const compiled = new RegExp(`^${regex}$`);
|
|
29
|
-
// Cap cache to prevent unbounded growth
|
|
30
|
-
if (_globCache.size > 500)
|
|
31
|
-
_globCache.clear();
|
|
32
|
-
_globCache.set(pattern, compiled);
|
|
33
|
-
return compiled;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Check if a string matches a glob pattern
|
|
37
|
-
*/
|
|
38
|
-
function isMatch(str, pattern) {
|
|
39
|
-
try {
|
|
40
|
-
return globToRegex(pattern).test(str);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Evaluate a single condition against the field value
|
|
48
|
-
*/
|
|
49
|
-
function evaluateSingleCondition(fieldValue, operator, value) {
|
|
50
|
-
switch (operator) {
|
|
51
|
-
case 'matches':
|
|
52
|
-
return isMatch(fieldValue, value);
|
|
53
|
-
case 'not_matches':
|
|
54
|
-
return !isMatch(fieldValue, value);
|
|
55
|
-
case 'contains':
|
|
56
|
-
return fieldValue.includes(value);
|
|
57
|
-
case 'not_contains':
|
|
58
|
-
return !fieldValue.includes(value);
|
|
59
|
-
case 'equals':
|
|
60
|
-
return fieldValue === value;
|
|
61
|
-
case 'not_equals':
|
|
62
|
-
return fieldValue !== value;
|
|
63
|
-
default:
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Evaluate a condition against a paths array (comma-separated)
|
|
69
|
-
* For 'matches'/'contains' - returns true if ANY path matches
|
|
70
|
-
* For 'not_matches'/'not_contains' - returns true if ALL paths don't match
|
|
71
|
-
*/
|
|
72
|
-
function evaluatePathsCondition(paths, operator, value) {
|
|
73
|
-
switch (operator) {
|
|
74
|
-
case 'matches':
|
|
75
|
-
return paths.some(p => isMatch(p, value));
|
|
76
|
-
case 'not_matches':
|
|
77
|
-
return paths.every(p => !isMatch(p, value));
|
|
78
|
-
case 'contains':
|
|
79
|
-
return paths.some(p => p.includes(value));
|
|
80
|
-
case 'not_contains':
|
|
81
|
-
return paths.every(p => !p.includes(value));
|
|
82
|
-
case 'equals':
|
|
83
|
-
return paths.some(p => p === value);
|
|
84
|
-
case 'not_equals':
|
|
85
|
-
return paths.every(p => p !== value);
|
|
86
|
-
default:
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Evaluate hook conditions against current variables
|
|
92
|
-
* Returns true if hook should be triggered, false to skip
|
|
93
|
-
*/
|
|
94
|
-
function evaluateCondition(condition, variables) {
|
|
95
|
-
// No conditions or empty conditions = always match
|
|
96
|
-
if (!condition || !condition.conditions || condition.conditions.length === 0) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
const results = condition.conditions.map((c) => {
|
|
100
|
-
const fieldValue = variables[c.field] || '';
|
|
101
|
-
// For 'paths' field, check if any path matches
|
|
102
|
-
if (c.field === 'paths' && fieldValue.includes(', ')) {
|
|
103
|
-
const paths = fieldValue.split(', ');
|
|
104
|
-
return evaluatePathsCondition(paths, c.operator, c.value);
|
|
105
|
-
}
|
|
106
|
-
// Single value evaluation
|
|
107
|
-
return evaluateSingleCondition(fieldValue, c.operator, c.value);
|
|
108
|
-
});
|
|
109
|
-
// Combine results based on logic (default: AND)
|
|
110
|
-
return condition.logic === 'OR'
|
|
111
|
-
? results.some((r) => r)
|
|
112
|
-
: results.every((r) => r);
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=condition-evaluator.js.map
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deduplication Utilities
|
|
3
|
-
*
|
|
4
|
-
* Functions to prevent duplicate hook triggers when OpenCode fires
|
|
5
|
-
* the same event multiple times in quick succession.
|
|
6
|
-
*/
|
|
7
|
-
// Track recent tool calls with timestamps
|
|
8
|
-
const recentCalls = new Map();
|
|
9
|
-
// Default window for considering calls as duplicates (2 seconds)
|
|
10
|
-
const DEFAULT_DEDUP_WINDOW = 2000;
|
|
11
|
-
/**
|
|
12
|
-
* Create a hash for a tool call based on tool name and output/input
|
|
13
|
-
* Used to identify potentially duplicate calls
|
|
14
|
-
*
|
|
15
|
-
* @param toolName - The name of the tool being called
|
|
16
|
-
* @param text - The output or input text to include in hash (truncated to 100 chars)
|
|
17
|
-
* @returns A string hash identifying this specific call
|
|
18
|
-
*/
|
|
19
|
-
export function createToolCallHash(toolName, text) {
|
|
20
|
-
// Simple hash using tool name + first 100 chars of text
|
|
21
|
-
return `${toolName}:${text?.substring(0, 100) || ''}`;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Check if a tool call is a duplicate of a recent call
|
|
25
|
-
* Also cleans up old entries from the tracking map
|
|
26
|
-
*
|
|
27
|
-
* @param hash - The hash created by createToolCallHash
|
|
28
|
-
* @param now - Current timestamp (allows injection for testing)
|
|
29
|
-
* @param dedupWindow - Time window in ms to consider calls as duplicates (default: 2000ms)
|
|
30
|
-
* @returns true if this appears to be a duplicate call, false otherwise
|
|
31
|
-
*/
|
|
32
|
-
export function isDuplicateToolCall(hash, now = Date.now(), dedupWindow = DEFAULT_DEDUP_WINDOW) {
|
|
33
|
-
const lastTime = recentCalls.get(hash);
|
|
34
|
-
if (lastTime && (now - lastTime) < dedupWindow) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
// Clean up old entries (older than 2x the dedup window)
|
|
38
|
-
const cleanupThreshold = dedupWindow * 2;
|
|
39
|
-
for (const [key, time] of recentCalls.entries()) {
|
|
40
|
-
if (now - time > cleanupThreshold) {
|
|
41
|
-
recentCalls.delete(key);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// Record this call
|
|
45
|
-
recentCalls.set(hash, now);
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Clear all tracked calls (useful for testing)
|
|
50
|
-
*/
|
|
51
|
-
export function clearRecentCalls() {
|
|
52
|
-
recentCalls.clear();
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Get the number of tracked calls (useful for testing)
|
|
56
|
-
*/
|
|
57
|
-
export function getRecentCallsCount() {
|
|
58
|
-
return recentCalls.size;
|
|
59
|
-
}
|
|
60
|
-
// Track last processed message ID per session to prevent duplicate triggers
|
|
61
|
-
const lastProcessedMessageId = new Map();
|
|
62
|
-
/**
|
|
63
|
-
* Atomically check if message was processed and mark it if not.
|
|
64
|
-
* Returns true if this call should proceed (message was NOT already processed).
|
|
65
|
-
* Returns false if should skip (message was already processed by another call).
|
|
66
|
-
*
|
|
67
|
-
* This prevents race conditions where two concurrent calls both check
|
|
68
|
-
* hasProcessedMessage() before either calls markMessageProcessed().
|
|
69
|
-
*/
|
|
70
|
-
export function tryMarkMessageProcessed(sessionId, messageId) {
|
|
71
|
-
if (!messageId)
|
|
72
|
-
return true; // No message ID = proceed (can't dedupe)
|
|
73
|
-
const lastId = lastProcessedMessageId.get(sessionId);
|
|
74
|
-
if (lastId === messageId) {
|
|
75
|
-
// Already processed - skip
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
// Mark as processed immediately (atomic with check above in single-threaded JS)
|
|
79
|
-
lastProcessedMessageId.set(sessionId, messageId);
|
|
80
|
-
// Clean up old entries (keep max 100 sessions)
|
|
81
|
-
if (lastProcessedMessageId.size > 100) {
|
|
82
|
-
const firstKey = lastProcessedMessageId.keys().next().value;
|
|
83
|
-
if (firstKey)
|
|
84
|
-
lastProcessedMessageId.delete(firstKey);
|
|
85
|
-
}
|
|
86
|
-
return true; // Proceed - we claimed this message
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Clear processed message tracking (for testing)
|
|
90
|
-
*/
|
|
91
|
-
export function clearProcessedMessages() {
|
|
92
|
-
lastProcessedMessageId.clear();
|
|
93
|
-
}
|
|
94
|
-
//# sourceMappingURL=dedup-utils.js.map
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook Utility Functions
|
|
3
|
-
*
|
|
4
|
-
* Shared utilities for hook processing including template interpolation
|
|
5
|
-
* and ID extraction from tool outputs.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Interpolate template variables in prompt
|
|
9
|
-
* Replaces {{variableName}} patterns with corresponding values from variables object
|
|
10
|
-
*
|
|
11
|
-
* @param template - The template string with {{variable}} placeholders
|
|
12
|
-
* @param variables - Key-value pairs to substitute into the template
|
|
13
|
-
* @returns The interpolated string with all variables replaced
|
|
14
|
-
*/
|
|
15
|
-
export function interpolatePrompt(template, variables) {
|
|
16
|
-
let result = template;
|
|
17
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
18
|
-
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Extract request ID from tool output
|
|
24
|
-
* Matches patterns like "Request completed: req001234" or "Request req001234 completed successfully"
|
|
25
|
-
*
|
|
26
|
-
* @param text - The tool output text to search
|
|
27
|
-
* @returns The request ID (e.g., "req001234") or null if not found
|
|
28
|
-
*/
|
|
29
|
-
export function extractRequestId(text) {
|
|
30
|
-
const patterns = [
|
|
31
|
-
/Request completed:\s*(req\d+)/i,
|
|
32
|
-
/Request (req\d+) completed/i,
|
|
33
|
-
/completed.*?(req\d{6})/i,
|
|
34
|
-
/Request finalized:\s*(req\d+)/i,
|
|
35
|
-
/Request (req\d+) finalized/i,
|
|
36
|
-
/finalized.*?(req\d{6})/i,
|
|
37
|
-
];
|
|
38
|
-
for (const pattern of patterns) {
|
|
39
|
-
const match = text.match(pattern);
|
|
40
|
-
if (match)
|
|
41
|
-
return match[1];
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Extract task ID from tool output
|
|
47
|
-
* Matches patterns like "Task tas002152 has been marked as completed"
|
|
48
|
-
*
|
|
49
|
-
* @param text - The tool output text to search
|
|
50
|
-
* @returns The task ID (e.g., "tas002152") or null if not found
|
|
51
|
-
*/
|
|
52
|
-
export function extractTaskId(text) {
|
|
53
|
-
const match = text.match(/Task\s+(tas\d+).*(?:completed|marked as completed)/i);
|
|
54
|
-
return match ? match[1] : null;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Extract file path from write_file tool output
|
|
58
|
-
* Matches patterns like "Created file path/to/file.ts"
|
|
59
|
-
*
|
|
60
|
-
* @param text - The tool output text to search
|
|
61
|
-
* @returns The file path or null if not found
|
|
62
|
-
*/
|
|
63
|
-
export function extractFilePath(text) {
|
|
64
|
-
// Match "Created file X" or "**Created file X**"
|
|
65
|
-
const createdMatch = text.match(/\*?\*?Created file\s+([^\s*]+)/i);
|
|
66
|
-
if (createdMatch)
|
|
67
|
-
return createdMatch[1];
|
|
68
|
-
// Match "File Modified: Yes" with path context
|
|
69
|
-
const modifiedMatch = text.match(/File \d+:\s*([^\n]+)\n.*File Modified:\s*Yes/i);
|
|
70
|
-
if (modifiedMatch)
|
|
71
|
-
return modifiedMatch[1];
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Sanitize a value for use in template interpolation
|
|
76
|
-
* Truncates long values and escapes special characters
|
|
77
|
-
*
|
|
78
|
-
* @param value - The value to sanitize
|
|
79
|
-
* @param maxLength - Maximum length before truncation (default: 1000)
|
|
80
|
-
* @returns The sanitized string
|
|
81
|
-
*/
|
|
82
|
-
export function sanitizeTemplateValue(value, maxLength = 1000) {
|
|
83
|
-
if (!value)
|
|
84
|
-
return '';
|
|
85
|
-
if (value.length > maxLength) {
|
|
86
|
-
return value.substring(0, maxLength) + '... (truncated)';
|
|
87
|
-
}
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
//# sourceMappingURL=hook-utils.js.map
|